diff --git a/packages/actions/src/contract-name.ts b/packages/actions/src/contract-name.ts index 8cc6d3403..1d67808e4 100644 --- a/packages/actions/src/contract-name.ts +++ b/packages/actions/src/contract-name.ts @@ -9,7 +9,9 @@ export type ContractName = | 'Governance' | 'GoldToken' | 'LockedGold' + | 'Reserve' | 'ScoreManager' + | 'SortedOracles' | 'StableToken' | 'StableTokenEUR' | 'StableTokenBRL' diff --git a/packages/actions/src/contracts/reserve.ts b/packages/actions/src/contracts/reserve.ts new file mode 100644 index 000000000..918bbbac7 --- /dev/null +++ b/packages/actions/src/contracts/reserve.ts @@ -0,0 +1,19 @@ +import { reserveABI } from '@celo/abis' +import { Address, getContract, GetContractReturnType } from 'viem' +import { Clients } from '../client.js' +import { resolveAddress } from './registry.js' + +export type ReserveContract = GetContractReturnType< + typeof reserveABI, + T +> + +export async function getReserveContract( + clients: T +): Promise> { + return getContract({ + address: await resolveAddress(clients.public, 'Reserve'), + abi: reserveABI, + client: clients, + }) +} diff --git a/packages/actions/src/contracts/sorted-oracles.ts b/packages/actions/src/contracts/sorted-oracles.ts new file mode 100644 index 000000000..abece4947 --- /dev/null +++ b/packages/actions/src/contracts/sorted-oracles.ts @@ -0,0 +1,19 @@ +import { sortedOraclesABI } from '@celo/abis' +import { Address, getContract, GetContractReturnType } from 'viem' +import { Clients } from '../client.js' +import { resolveAddress } from './registry.js' + +export type SortedOraclesContract = GetContractReturnType< + typeof sortedOraclesABI, + T +> + +export async function getSortedOraclesContract( + clients: T +): Promise> { + return getContract({ + address: await resolveAddress(clients.public, 'SortedOracles'), + abi: sortedOraclesABI, + client: clients, + }) +} diff --git a/packages/cli/src/commands/network/parameters.test.ts b/packages/cli/src/commands/network/parameters.test.ts index 70f319e1f..9e2e6ece6 100644 --- a/packages/cli/src/commands/network/parameters.test.ts +++ b/packages/cli/src/commands/network/parameters.test.ts @@ -20,8 +20,8 @@ testWithAnvilL2('network:parameters', (web3) => { maxNumGroupsVotedFor: 10 totalVotes: 60000000000000000000000 (~6.000e+22) EpochManager: - currentEpochNumber: 4 - epochDuration: 86400 + currentEpochNumber: 4 + epochDuration: 86400 (~8.640e+4) isTimeForNextEpoch: false FeeCurrencyDirectory: intrinsicGasForAlternativeFeeCurrency: @@ -42,7 +42,7 @@ testWithAnvilL2('network:parameters', (web3) => { Execution: 1 week Referendum: 1 day LockedCelo: - totalLockedGold: 120000000000000000000000 (~1.200e+23) + totalLockedCelo: 120000000000000000000000 (~1.200e+23) unlockingPeriod: 6 hours Reserve: frozenReserveGoldDays: 0 @@ -54,15 +54,15 @@ testWithAnvilL2('network:parameters', (web3) => { SortedOracles: reportExpiry: 5 minutes Validators: - commissionUpdateDelay: 3 days + commissionUpdateDelay: 14 hours 24 minutes downtimeGracePeriod: 0 - groupLockedGoldRequirements: + groupLockedCeloRequirements: duration: 6 months value: 10000000000000000000000 (~1.000e+22) maxGroupSize: 2 membershipHistoryLength: 60 slashingMultiplierResetPeriod: 1 month - validatorLockedGoldRequirements: + validatorLockedCeloRequirements: duration: 2 months value: 10000000000000000000000 (~1.000e+22)", ], diff --git a/packages/cli/src/commands/network/parameters.ts b/packages/cli/src/commands/network/parameters.ts index f317841c1..a93c070a9 100644 --- a/packages/cli/src/commands/network/parameters.ts +++ b/packages/cli/src/commands/network/parameters.ts @@ -2,6 +2,7 @@ import { Flags } from '@oclif/core' import { BaseCommand } from '../../base' import { printValueMapRecursive } from '../../utils/cli' import { ViewCommmandFlags } from '../../utils/flags' +import { getNetworkConfig } from '../../utils/network' export default class Parameters extends BaseCommand { static description = @@ -17,9 +18,9 @@ export default class Parameters extends BaseCommand { } async run() { - const kit = await this.getKit() + const client = await this.getPublicClient() const res = await this.parse(Parameters) - const config = await kit.getNetworkConfig(!res.flags.raw) + const config = await getNetworkConfig(client, !res.flags.raw) printValueMapRecursive(config) } } diff --git a/packages/cli/src/utils/duration.ts b/packages/cli/src/utils/duration.ts new file mode 100644 index 000000000..6c0595585 --- /dev/null +++ b/packages/cli/src/utils/duration.ts @@ -0,0 +1,51 @@ +import BigNumber from 'bignumber.js' + +function valueToBigNumber(input: BigNumber.Value): BigNumber { + return new BigNumber(input.toString()) +} + +enum TimeDurations { + millennium = 31536000000000, + century = 3153600000000, + decade = 315360000000, + year = 31536000000, + quarter = 7776000000, + month = 2592000000, + week = 604800000, + day = 86400000, + hour = 3600000, + minute = 60000, + second = 1000, + millisecond = 1, +} + +type TimeUnit = keyof typeof TimeDurations + +// taken mostly from https://gist.github.com/RienNeVaPlus/024de3431ae95546d60f2acce128a7e2 +export function secondsToDurationString( + durationSeconds: BigNumber.Value, + outputUnits: TimeUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'] +): string { + let durationMilliseconds = valueToBigNumber(durationSeconds) + .times(TimeDurations.second) + .toNumber() + if (durationMilliseconds <= 0) { + return 'past' + } + const durations = outputUnits.reduce((res: Map, key) => { + const unitDuration = TimeDurations[key] + const value = Math.floor(durationMilliseconds / unitDuration) + durationMilliseconds -= value * unitDuration + return res.set(key, value) + }, new Map()) + let s = '' + durations.forEach((value, unit) => { + if (value > 0) { + s += `${value} ${unit}${value !== 1 ? 's' : ''} ` + } + }) + return s.trim() +} + +export const blocksToDurationString = (input: BigNumber.Value): string => + secondsToDurationString(valueToBigNumber(input).times(1)) // Celo block time is 1 second diff --git a/packages/cli/src/utils/network.ts b/packages/cli/src/utils/network.ts new file mode 100644 index 000000000..98478f2f8 --- /dev/null +++ b/packages/cli/src/utils/network.ts @@ -0,0 +1,448 @@ +import { PublicCeloClient } from '@celo/actions' +import { ElectionContract, getElectionContract } from '@celo/actions/contracts/election' +import { EpochManager, getEpochManagerContract } from '@celo/actions/contracts/epoch-manager' +import { FeeCurrencyDirectory, getFeeCurrencyDirectoryContract } from '@celo/actions/contracts/feecurrency-directory' +import { getGovernanceContract, GovernanceContract } from '@celo/actions/contracts/governance' +import { getLockedCeloContract, LockedCeloContract } from '@celo/actions/contracts/locked-celo' +import { getReserveContract, ReserveContract } from '@celo/actions/contracts/reserve' +import { getSortedOraclesContract, SortedOraclesContract } from '@celo/actions/contracts/sorted-oracles' +import { getValidatorsContract, ValidatorsContract } from '@celo/actions/contracts/validators' +import BigNumber from 'bignumber.js' +import { blocksToDurationString, secondsToDurationString } from './duration' + +// Specific types for each contract's config +export interface ElectionConfig { + electableValidators: { min: BigNumber; max: BigNumber } + electabilityThreshold: BigNumber + maxNumGroupsVotedFor: BigNumber + totalVotes: BigNumber + currentThreshold: BigNumber +} + +export interface GovernanceConfig { + concurrentProposals: BigNumber + dequeueFrequency: BigNumber + minDeposit: BigNumber + queueExpiry: BigNumber + stageDurations: { + approval: BigNumber + referendum: BigNumber + execution: BigNumber + } + participationParameters: { + baseline: BigNumber + baselineFloor: BigNumber + baselineUpdateFactor: BigNumber + baselineQuorumFactor: BigNumber + } +} + +export interface LockedCeloConfig { + unlockingPeriod: BigNumber + totalLockedCelo: BigNumber +} + +export interface SortedOraclesConfig { + reportExpirySeconds: BigNumber +} + +export interface ReserveConfig { + tobinTaxStalenessThreshold: BigNumber + frozenReserveGoldStartBalance: BigNumber + frozenReserveGoldStartDay: BigNumber + frozenReserveGoldDays: BigNumber + otherReserveAddresses: readonly string[] +} + +export interface ValidatorsConfig { + validatorLockedCeloRequirements: { + value: BigNumber + duration: BigNumber + } + groupLockedCeloRequirements: { + value: BigNumber + duration: BigNumber + } + maxGroupSize: BigNumber + membershipHistoryLength: BigNumber + slashingMultiplierResetPeriod: BigNumber + commissionUpdateDelay: BigNumber + downtimeGracePeriod: BigNumber +} + +export interface EpochManagerConfig { + currentEpochNumber: BigNumber + epochDuration: BigNumber + isTimeForNextEpoch: boolean +} + +export interface FeeCurrencyDirectoryConfig { + intrinsicGasForAlternativeFeeCurrency: { + [feeCurrencyAddress: string]: BigNumber + } +} + +export interface NetworkConfig { + Election?: ElectionConfig | Error + Governance?: GovernanceConfig | Error + LockedCelo?: LockedCeloConfig | Error + SortedOracles?: SortedOraclesConfig | Error + Reserve?: ReserveConfig | Error + Validators?: ValidatorsConfig | Error + FeeCurrencyDirectory?: FeeCurrencyDirectoryConfig | Error + EpochManager?: EpochManagerConfig | Error +} + +function valueToBigNumber(input: string | number | bigint): BigNumber { + return new BigNumber(input.toString()) +} + +function fixidityValueToBigNumber(input: string | number | bigint): BigNumber { + const FIXED1 = new BigNumber('1000000000000000000000000') // 10^24 + return new BigNumber(input.toString()).dividedBy(FIXED1) +} + +export async function getNetworkConfig( + client: PublicCeloClient, + humanReadable = false +): Promise { + const contractGetters = { + Election: () => getElectionContract({ public: client }), + Governance: () => getGovernanceContract({ public: client }), + LockedCelo: () => getLockedCeloContract({ public: client }), + SortedOracles: () => getSortedOraclesContract({ public: client }), + Reserve: () => getReserveContract({ public: client }), + Validators: () => getValidatorsContract({ public: client }), + FeeCurrencyDirectory: () => getFeeCurrencyDirectoryContract({ public: client }), + EpochManager: () => getEpochManagerContract({ public: client }), + } + + const configMethod = async (contractName: keyof typeof contractGetters) => { + try { + const contract = await contractGetters[contractName]() + + if (humanReadable && contractName in humanReadableConfigs) { + return await humanReadableConfigs[contractName](contract) + } else if (contractName in configs) { + // @ts-expect-error + return await configs[contractName](contract) + } else { + throw new Error('No config endpoint found') + } + } catch (e) { + return new Error(`Failed to fetch config for contract ${contractName}: \n${e}`) + } + } + + const configArray = await Promise.all( + Object.keys(contractGetters).map((k) => configMethod(k as keyof typeof contractGetters)) + ) + const configMap: NetworkConfig = {} + + Object.keys(contractGetters).forEach((contractName, index) => { + const result = configArray[index] + const key = contractName as keyof NetworkConfig + if (key === 'Election') { + configMap.Election = result as ElectionConfig | Error + } else if (key === 'Governance') { + configMap.Governance = result as GovernanceConfig | Error + } else if (key === 'LockedCelo') { + configMap.LockedCelo = result as LockedCeloConfig | Error + } else if (key === 'SortedOracles') { + configMap.SortedOracles = result as SortedOraclesConfig | Error + } else if (key === 'Reserve') { + configMap.Reserve = result as ReserveConfig | Error + } else if (key === 'Validators') { + configMap.Validators = result as ValidatorsConfig | Error + } else if (key === 'FeeCurrencyDirectory') { + configMap.FeeCurrencyDirectory = result as FeeCurrencyDirectoryConfig | Error + } else if (key === 'EpochManager') { + configMap.EpochManager = result as EpochManagerConfig | Error + } + }) + + return configMap +} + +// Exact replica of each contract's getConfig method +const configs: { + Election: (contract: ElectionContract) => Promise + Governance: ( + contract: GovernanceContract + ) => Promise + LockedCelo: ( + contract: LockedCeloContract + ) => Promise + SortedOracles: ( + contract: SortedOraclesContract + ) => Promise + Reserve: (contract: ReserveContract) => Promise + Validators: ( + contract: ValidatorsContract + ) => Promise + FeeCurrencyDirectory: ( + contract: FeeCurrencyDirectory + ) => Promise + EpochManager: ( + contract: EpochManager + ) => Promise +} = { + Election: async ( + contract: ElectionContract + ): Promise => { + const [electableValidators, electabilityThreshold, maxNumGroupsVotedFor, totalVotes] = + await Promise.all([ + contract.read.getElectableValidators(), + contract.read.getElectabilityThreshold(), + contract.read.maxNumGroupsVotedFor(), + contract.read.getTotalVotes(), + ]) + + const electableValidatorsResult = { + min: valueToBigNumber(electableValidators[0]), + max: valueToBigNumber(electableValidators[1]), + } + const totalVotesResult = valueToBigNumber(totalVotes) + + const electabilityThresholdResult = fixidityValueToBigNumber(electabilityThreshold) + + return { + electableValidators: electableValidatorsResult, + electabilityThreshold: electabilityThresholdResult, + maxNumGroupsVotedFor: valueToBigNumber(maxNumGroupsVotedFor), + totalVotes: totalVotesResult, + currentThreshold: totalVotesResult.multipliedBy(electabilityThresholdResult), + } + }, + + Governance: async ( + contract: GovernanceContract + ): Promise => { + const [ + concurrentProposals, + dequeueFrequency, + minDeposit, + queueExpiry, + stageDurations, + participationParameters, + ] = await Promise.all([ + contract.read.concurrentProposals(), + contract.read.dequeueFrequency(), + contract.read.minDeposit(), + contract.read.queueExpiry(), + contract.read.stageDurations(), + contract.read.getParticipationParameters(), + ]) + + return { + concurrentProposals: valueToBigNumber(concurrentProposals), + dequeueFrequency: valueToBigNumber(dequeueFrequency), + minDeposit: valueToBigNumber(minDeposit), + queueExpiry: valueToBigNumber(queueExpiry), + stageDurations: stageDurations + ? { + approval: valueToBigNumber( stageDurations[0]), + referendum: valueToBigNumber(stageDurations[1]), + execution: valueToBigNumber(stageDurations[2]), + } + : { + approval: valueToBigNumber(0), + referendum: valueToBigNumber(0), + execution: valueToBigNumber(0), + }, + participationParameters: participationParameters + ? { + baseline: fixidityValueToBigNumber(participationParameters[0] || 0), + baselineFloor: fixidityValueToBigNumber(participationParameters[1] || 0), + baselineUpdateFactor: fixidityValueToBigNumber(participationParameters[2] || 0), + baselineQuorumFactor: fixidityValueToBigNumber(participationParameters[3] || 0), + } + : { + baseline: fixidityValueToBigNumber(0), + baselineFloor: fixidityValueToBigNumber(0), + baselineUpdateFactor: fixidityValueToBigNumber(0), + baselineQuorumFactor: fixidityValueToBigNumber(0), + }, + } + }, + + LockedCelo: async ( + contract: LockedCeloContract + ): Promise => { + const [unlockingPeriod, totalLockedCelo] = await Promise.all([ + contract.read.unlockingPeriod(), + contract.read.getTotalLockedGold(), + ]) + + return { + unlockingPeriod: valueToBigNumber(unlockingPeriod), + totalLockedCelo: valueToBigNumber(totalLockedCelo), + } + }, + + SortedOracles: async ( + contract: SortedOraclesContract + ): Promise => { + const reportExpirySeconds = await contract.read.reportExpirySeconds() + + return { + reportExpirySeconds: valueToBigNumber(reportExpirySeconds), + } + }, + + Reserve: async ( + contract: Awaited> + ): Promise => { + const [ + tobinTaxStalenessThreshold, + frozenReserveGoldStartBalance, + frozenReserveGoldStartDay, + frozenReserveGoldDays, + otherReserveAddresses, + ] = await Promise.all([ + contract.read.tobinTaxStalenessThreshold(), + contract.read.frozenReserveGoldStartBalance(), + contract.read.frozenReserveGoldStartDay(), + contract.read.frozenReserveGoldDays(), + contract.read.getOtherReserveAddresses(), + ]) + + return { + tobinTaxStalenessThreshold: valueToBigNumber(tobinTaxStalenessThreshold), + frozenReserveGoldStartBalance: valueToBigNumber(frozenReserveGoldStartBalance), + frozenReserveGoldStartDay: valueToBigNumber(frozenReserveGoldStartDay), + frozenReserveGoldDays: valueToBigNumber(frozenReserveGoldDays), + otherReserveAddresses, + } + }, + + Validators: async ( + contract: ValidatorsContract + ): Promise => { + const [ + validatorLockedCeloRequirements, + groupLockedCeloRequirements, + maxGroupSize, + membershipHistoryLength, + slashingMultiplierResetPeriod, + commissionUpdateDelay, + downtimeGracePeriod, + ] = await Promise.all([ + contract.read.getValidatorLockedGoldRequirements(), + contract.read.getGroupLockedGoldRequirements(), + contract.read.maxGroupSize(), + contract.read.membershipHistoryLength(), + contract.read.slashingMultiplierResetPeriod(), + contract.read.commissionUpdateDelay(), + contract.read.deprecated_downtimeGracePeriod(), + ]) + + return { + validatorLockedCeloRequirements: { + value: valueToBigNumber(validatorLockedCeloRequirements[0]), + duration: valueToBigNumber(validatorLockedCeloRequirements[1]), + }, + groupLockedCeloRequirements: { + value: valueToBigNumber(groupLockedCeloRequirements[0]), + duration: valueToBigNumber(groupLockedCeloRequirements[1]), + }, + maxGroupSize: valueToBigNumber(maxGroupSize), + membershipHistoryLength: valueToBigNumber(membershipHistoryLength), + slashingMultiplierResetPeriod: valueToBigNumber(slashingMultiplierResetPeriod), + commissionUpdateDelay: valueToBigNumber(commissionUpdateDelay), + downtimeGracePeriod: valueToBigNumber(downtimeGracePeriod), + } + }, + + FeeCurrencyDirectory: async ( + contract: Awaited> + ): Promise => { + const addresses = await contract.read.getCurrencies() + const config: FeeCurrencyDirectoryConfig = { intrinsicGasForAlternativeFeeCurrency: {} } + + for (const address of addresses) { + const currencyConfig = await contract.read.getCurrencyConfig([address]) + config.intrinsicGasForAlternativeFeeCurrency[address] = valueToBigNumber( + currencyConfig.intrinsicGas + ) + } + + return config + }, + + EpochManager: async ( + contract: Awaited> + ): Promise => { + const [currentEpochNumber, epochDuration, isTimeForNextEpoch] = await Promise.all([ + contract.read.getCurrentEpochNumber(), + contract.read.epochDuration(), + contract.read.isTimeForNextEpoch(), + ]) + + return { + currentEpochNumber: valueToBigNumber(currentEpochNumber), + epochDuration: valueToBigNumber(epochDuration), + isTimeForNextEpoch, + } + }, +} + +// Human readable versions - convert durations to readable strings +const humanReadableConfigs = { + Election: configs.Election, + + Governance: async (contract: GovernanceContract) => { + const config = await configs.Governance(contract) + return { + ...config, + dequeueFrequency: secondsToDurationString(config.dequeueFrequency), + participationParameters: { + ...config.participationParameters, + }, + queueExpiry: secondsToDurationString(config.queueExpiry), + stageDurations: { + Execution: blocksToDurationString(config.stageDurations.execution), + Referendum: blocksToDurationString(config.stageDurations.referendum), + }, + } + }, + + LockedCelo: async (contract: LockedCeloContract) => { + const config = await configs.LockedCelo(contract) + return { + ...config, + unlockingPeriod: secondsToDurationString(config.unlockingPeriod), + } + }, + + SortedOracles: async (contract: Awaited>) => { + const config = await configs.SortedOracles(contract) + return { + reportExpiry: secondsToDurationString(config.reportExpirySeconds), + } + }, + + Reserve: configs.Reserve, // No duration fields to convert + + Validators: async (contract: ValidatorsContract) => { + const config = await configs.Validators(contract) + return { + ...config, + validatorLockedCeloRequirements: { + ...config.validatorLockedCeloRequirements, + duration: secondsToDurationString(config.validatorLockedCeloRequirements.duration), + }, + groupLockedCeloRequirements: { + ...config.groupLockedCeloRequirements, + duration: secondsToDurationString(config.groupLockedCeloRequirements.duration), + }, + slashingMultiplierResetPeriod: secondsToDurationString(config.slashingMultiplierResetPeriod), + commissionUpdateDelay: secondsToDurationString(config.commissionUpdateDelay), + downtimeGracePeriod: config.downtimeGracePeriod, + } + }, + + FeeCurrencyDirectory: configs.FeeCurrencyDirectory, + + EpochManager: configs.EpochManager, +}