diff --git a/packages/node/src/helpers/multiply.ts b/packages/node/src/helpers/multiply.ts deleted file mode 100644 index 084c764f7a..0000000000 --- a/packages/node/src/helpers/multiply.ts +++ /dev/null @@ -1,6 +0,0 @@ -// this precision is big enough as we typically handle bigints which are in weis (the smallest denomination of Ethereum) -const PRECISION = 1e18 - -export const multiply = (val1: bigint, val2: number): bigint => { - return val1 * BigInt(PRECISION * val2) / BigInt(PRECISION) -} diff --git a/packages/node/src/plugins/operator/OperatorPlugin.ts b/packages/node/src/plugins/operator/OperatorPlugin.ts index fbb9028325..ed4ac85b90 100644 --- a/packages/node/src/plugins/operator/OperatorPlugin.ts +++ b/packages/node/src/plugins/operator/OperatorPlugin.ts @@ -164,7 +164,7 @@ export class OperatorPlugin extends Plugin { try { await maintainOperatorValue( this.pluginConfig.maintainOperatorValue.withdrawLimitSafetyFraction, - this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw, + BigInt(this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw), this.pluginConfig.maintainOperatorValue.maxSponsorshipsInWithdraw, operator ) @@ -215,7 +215,7 @@ export class OperatorPlugin extends Plugin { operator, streamrClient, () => stakedOperatorsCache.get(), - this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw, + BigInt(this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw), this.pluginConfig.maintainOperatorValue.maxSponsorshipsInWithdraw ).catch((err) => { logger.warn('Encountered error', { err }) diff --git a/packages/node/src/plugins/operator/checkOperatorValueBreach.ts b/packages/node/src/plugins/operator/checkOperatorValueBreach.ts index 2e56f54b3c..ed049fd397 100644 --- a/packages/node/src/plugins/operator/checkOperatorValueBreach.ts +++ b/packages/node/src/plugins/operator/checkOperatorValueBreach.ts @@ -1,5 +1,5 @@ import { StreamrClient, Operator } from '@streamr/sdk' -import { EthereumAddress, Logger } from '@streamr/utils' +import { EthereumAddress, Logger, WeiAmount } from '@streamr/utils' import { sample, without } from 'lodash' const logger = new Logger(module) @@ -8,7 +8,7 @@ export const checkOperatorValueBreach = async ( myOperator: Operator, client: StreamrClient, getStakedOperators: () => Promise, - minSponsorshipEarningsInWithdraw: number, + minSponsorshipEarningsInWithdraw: WeiAmount, maxSponsorshipsInWithdraw: number ): Promise => { const targetOperatorAddress = sample(without(await getStakedOperators(), await myOperator.getContractAddress())) @@ -17,14 +17,19 @@ export const checkOperatorValueBreach = async ( return } logger.info('Check other operator\'s earnings for breach', { targetOperatorAddress }) - const { sumDataWei, maxAllowedEarningsDataWei, sponsorshipAddresses } = await client.getOperator(targetOperatorAddress).getEarnings( + const { sum, maxAllowedEarnings, sponsorshipAddresses } = await client.getOperator(targetOperatorAddress).getEarnings( minSponsorshipEarningsInWithdraw, maxSponsorshipsInWithdraw ) - logger.trace(` -> is ${sumDataWei} > ${maxAllowedEarningsDataWei}?`) - if (sumDataWei > maxAllowedEarningsDataWei) { + logger.trace(` -> is ${sum} > ${maxAllowedEarnings}?`) + if (sum > maxAllowedEarnings) { logger.info('Withdraw earnings from sponsorships (target operator value in breach)', - { targetOperatorAddress, sponsorshipAddresses, sumDataWei, maxAllowedEarningsDataWei }) + { + targetOperatorAddress, + sponsorshipAddresses, + sum: sum.toString(), + maxAllowedEarnings: maxAllowedEarnings.toString() + }) await myOperator.triggerAnotherOperatorWithdraw(targetOperatorAddress, sponsorshipAddresses) } } diff --git a/packages/node/src/plugins/operator/maintainOperatorValue.ts b/packages/node/src/plugins/operator/maintainOperatorValue.ts index 3525ec294f..dd9f603870 100644 --- a/packages/node/src/plugins/operator/maintainOperatorValue.ts +++ b/packages/node/src/plugins/operator/maintainOperatorValue.ts @@ -1,23 +1,22 @@ import { Operator } from '@streamr/sdk' -import { Logger } from '@streamr/utils' -import { multiply } from '../../helpers/multiply' +import { Logger, multiplyWeiAmount, WeiAmount } from '@streamr/utils' const logger = new Logger(module) export const maintainOperatorValue = async ( withdrawLimitSafetyFraction: number, - minSponsorshipEarningsInWithdraw: number, + minSponsorshipEarningsInWithdraw: WeiAmount, maxSponsorshipsInWithdraw: number, myOperator: Operator ): Promise => { logger.info('Check whether it is time to withdraw my earnings') - const { sumDataWei, maxAllowedEarningsDataWei, sponsorshipAddresses } = await myOperator.getEarnings( + const { sum, maxAllowedEarnings, sponsorshipAddresses } = await myOperator.getEarnings( minSponsorshipEarningsInWithdraw, maxSponsorshipsInWithdraw ) - const triggerWithdrawLimitDataWei = multiply(maxAllowedEarningsDataWei, 1 - withdrawLimitSafetyFraction) - logger.trace(` -> is ${sumDataWei} > ${triggerWithdrawLimitDataWei} ?`) - if (sumDataWei > triggerWithdrawLimitDataWei) { + const triggerWithdrawLimit = multiplyWeiAmount(maxAllowedEarnings, 1 - withdrawLimitSafetyFraction) + logger.trace(` -> is ${sum} > ${triggerWithdrawLimit} ?`) + if (sum > triggerWithdrawLimit) { logger.info('Withdraw earnings from sponsorships', { sponsorshipAddresses }) await myOperator.withdrawEarningsFromSponsorships(sponsorshipAddresses) } else { diff --git a/packages/node/test/integration/plugins/operator/MaintainTopologyService.test.ts b/packages/node/test/integration/plugins/operator/MaintainTopologyService.test.ts index 44e2661299..152660c5b9 100644 --- a/packages/node/test/integration/plugins/operator/MaintainTopologyService.test.ts +++ b/packages/node/test/integration/plugins/operator/MaintainTopologyService.test.ts @@ -9,6 +9,7 @@ import { OperatorFleetState } from '../../../../src/plugins/operator/OperatorFle import { StreamPartAssignments } from '../../../../src/plugins/operator/StreamPartAssignments' import { formCoordinationStreamId } from '../../../../src/plugins/operator/formCoordinationStreamId' import { createClient, createTestStream } from '../../../utils' +import { parseEther } from 'ethers' const { delegate, @@ -70,8 +71,8 @@ describe('MaintainTopologyService', () => { const sponsorship1 = await deploySponsorshipContract({ deployer: operatorWallet, streamId: stream1.id }) const sponsorship2 = await deploySponsorshipContract({ deployer: operatorWallet, streamId: stream2.id }) const operatorContract = await deployOperatorContract({ deployer: operatorWallet }) - await delegate(operatorWallet, await operatorContract.getAddress(), 20000) - await stake(operatorContract, await sponsorship1.getAddress(), 10000) + await delegate(operatorWallet, await operatorContract.getAddress(), parseEther('20000')) + await stake(operatorContract, await sponsorship1.getAddress(), parseEther('10000')) const createOperatorFleetState = OperatorFleetState.createOperatorFleetStateBuilder( client, @@ -104,7 +105,7 @@ describe('MaintainTopologyService', () => { return containsAll(await getSubscribedStreamPartIds(client), await stream1.getStreamParts()) }, 10000, 1000) - await stake(operatorContract, await sponsorship2.getAddress(), 10000) + await stake(operatorContract, await sponsorship2.getAddress(), parseEther('10000')) await until(async () => { return containsAll(await getSubscribedStreamPartIds(client), [ ...await stream1.getStreamParts(), diff --git a/packages/node/test/integration/plugins/operator/OperatorPlugin.test.ts b/packages/node/test/integration/plugins/operator/OperatorPlugin.test.ts index 3c65c89b04..a6f6328c95 100644 --- a/packages/node/test/integration/plugins/operator/OperatorPlugin.test.ts +++ b/packages/node/test/integration/plugins/operator/OperatorPlugin.test.ts @@ -6,7 +6,7 @@ import { } from '@streamr/sdk' import { fastPrivateKey, fetchPrivateKeyWithGas } from '@streamr/test-utils' import { EthereumAddress, collect, toEthereumAddress, toStreamPartID, until } from '@streamr/utils' -import { Wallet } from 'ethers' +import { parseEther, Wallet } from 'ethers' import { cloneDeep, set } from 'lodash' import { Broker, createBroker } from '../../../../src/broker' import { formCoordinationStreamId } from '../../../../src/plugins/operator/formCoordinationStreamId' @@ -56,9 +56,9 @@ describe('OperatorPlugin', () => { const sponsorer = await generateWalletWithGasAndTokens() const sponsorship1 = await deploySponsorshipContract({ streamId: stream.id, deployer: sponsorer }) - await sponsor(sponsorer, await sponsorship1.getAddress(), 10000) - await delegate(operatorWallet, await operatorContract.getAddress(), 10000) - await stake(operatorContract, await sponsorship1.getAddress(), 10000) + await sponsor(sponsorer, await sponsorship1.getAddress(), parseEther('10000')) + await delegate(operatorWallet, await operatorContract.getAddress(), parseEther('10000')) + await stake(operatorContract, await sponsorship1.getAddress(), parseEther('10000')) const publisher = createClient(fastPrivateKey()) await stream.grantPermissions({ @@ -114,9 +114,9 @@ describe('OperatorPlugin', () => { const sponsorer = await generateWalletWithGasAndTokens() const sponsorship = await deploySponsorshipContract({ streamId: stream.id, deployer: sponsorer }) - await sponsor(sponsorer, await sponsorship.getAddress(), 10000) - await delegate(operatorWallet, await operatorContract.getAddress(), 10000) - await stake(operatorContract, await sponsorship.getAddress(), 10000) + await sponsor(sponsorer, await sponsorship.getAddress(), parseEther('10000')) + await delegate(operatorWallet, await operatorContract.getAddress(), parseEther('10000')) + await stake(operatorContract, await sponsorship.getAddress(), parseEther('10000')) const operatorContractAddress = await operatorContract.getAddress() broker = await startBroker({ diff --git a/packages/node/test/integration/plugins/operator/checkOperatorValueBreach.test.ts b/packages/node/test/integration/plugins/operator/checkOperatorValueBreach.test.ts index c5e6289308..b184a5860b 100644 --- a/packages/node/test/integration/plugins/operator/checkOperatorValueBreach.test.ts +++ b/packages/node/test/integration/plugins/operator/checkOperatorValueBreach.test.ts @@ -4,7 +4,7 @@ import { _operatorContractUtils, } from '@streamr/sdk' import { Logger, toEthereumAddress, until } from '@streamr/utils' -import { Contract } from 'ethers' +import { Contract, parseEther } from 'ethers' import { checkOperatorValueBreach } from '../../../../src/plugins/operator/checkOperatorValueBreach' import { createClient, createTestStream } from '../../../utils' @@ -39,7 +39,7 @@ describe('checkOperatorValueBreach', () => { await client.destroy() deployConfig = { operatorConfig: { - operatorsCutPercent: 10 + operatorsCutPercentage: 10 } } }, 60 * 1000) @@ -49,13 +49,13 @@ describe('checkOperatorValueBreach', () => { const { operatorContract: watcherOperatorContract, nodeWallets: watcherWallets } = await setupOperatorContract({ nodeCount: 1, ...deployConfig }) const { operatorWallet, operatorContract } = await setupOperatorContract(deployConfig) const sponsorer = await generateWalletWithGasAndTokens() - await delegate(operatorWallet, await operatorContract.getAddress(), 20000) - const sponsorship1 = await deploySponsorshipContract({ earningsPerSecond: 100, streamId, deployer: operatorWallet }) - await sponsor(sponsorer, await sponsorship1.getAddress(), 25000) - await stake(operatorContract, await sponsorship1.getAddress(), 10000) - const sponsorship2 = await deploySponsorshipContract({ earningsPerSecond: 200, streamId, deployer: operatorWallet }) - await sponsor(sponsorer, await sponsorship2.getAddress(), 25000) - await stake(operatorContract, await sponsorship2.getAddress(), 10000) + await delegate(operatorWallet, await operatorContract.getAddress(), parseEther('20000')) + const sponsorship1 = await deploySponsorshipContract({ earningsPerSecond: parseEther('100'), streamId, deployer: operatorWallet }) + await sponsor(sponsorer, await sponsorship1.getAddress(), parseEther('25000')) + await stake(operatorContract, await sponsorship1.getAddress(), parseEther('10000')) + const sponsorship2 = await deploySponsorshipContract({ earningsPerSecond: parseEther('200'), streamId, deployer: operatorWallet }) + await sponsor(sponsorer, await sponsorship2.getAddress(), parseEther('25000')) + await stake(operatorContract, await sponsorship2.getAddress(), parseEther('10000')) const valueBeforeWithdraw = await operatorContract.valueWithoutEarnings() const streamrConfigAddress = await operatorContract.streamrConfig() const streamrConfig = new Contract(streamrConfigAddress, streamrConfigABI, getProvider()) as unknown as StreamrConfig @@ -67,7 +67,7 @@ describe('checkOperatorValueBreach', () => { await until(async () => await getEarnings(operatorContract) > allowedDifference, 10000, 1000) await checkOperatorValueBreach(operator, client, async () => { return [toEthereumAddress(await operatorContract.getAddress())] - }, 1, 20) + }, 1n, 20) const earnings = await getEarnings(operatorContract) expect(earnings).toBeLessThan(allowedDifference) diff --git a/packages/node/test/integration/plugins/operator/maintainOperatorValue.test.ts b/packages/node/test/integration/plugins/operator/maintainOperatorValue.test.ts index 0db255bf55..bff2683916 100644 --- a/packages/node/test/integration/plugins/operator/maintainOperatorValue.test.ts +++ b/packages/node/test/integration/plugins/operator/maintainOperatorValue.test.ts @@ -1,7 +1,7 @@ import { _operatorContractUtils } from '@streamr/sdk' import { fetchPrivateKeyWithGas } from '@streamr/test-utils' -import { Logger, toEthereumAddress, until } from '@streamr/utils' -import { multiply } from '../../../../src/helpers/multiply' +import { Logger, multiplyWeiAmount, toEthereumAddress, until } from '@streamr/utils' +import { parseEther } from 'ethers' import { maintainOperatorValue } from '../../../../src/plugins/operator/maintainOperatorValue' import { createClient, createTestStream } from '../../../utils' @@ -16,7 +16,7 @@ const { const logger = new Logger(module) -const STAKE_AMOUNT = 10000 +const STAKE_AMOUNT = parseEther('10000') const SAFETY_FRACTION = 0.5 // 50% describe('maintainOperatorValue', () => { @@ -40,27 +40,26 @@ describe('maintainOperatorValue', () => { const { operatorWallet, operatorContract, nodeWallets } = await setupOperatorContract({ nodeCount: 1, operatorConfig: { - operatorsCutPercent: 10 + operatorsCutPercentage: 10 } }) const sponsorer = await generateWalletWithGasAndTokens() - const sponsorship = await deploySponsorshipContract({ earningsPerSecond: 100, streamId, deployer: operatorWallet }) - await sponsor(sponsorer, await sponsorship.getAddress(), 25000) + const sponsorship = await deploySponsorshipContract({ earningsPerSecond: parseEther('100'), streamId, deployer: operatorWallet }) + await sponsor(sponsorer, await sponsorship.getAddress(), parseEther('25000')) await delegate(operatorWallet, await operatorContract.getAddress(), STAKE_AMOUNT) await stake(operatorContract, await sponsorship.getAddress(), STAKE_AMOUNT) const operator = createClient(nodeWallets[0].privateKey).getOperator(toEthereumAddress(await operatorContract.getAddress())) - const { maxAllowedEarningsDataWei } = await operator.getEarnings(1, 20) - const triggerWithdrawLimitDataWei = multiply(maxAllowedEarningsDataWei, 1 - SAFETY_FRACTION) + const { maxAllowedEarnings } = await operator.getEarnings(1n, 20) + const triggerWithdrawLimit = multiplyWeiAmount(maxAllowedEarnings, 1 - SAFETY_FRACTION) await until(async () => { - const { sumDataWei } = await operator.getEarnings(1, 20) - const earnings = sumDataWei - return earnings > triggerWithdrawLimitDataWei + const { sum } = await operator.getEarnings(1n, 20) + return sum > triggerWithdrawLimit }, 10000, 1000) const valueBeforeWithdraw = await operatorContract.valueWithoutEarnings() await maintainOperatorValue( SAFETY_FRACTION, - 1, + 1n, 20, operator ) diff --git a/packages/node/test/smoke/inspect.test.ts b/packages/node/test/smoke/inspect.test.ts index 2b53cc3767..9748f48a8d 100644 --- a/packages/node/test/smoke/inspect.test.ts +++ b/packages/node/test/smoke/inspect.test.ts @@ -2,8 +2,8 @@ import { config as CHAIN_CONFIG } from '@streamr/config' import { StreamrConfig, streamrConfigABI } from '@streamr/network-contracts' import { _operatorContractUtils } from '@streamr/sdk' import { fetchPrivateKeyWithGas } from '@streamr/test-utils' -import { Logger, StreamID, TheGraphClient, wait, until } from '@streamr/utils' -import { Contract, formatEther, JsonRpcProvider, parseEther, Wallet } from 'ethers' +import { Logger, StreamID, TheGraphClient, wait, until, multiplyWeiAmount } from '@streamr/utils' +import { Contract, JsonRpcProvider, parseEther, Wallet } from 'ethers' import { Broker, createBroker } from '../../src/broker' import { createClient, createTestStream, formConfig } from '../utils' import { OperatorPluginConfig } from './../../src/plugins/operator/OperatorPlugin' @@ -61,11 +61,11 @@ const CLOSE_EXPIRED_FLAGS_MAX_AGE = 30 * 1000 const VALID_OPERATOR_COUNT = 3 // one flagger and at least two voters are needed (see VoteKickPolicy.sol:166) const MAX_TEST_RUN_TIME = 15 * 60 * 1000 -const DELEGATE_WEI = 50000 -const STAKE_WEI = 10000 -const REVIEWER_REWARD_WEI = 700 -const FLAGGER_REWARD_WEI = 900 -const SLASHING_FRACTION = 0.25 +const DELEGATE_AMOUNT = parseEther('50000') +const STAKE_AMOUNT = parseEther('10000') +const REVIEWER_REWARD_AMOUNT = parseEther('700') +const FLAGGER_REWARD_AMOUNT = parseEther('900') +const SLASHING_PERCENTAGE = 25 // two operators and a sponsorship which have been created in dev-chain init const PRE_BAKED_OPERATORS = [{ @@ -97,8 +97,8 @@ const createOperator = async ( metadata: JSON.stringify({ redundancyFactor: 1 }) } }) - await delegate(operator.operatorWallet, await operator.operatorContract.getAddress(), DELEGATE_WEI) - await stake(operator.operatorContract, sponsorshipAddress, STAKE_WEI) + await delegate(operator.operatorWallet, await operator.operatorContract.getAddress(), DELEGATE_AMOUNT) + await stake(operator.operatorContract, sponsorshipAddress, STAKE_AMOUNT) const node = await createBroker(formConfig({ privateKey: operator.nodeWallets[0].privateKey, extraPlugins: { @@ -190,8 +190,8 @@ const getOperatorStakeCount = async (operatorContractAddress: string, theGraphCl return response.operator.stakes.length } -const getTokenBalance = async (address: string, token: any): Promise => { - return Number(formatEther(await token.balanceOf(address))) +const getTokenBalance = async (address: string, token: any): Promise => { + return await token.balanceOf(address) } describe('inspect', () => { @@ -214,13 +214,13 @@ describe('inspect', () => { await streamrConfig.setReviewPeriodSeconds(REVIEW_PERIOD) await streamrConfig.setVotingPeriodSeconds(VOTING_PERIOD) await streamrConfig.setFlagProtectionSeconds(0) - await streamrConfig.setFlagReviewerRewardWei(parseEther(String(REVIEWER_REWARD_WEI))) - await streamrConfig.setFlaggerRewardWei(parseEther(String(FLAGGER_REWARD_WEI))) - await streamrConfig.setSlashingFraction(parseEther(String(SLASHING_FRACTION))) + await streamrConfig.setFlagReviewerRewardWei(REVIEWER_REWARD_AMOUNT) + await streamrConfig.setFlaggerRewardWei(FLAGGER_REWARD_AMOUNT) + await streamrConfig.setSlashingFraction(parseEther(String(SLASHING_PERCENTAGE / 100))) logger.info('Setup sponsorship') const streamId = await createStream() const sponsorer = await generateWalletWithGasAndTokens() - const sponsorship = await deploySponsorshipContract({ earningsPerSecond: 0, streamId, deployer: sponsorer }) + const sponsorship = await deploySponsorshipContract({ earningsPerSecond: 0n, streamId, deployer: sponsorer }) logger.info('Create operators') freeriderOperator = await createOperator({}, await sponsorship.getAddress(), true) const CONFIG = { @@ -311,10 +311,12 @@ describe('inspect', () => { // assert slashing and rewards const token = getTestTokenContract().connect(getProvider()) - expect(await getTokenBalance(freeriderOperator.contractAddress, token)).toEqual(DELEGATE_WEI - SLASHING_FRACTION * STAKE_WEI) - expect(await getTokenBalance(flags[0].flagger, token)).toEqual(DELEGATE_WEI - STAKE_WEI + FLAGGER_REWARD_WEI) + expect(await getTokenBalance(freeriderOperator.contractAddress, token)).toEqual( + DELEGATE_AMOUNT - multiplyWeiAmount(STAKE_AMOUNT, SLASHING_PERCENTAGE / 100) + ) + expect(await getTokenBalance(flags[0].flagger, token)).toEqual(DELEGATE_AMOUNT - STAKE_AMOUNT + FLAGGER_REWARD_AMOUNT) for (const voter of flags[0].votes.map((vote) => vote.voter)) { - expect(await getTokenBalance(voter, token)).toEqual(DELEGATE_WEI - STAKE_WEI + REVIEWER_REWARD_WEI) + expect(await getTokenBalance(voter, token)).toEqual(DELEGATE_AMOUNT - STAKE_AMOUNT + REVIEWER_REWARD_AMOUNT) } }, 1.1 * MAX_TEST_RUN_TIME) diff --git a/packages/node/test/smoke/profit.test.ts b/packages/node/test/smoke/profit.test.ts index 25022dd3a9..2b45d443ab 100644 --- a/packages/node/test/smoke/profit.test.ts +++ b/packages/node/test/smoke/profit.test.ts @@ -3,8 +3,8 @@ import type { Operator, Sponsorship } from '@streamr/network-contracts' import { StreamrConfig, streamrConfigABI } from '@streamr/network-contracts' import { _operatorContractUtils } from '@streamr/sdk' import { fetchPrivateKeyWithGas } from '@streamr/test-utils' -import { until } from '@streamr/utils' -import { Contract, Wallet, formatEther, parseEther } from 'ethers' +import { multiplyWeiAmount, until, WeiAmount } from '@streamr/utils' +import { Contract, Wallet, parseEther } from 'ethers' import { createClient, createTestStream, startBroker } from '../utils' /* @@ -46,16 +46,16 @@ const { getTestAdminWallet } = _operatorContractUtils -const SPONSOR_AMOUNT = 6000 -const OPERATOR_DELEGATED_AMOUNT = 5000 -const EXTERNAL_DELEGATED_AMOUNT = 5260 -const EARNINGS_PER_SECOND = 1000 +const SPONSOR_AMOUNT = parseEther('6000') +const OPERATOR_DELEGATED_AMOUNT = parseEther('5000') +const EXTERNAL_DELEGATED_AMOUNT = parseEther('5260') +const EARNINGS_PER_SECOND = parseEther('1000') const OPERATORS_CUT_PERCENTAGE = 10 const PROTOCOL_FEE_PERCENTAGE = 5 -const PROTOCOL_FEE = SPONSOR_AMOUNT * (PROTOCOL_FEE_PERCENTAGE / 100) +const PROTOCOL_FEE = multiplyWeiAmount(SPONSOR_AMOUNT, PROTOCOL_FEE_PERCENTAGE / 100) const TOTAL_PROFIT = SPONSOR_AMOUNT - PROTOCOL_FEE const TOTAL_DELEGATED = OPERATOR_DELEGATED_AMOUNT + EXTERNAL_DELEGATED_AMOUNT -const OPERATORS_CUT = TOTAL_PROFIT * (OPERATORS_CUT_PERCENTAGE / 100) +const OPERATORS_CUT = multiplyWeiAmount(TOTAL_PROFIT, OPERATORS_CUT_PERCENTAGE / 100) const OPERATOR_PROFIT_WHEN_NO_WITHDRAWALS = (TOTAL_PROFIT - OPERATORS_CUT) * OPERATOR_DELEGATED_AMOUNT / TOTAL_DELEGATED + OPERATORS_CUT const DELEGATOR_PROFIT_WHEN_NO_WITHDRAWALS = (TOTAL_PROFIT - OPERATORS_CUT) * EXTERNAL_DELEGATED_AMOUNT / TOTAL_DELEGATED // If the operator doesn't make any withdrawals during the sponsorship period, the profit is split between @@ -63,7 +63,7 @@ const DELEGATOR_PROFIT_WHEN_NO_WITHDRAWALS = (TOTAL_PROFIT - OPERATORS_CUT) * EX // the operator gets a larger share of the profit. This happens because the operator's delegated amount // grows by both their profit share and their cut of the total profit, while the external delegator's amount // only grows by their profit share. -const PROFIT_INACCURACY = 50 +const PROFIT_INACCURACY = parseEther('50') describe('profit', () => { @@ -75,20 +75,20 @@ describe('profit', () => { let sponsorshipContract: Sponsorship const getBalances = async (): Promise<{ - operator: number - delegator: number - sponsor: number - admin: number - operatorContract: number + operator: WeiAmount + delegator: WeiAmount + sponsor: WeiAmount + admin: WeiAmount + operatorContract: WeiAmount }> => { const dataToken = getTestTokenContract().connect(getProvider()) const adminWallet = getTestAdminWallet() return { - operator: Number(formatEther(await dataToken.balanceOf(operatorWallet.address))), - delegator: Number(formatEther(await dataToken.balanceOf(delegatorWallet.address))), - sponsor: Number(formatEther(await dataToken.balanceOf(sponsorWallet.address))), - admin: Number(formatEther(await dataToken.balanceOf(adminWallet.address))), - operatorContract: Number(formatEther(await dataToken.balanceOf(await operatorContract.getAddress()))), + operator: await dataToken.balanceOf(operatorWallet.address), + delegator: await dataToken.balanceOf(delegatorWallet.address), + sponsor: await dataToken.balanceOf(sponsorWallet.address), + admin: await dataToken.balanceOf(adminWallet.address), + operatorContract: await dataToken.balanceOf(await operatorContract.getAddress()), } } @@ -103,7 +103,7 @@ describe('profit', () => { } = await setupOperatorContract({ nodeCount: 1, operatorConfig: { - operatorsCutPercent: OPERATORS_CUT_PERCENTAGE + operatorsCutPercentage: OPERATORS_CUT_PERCENTAGE } })) sponsorshipContract = await deploySponsorshipContract({ @@ -164,7 +164,7 @@ describe('profit', () => { OPERATOR_DELEGATED_AMOUNT + OPERATOR_PROFIT_WHEN_NO_WITHDRAWALS + PROFIT_INACCURACY ) const afterBalances = await getBalances() - expect(afterBalances.operatorContract).toEqual(0) + expect(afterBalances.operatorContract).toEqual(0n) const diff = { operator: afterBalances.operator - beforeBalances.operator, delegator: afterBalances.delegator - beforeBalances.delegator, diff --git a/packages/sdk/src/contracts/Operator.ts b/packages/sdk/src/contracts/Operator.ts index 9ac45a5490..da4572385a 100644 --- a/packages/sdk/src/contracts/Operator.ts +++ b/packages/sdk/src/contracts/Operator.ts @@ -2,6 +2,7 @@ import { EthereumAddress, Logger, ObservableEventEmitter, StreamID, TheGraphClient, + WeiAmount, collect, ensureValidStreamPartitionIndex, toEthereumAddress, toStreamID } from '@streamr/utils' import { Overrides } from 'ethers' @@ -25,8 +26,8 @@ interface RawResult { interface EarningsData { sponsorshipAddresses: EthereumAddress[] - sumDataWei: bigint - maxAllowedEarningsDataWei: bigint + sum: WeiAmount + maxAllowedEarnings: WeiAmount } /** @@ -401,30 +402,29 @@ export class Operator { * - only take sponsorships that have more than minSponsorshipEarningsInWithdraw, or all if undefined */ async getEarnings( - minSponsorshipEarningsInWithdraw: number, + minSponsorshipEarningsInWithdraw: WeiAmount, maxSponsorshipsInWithdraw: number ): Promise { - const minSponsorshipEarningsInWithdrawWei = BigInt(minSponsorshipEarningsInWithdraw ?? 0) const { addresses: allSponsorshipAddresses, earnings, maxAllowedEarnings, } = await this.contractReadonly.getSponsorshipsAndEarnings() as { // TODO why casting is needed? addresses: string[] - earnings: bigint[] - maxAllowedEarnings: bigint + earnings: WeiAmount[] + maxAllowedEarnings: WeiAmount } const sponsorships = allSponsorshipAddresses .map((address, i) => ({ address, earnings: earnings[i] })) - .filter((sponsorship) => sponsorship.earnings >= minSponsorshipEarningsInWithdrawWei) + .filter((sponsorship) => sponsorship.earnings >= minSponsorshipEarningsInWithdraw) .sort((a, b) => compareBigInts(a.earnings, b.earnings)) // TODO: after Node 20, use .toSorted() instead .slice(0, maxSponsorshipsInWithdraw) // take all if maxSponsorshipsInWithdraw is undefined return { sponsorshipAddresses: sponsorships.map((sponsorship) => toEthereumAddress(sponsorship.address)), - sumDataWei: sponsorships.reduce((sum, sponsorship) => sum += sponsorship.earnings, 0n), - maxAllowedEarningsDataWei: maxAllowedEarnings + sum: sponsorships.reduce((sum, sponsorship) => sum += sponsorship.earnings, 0n), + maxAllowedEarnings: maxAllowedEarnings } } diff --git a/packages/sdk/src/contracts/operatorContractUtils.ts b/packages/sdk/src/contracts/operatorContractUtils.ts index 736b6a94ad..8452d48767 100644 --- a/packages/sdk/src/contracts/operatorContractUtils.ts +++ b/packages/sdk/src/contracts/operatorContractUtils.ts @@ -1,5 +1,5 @@ import { config as CHAIN_CONFIG } from '@streamr/config' -import { Logger, retry } from '@streamr/utils' +import { Logger, multiplyWeiAmount, retry, WeiAmount } from '@streamr/utils' import { Contract, EventLog, JsonRpcProvider, Provider, Wallet, ZeroAddress, parseEther } from 'ethers' import { range } from 'lodash' import type { Operator as OperatorContract } from '../ethereumArtifacts/Operator' @@ -16,6 +16,7 @@ import { SignerWithProvider } from '../Authentication' import crypto from 'crypto' const TEST_CHAIN_CONFIG = CHAIN_CONFIG.dev2 +const FRACTION_MAX = parseEther('1') /** * @deprecated @@ -33,7 +34,7 @@ export interface SetupOperatorContractOpts { } } operatorConfig?: { - operatorsCutPercent?: number + operatorsCutPercentage?: number metadata?: string } } @@ -59,7 +60,7 @@ export async function setupOperatorContract( const operatorContract = await deployOperatorContract({ chainConfig: opts?.chainConfig ?? TEST_CHAIN_CONFIG, deployer: operatorWallet, - operatorsCutPercent: opts?.operatorConfig?.operatorsCutPercent, + operatorsCutPercentage: opts?.operatorConfig?.operatorsCutPercentage, metadata: opts?.operatorConfig?.metadata }) const nodeWallets: (Wallet & SignerWithProvider)[] = [] @@ -80,7 +81,7 @@ export async function setupOperatorContract( */ export interface DeployOperatorContractOpts { deployer: Wallet - operatorsCutPercent?: number + operatorsCutPercentage?: number metadata?: string operatorTokenName?: string chainConfig?: { @@ -107,7 +108,7 @@ export async function deployOperatorContract(opts: DeployOperatorContractOpts): throw new Error('Operator already has a contract') } const operatorReceipt = await (await operatorFactory.deployOperator( - parseEther('1') * BigInt(opts.operatorsCutPercent ?? 0) / 100n, + multiplyWeiAmount(FRACTION_MAX, ((opts.operatorsCutPercentage ?? 0) / 100)), opts.operatorTokenName ?? `OperatorToken-${Date.now()}`, opts.metadata ?? '', [ @@ -136,7 +137,7 @@ export interface DeploySponsorshipContractOpts { deployer: Wallet metadata?: string minOperatorCount?: number - earningsPerSecond?: number + earningsPerSecond?: WeiAmount chainConfig?: { contracts: { SponsorshipFactory: string @@ -164,7 +165,7 @@ export async function deploySponsorshipContract(opts: DeploySponsorshipContractO chainConfig.contracts.SponsorshipDefaultLeavePolicy, chainConfig.contracts.SponsorshipVoteKickPolicy, ], [ - parseEther((opts.earningsPerSecond ?? 1).toString()).toString(), + opts.earningsPerSecond ?? parseEther('1'), '0', '0', ] @@ -222,21 +223,21 @@ export async function generateWalletWithGasAndTokens(opts?: GenerateWalletWithGa return newWallet.connect(provider) as (Wallet & SignerWithProvider) } -export const delegate = async (delegator: Wallet, operatorContractAddress: string, amount: number, token?: DATATokenContract): Promise => { - logger.debug('Delegate', { amount }) +export const delegate = async (delegator: Wallet, operatorContractAddress: string, amount: WeiAmount, token?: DATATokenContract): Promise => { + logger.debug('Delegate', { amount: amount.toString() }) // onTokenTransfer: the tokens are delegated on behalf of the given data address // eslint-disable-next-line max-len // https://github.com/streamr-dev/network-contracts/blob/01ec980cfe576e25e8c9acc08a57e1e4769f3e10/packages/network-contracts/contracts/OperatorTokenomics/Operator.sol#L233 await transferTokens(delegator, operatorContractAddress, amount, delegator.address, token) } -export const undelegate = async (delegator: Wallet, operatorContract: OperatorContract, amount: number): Promise => { - await (await operatorContract.connect(delegator).undelegate(parseEther(amount.toString()))).wait() +export const undelegate = async (delegator: Wallet, operatorContract: OperatorContract, amount: WeiAmount): Promise => { + await (await operatorContract.connect(delegator).undelegate(amount)).wait() } -export const stake = async (operatorContract: OperatorContract, sponsorshipContractAddress: string, amount: number): Promise => { - logger.debug('Stake', { amount }) - await (await operatorContract.stake(sponsorshipContractAddress, parseEther(amount.toString()))).wait() +export const stake = async (operatorContract: OperatorContract, sponsorshipContractAddress: string, amount: WeiAmount): Promise => { + logger.debug('Stake', { amount: amount.toString() }) + await (await operatorContract.stake(sponsorshipContractAddress, amount)).wait() } export const unstake = async (operatorContract: OperatorContract, sponsorshipContractAddress: string): Promise => { @@ -244,15 +245,15 @@ export const unstake = async (operatorContract: OperatorContract, sponsorshipCon await (await operatorContract.unstake(sponsorshipContractAddress)).wait() } -export const sponsor = async (sponsorer: Wallet, sponsorshipContractAddress: string, amount: number, token?: DATATokenContract): Promise => { - logger.debug('Sponsor', { amount }) +export const sponsor = async (sponsorer: Wallet, sponsorshipContractAddress: string, amount: WeiAmount, token?: DATATokenContract): Promise => { + logger.debug('Sponsor', { amount: amount.toString() }) // eslint-disable-next-line max-len // https://github.com/streamr-dev/network-contracts/blob/01ec980cfe576e25e8c9acc08a57e1e4769f3e10/packages/network-contracts/contracts/OperatorTokenomics/Sponsorship.sol#L139 await transferTokens(sponsorer, sponsorshipContractAddress, amount, undefined, token) } -export const transferTokens = async (from: Wallet, to: string, amount: number, data?: string, token?: DATATokenContract): Promise => { - const tx = await ((token ?? getTestTokenContract()).connect(from).transferAndCall(to, parseEther(amount.toString()), data ?? '0x')) +export const transferTokens = async (from: Wallet, to: string, amount: WeiAmount, data?: string, token?: DATATokenContract): Promise => { + const tx = await ((token ?? getTestTokenContract()).connect(from).transferAndCall(to, amount, data ?? '0x')) await tx.wait() } diff --git a/packages/sdk/test/end-to-end/Operator.test.ts b/packages/sdk/test/end-to-end/Operator.test.ts index c38455e60a..35f5a04bee 100644 --- a/packages/sdk/test/end-to-end/Operator.test.ts +++ b/packages/sdk/test/end-to-end/Operator.test.ts @@ -1,7 +1,7 @@ import { config as CHAIN_CONFIG } from '@streamr/config' import { fetchPrivateKeyWithGas } from '@streamr/test-utils' import { Logger, TheGraphClient, toEthereumAddress, until } from '@streamr/utils' -import { Contract, Wallet } from 'ethers' +import { Contract, parseEther, Wallet } from 'ethers' import { StreamrClient } from '../../src/StreamrClient' import { Operator } from '../../src/contracts/Operator' import { @@ -79,8 +79,8 @@ describe('Operator', () => { }, 90 * 1000) it('getStakedOperators', async () => { - await delegate(deployedOperator.operatorWallet, await deployedOperator.operatorContract.getAddress(), 20000) - await stake(deployedOperator.operatorContract, await sponsorship1.getAddress(), 10000) + await delegate(deployedOperator.operatorWallet, await deployedOperator.operatorContract.getAddress(), parseEther('20000')) + await stake(deployedOperator.operatorContract, await sponsorship1.getAddress(), parseEther('10000')) const dummyOperator = await getOperator(deployedOperator.nodeWallets[0], deployedOperator) const randomOperatorAddress = sample(await dummyOperator.getStakedOperators()) expect(randomOperatorAddress).toBeDefined() @@ -104,9 +104,9 @@ describe('Operator', () => { it('getSponsorships, getOperatorsInSponsorship', async () => { const operatorContractAddress = toEthereumAddress(await deployedOperator.operatorContract.getAddress()) - await delegate(deployedOperator.operatorWallet, operatorContractAddress, 20000) - await stake(deployedOperator.operatorContract, await sponsorship1.getAddress(), 10000) - await stake(deployedOperator.operatorContract, await sponsorship2.getAddress(), 10000) + await delegate(deployedOperator.operatorWallet, operatorContractAddress, parseEther('20000')) + await stake(deployedOperator.operatorContract, await sponsorship1.getAddress(), parseEther('10000')) + await stake(deployedOperator.operatorContract, await sponsorship2.getAddress(), parseEther('10000')) const operator = await getOperator(undefined, deployedOperator) @@ -137,12 +137,12 @@ describe('Operator', () => { const flagger = deployedOperator const target = await setupOperatorContract() - await sponsor(flagger.operatorWallet, await sponsorship2.getAddress(), 50000) + await sponsor(flagger.operatorWallet, await sponsorship2.getAddress(), parseEther('50000')) - await delegate(flagger.operatorWallet, await flagger.operatorContract.getAddress(), 20000) - await delegate(target.operatorWallet, await target.operatorContract.getAddress(), 30000) - await stake(flagger.operatorContract, await sponsorship2.getAddress(), 15000) - await stake(target.operatorContract, await sponsorship2.getAddress(), 25000) + await delegate(flagger.operatorWallet, await flagger.operatorContract.getAddress(), parseEther('20000')) + await delegate(target.operatorWallet, await target.operatorContract.getAddress(), parseEther('30000')) + await stake(flagger.operatorContract, await sponsorship2.getAddress(), parseEther('15000')) + await stake(target.operatorContract, await sponsorship2.getAddress(), parseEther('25000')) const contractFacade = await getOperator(deployedOperator.nodeWallets[0], flagger) await contractFacade.flag( diff --git a/packages/utils/src/WeiAmount.ts b/packages/utils/src/WeiAmount.ts new file mode 100644 index 0000000000..1eb246547e --- /dev/null +++ b/packages/utils/src/WeiAmount.ts @@ -0,0 +1,7 @@ +export type WeiAmount = bigint + +const PRECISION = 1e18 + +export const multiplyWeiAmount = (val1: WeiAmount, val2: number): WeiAmount => { + return val1 * BigInt(PRECISION * val2) / BigInt(PRECISION) +} diff --git a/packages/utils/src/exports.ts b/packages/utils/src/exports.ts index ba8cd40e83..a0740dd3df 100644 --- a/packages/utils/src/exports.ts +++ b/packages/utils/src/exports.ts @@ -127,3 +127,4 @@ export { StreamPartID, toStreamPartID, StreamPartIDUtils } from './StreamPartID' export { UserID, UserIDRaw, toUserId, toUserIdRaw, isValidUserId, isEthereumAddressUserId } from './UserID' export { HexString } from './HexString' export { ChangeFieldType, MapKey } from './types' +export { WeiAmount, multiplyWeiAmount } from './WeiAmount'