Skip to content

Commit

Permalink
refactor(sdk): [NET-1388] Use bigint for Wei amounts in `operatorCo…
Browse files Browse the repository at this point in the history
…ntractUtils` (#2952)

## Summary

The `operatorContractUtils` functions now use Wei units instead of full
DATA token units. The parameter type for those functions is now
`WeiAmount` instead of `number`. The `WeiAmount` is a type alias for
`bigint`.

The purpose for `WeiAmount` is similar to `HexString`: it improves
readability as it tells more about what _kind_ of `bigint` value the
variable stores. I.e. that the value is a token amount, and the unit of
the token amount is Wei. (The same information could be in the variable
name, but this approach is more readable).

## Changes

- function signatures: `delegate`/`delegate`, `stake`/`unstake`,
`sponsor`, `transferTokens`
- `earningsPerSeconds` field type in `DeployOperatorContractOpts`

Also moved `multiply` utility from `node` to `utils` and renamed it to
`multiplyWeiAmount`. It is now used also in the `sdk` package.

## Other changes

- Improved logging for `sumDataWei` and `maxAllowedEarningsDataWei` in
`checkOperatorValueBreach`, ensuring a more readable format. Previously
these values were sometimes logged using scientific notation.
- Small renames:
  - `operatorsCutPercent` -> `operatorsCutPercentage`
- shortened some variable names as we now use `WeiAmount` type (no need
have the type also in the variable name, e.g.
`maxAllowedEarningsDataWei`)
  - unified Wei amount variable naming in `inspect.test.ts` 
  
## Future improvements

We use `ether`'s `parseEther()` to convert full DATA token units to Wei
units. The word Ether is maybe bit misleading there, so we could
consider having a wrapper to make it more readable?

Possible higher level approach would be to have a `TokenAmount` class
for holding the value. In that case we'd have builder functions like
`TokenAmount.full(123.45)` and
`TokenAmount.wei(123450000000000000000n)`.
  • Loading branch information
teogeb authored Dec 20, 2024
1 parent 989c1f4 commit a03fd46
Show file tree
Hide file tree
Showing 15 changed files with 139 additions and 130 deletions.
6 changes: 0 additions & 6 deletions packages/node/src/helpers/multiply.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/node/src/plugins/operator/OperatorPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class OperatorPlugin extends Plugin<OperatorPluginConfig> {
try {
await maintainOperatorValue(
this.pluginConfig.maintainOperatorValue.withdrawLimitSafetyFraction,
this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw,
BigInt(this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw),
this.pluginConfig.maintainOperatorValue.maxSponsorshipsInWithdraw,
operator
)
Expand Down Expand Up @@ -215,7 +215,7 @@ export class OperatorPlugin extends Plugin<OperatorPluginConfig> {
operator,
streamrClient,
() => stakedOperatorsCache.get(),
this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw,
BigInt(this.pluginConfig.maintainOperatorValue.minSponsorshipEarningsInWithdraw),
this.pluginConfig.maintainOperatorValue.maxSponsorshipsInWithdraw
).catch((err) => {
logger.warn('Encountered error', { err })
Expand Down
17 changes: 11 additions & 6 deletions packages/node/src/plugins/operator/checkOperatorValueBreach.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -8,7 +8,7 @@ export const checkOperatorValueBreach = async (
myOperator: Operator,
client: StreamrClient,
getStakedOperators: () => Promise<EthereumAddress[]>,
minSponsorshipEarningsInWithdraw: number,
minSponsorshipEarningsInWithdraw: WeiAmount,
maxSponsorshipsInWithdraw: number
): Promise<void> => {
const targetOperatorAddress = sample(without(await getStakedOperators(), await myOperator.getContractAddress()))
Expand All @@ -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)
}
}
13 changes: 6 additions & 7 deletions packages/node/src/plugins/operator/maintainOperatorValue.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -39,7 +39,7 @@ describe('checkOperatorValueBreach', () => {
await client.destroy()
deployConfig = {
operatorConfig: {
operatorsCutPercent: 10
operatorsCutPercentage: 10
}
}
}, 60 * 1000)
Expand All @@ -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
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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', () => {
Expand All @@ -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
)
Expand Down
38 changes: 20 additions & 18 deletions packages/node/test/smoke/inspect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 = [{
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -190,8 +190,8 @@ const getOperatorStakeCount = async (operatorContractAddress: string, theGraphCl
return response.operator.stakes.length
}

const getTokenBalance = async (address: string, token: any): Promise<number> => {
return Number(formatEther(await token.balanceOf(address)))
const getTokenBalance = async (address: string, token: any): Promise<bigint> => {
return await token.balanceOf(address)
}

describe('inspect', () => {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit a03fd46

Please sign in to comment.