diff --git a/docs/core/RewardsCoordinator.md b/docs/core/RewardsCoordinator.md index 970e728969..5f2bfd4509 100644 --- a/docs/core/RewardsCoordinator.md +++ b/docs/core/RewardsCoordinator.md @@ -9,17 +9,22 @@ The `RewardsCoordinator` accepts ERC20s from AVSs alongside rewards submission requests made out to Operators who, during a specified time range, were registered to the AVS in the core [`AVSDirectory`](./AVSDirectory.md) contract. -*Off-chain*, the trusted *rewards updater* calculates a rewards distribution according to (as of the rewards submission's time range): (i) the relative stake weight of each Operator's Stakers and (ii) a globally-defined commission given to the Operator. *On-chain*, the rewards updater sends the `RewardsCoordinator` a merkle root of each earner's cumulative earnings. +There are two forms of rewards: +* Rewards v1, also known as rewards submissions. +* Rewards v2, also known as operator-directed rewards submissions. See the [ELIP](https://github.com/eigenfoundation/ELIPs/blob/main/ELIPs/ELIP-001.md) for additional context on this rewards type. -Earners provide merkle proofs to the `RewardsCoordinator` to claim rewards against these roots. +*Off-chain*, the trusted *rewards updater* calculates a rewards distribution over some rewards submission's time range, depending on the rewards type. For a **v1 rewards submission**, it is based on: (i) the relative stake weight of each Operator's Stakers and (ii) a default split given to the Operator. For a **v2 rewards submission**, it is based on: (i) an AVS's custom rewards logic, (ii) the per-operator splits. + +*On-chain*, the rewards updater sends the `RewardsCoordinator` a merkle root of each earner's cumulative earnings. Earners provide merkle proofs to the `RewardsCoordinator` to claim rewards against these roots. The typical user flow is as follows: -1. An AVS submits a `RewardsSubmission` to the `RewardsCoordinator` contract, which specifies a time range (`startTimestamp` and `duration`) and reward (`token` and `amount`). The `RewardsSubmission` also specifies the strategies Stakers/Operators should have held over the time range in order to receive the rewards (i.e. "distribute out to holders of X strategy specifically"). -2. Off-chain, `RewardsSubmissions` are used to calculate reward distributions, which are periodically consolidated into a merkle tree. +1. An AVS submits a *rewards submission*, either a `RewardsSubmission` (v1) or `OperatorDirectedRewardsSubmission` (v2), to the `RewardsCoordinator` contract, which specifies a time range (`startTimestamp` and `duration`) and `token`. The rewards submission also specifies the relative reward weights of strategies (i.e. "distribute 80% out to holders of X strategy, and 20% to holders of strategy Y"). + * Note that **v1 rewards** specify a total `amount`, whereas **v2 rewards** specify a per-operator reward (due to customizable rewards logic). **v2 rewards** also allow for adding a `description` of the rewards submission's purpose. +2. Off-chain, the rewards submissions are used to calculate reward distributions, which are periodically consolidated into a merkle tree. 3. The root of this tree (aka the `DistributionRoot`) is posted on-chain by the *rewards updater*. A `DistributionRoot` becomes active for claims after some globally-configured `activationDelay`. 4. Stakers and Operators (or their configured "claimers") can claim their accumulated earnings by providing a merkle proof against any previously-posted `DistributionRoot`. -This entire flow will repeat periodically as AVSs submit `RewardsSubmissions`, `DistributionRoots` are submitted, and Stakers/Operators claim their accumulated earnings. Note that `DistributionRoots` contain *cumulative earnings*, meaning Stakers/Operators aren't required to claim against every root - simply claiming against the most recent root will claim anything not yet claimed. +This entire flow will repeat periodically as AVSs submit rewards submissions, `DistributionRoots` are submitted, and Stakers/Operators claim their accumulated earnings. Note that `DistributionRoots` contain *cumulative earnings*, meaning Stakers/Operators aren't required to claim against every root - simply claiming against the most recent root will claim anything not yet claimed. #### High-level Concepts @@ -32,15 +37,19 @@ This document is organized according to the following themes (click each to be t #### Important state variables -* `DistributionRoot[] public distributionRoots`: +* `DistributionRoot[] public distributionRoots`: * `distributionRoots` stores historic reward merkle tree roots submitted by the rewards updater. For each earner, the rewards merkle tree stores cumulative earnings per ERC20 reward token. For more details on merkle tree structure see [Rewards Merkle Tree Structure](#rewards-merkle-tree-structure) below. * `mapping(address => address) public claimerFor`: earner => claimer * Stakers and Operators can designate a "claimer" who can claim rewards via on their behalf via `processClaim`. If a claimer is not set in `claimerFor`, the earner will have to call `processClaim` themselves. * Note that the claimer isn't necessarily the reward recipient, but they do have the authority to specify the recipient when calling `processClaim` on the earner's behalf. * `mapping(address => mapping(IERC20 => uint256)) public cumulativeClaimed`: earner => token => total amount claimed to date * Mapping for earners(Stakers/Operators) to track their total claimed earnings per reward token. This mapping is used to calculate the difference between the cumulativeEarnings stored in the merkle tree and the previous total claimed amount. This difference is then transfered to the specified destination address. -* `uint16 public globalOperatorCommissionBips`: *Used off-chain* by the rewards updater to calculate an Operator's commission for a specific reward. +* `uint16 public defaultOperatorSplitBips`: *Used off-chain* by the rewards updater to calculate an Operator's split for a specific reward. * This is expected to be a flat 10% rate for the initial rewards release. Expressed in basis points, this is `1000`. +* `mapping(address => mapping(address => OperatorSplit)) internal operatorAVSSplitBips`: operator => AVS => `OperatorSplit` + * Operators specify their custom split for a given AVS for each `OperatorDirectedRewardsSubmission`, where Stakers receive a relative proportion (by stake weight) of the remaining amount. +* `mapping(address => OperatorSplit) internal operatorPISplitBips`: operator => `OperatorSplit` + * Operators may also specify their custom split for [programmatic incentives](https://www.blog.eigenlayer.xyz/introducing-programmatic-incentives-v1/), where Stakers similarly receive a relative proportion (by stake weight) of the remaining amount. #### Helpful definitions @@ -51,26 +60,27 @@ This document is organized according to the following themes (click each to be t * earner proof reverting from calling `_verifyEarnerClaimProof` * any of the token proofs reverting from calling `_verifyTokenClaimProof` * Wherever AVS (Actively Validated Service) is mentioned, it refers to the contract entity that is submitting rewards to the `RewardsCoordinator`. This is assumed to be a customized `ServiceManager` contract of some kind that is interfacing with the EigenLayer protocol. See the `ServiceManagerBase` docs here: [`eigenlayer-middleware/docs/ServiceManagerBase.md`](https://github.com/Layr-Labs/eigenlayer-middleware/blob/dev/docs/ServiceManagerBase.md). +* A rewards submission includes, unless specified otherwise, both the v1 `RewardsSubmission` and the v2 `OperatorDirectedRewardsSubmission` types. --- ### Submitting Rewards Requests - Rewards are initially submitted to the contract to be distributed to Operators and Stakers by the following functions: * [`RewardsCoordinator.createAVSRewardsSubmission`](#createavsrewardssubmission) * [`RewardsCoordinator.createRewardsForAllSubmission`](#createrewardsforallsubmission) * [`RewardsCoordinator.createRewardsForAllEarners`](#createrewardsforallearners) +* [`RewardsCoordinator.createOperatorDirectedAVSRewardsSubmission`](#createOperatorDirectedAVSRewardsSubmission) #### `createAVSRewardsSubmission` ```solidity function createAVSRewardsSubmission( RewardsSubmission[] calldata RewardsSubmissions -) - external - onlyWhenNotPaused(PAUSED_AVS_REWARDS_SUBMISSION) +) + external + onlyWhenNotPaused(PAUSED_AVS_REWARDS_SUBMISSION) nonReentrant ``` @@ -94,7 +104,7 @@ See the `ServiceManagerBase` abstract contract here: [`ServiceManagerBase.sol`]( *Rewards Distribution*: -The rewards distribution amongst the AVS's Operators and delegated Stakers is determined offchain using the strategies and multipliers provided in the `RewardsSubmission` struct as well as the actual shares for those defined strategies over the `RewardsSubmission`'s time range. These shares are read from the [`EigenPodManager`](./EigenPodManager.md) (in the case of the Beacon Chain ETH strategy), or the [`StrategyManager`](./StrategyManager.md) for any other strategy. Note that Stakers' shares specifically are what determines rewards distribution; Operators earn based on a combination of their own deposited shares and a configured `globalOperatorCommissionBips`. +The rewards distribution amongst the AVS's Operators and delegated Stakers is determined offchain using the strategies and multipliers provided in the `RewardsSubmission` struct as well as the actual shares for those defined strategies over the `RewardsSubmission`'s time range. These shares are read from the [`EigenPodManager`](./EigenPodManager.md) (in the case of the Beacon Chain ETH strategy), or the [`StrategyManager`](./StrategyManager.md) for any other strategy. Note that Stakers' shares specifically are what determines rewards distribution; Operators earn based on a combination of their own deposited shares and a configured `defaultOperatorSplitBips`. *Effects*: * For each `RewardsSubmission` element @@ -145,8 +155,8 @@ GENESIS_REWARDS_TIMESTAMP ```solidity function createRewardsForAllSubmission( RewardsSubmission[] calldata RewardsSubmissions -) - external +) + external onlyWhenNotPaused(PAUSED_REWARDS_FOR_ALL_SUBMISSION) onlyRewardsForAllSubmitter nonReentrant @@ -164,13 +174,13 @@ This method is identical in function to [`createAVSRewardsSubmission`](#createav *Requirements*: * See [`createAVSRewardsSubmission`](#createavsrewardssubmission) above. The only difference is that each calculated rewards submission hash MUST NOT already exist in the `isRewardsSubmissionForAllHash` mapping. -#### `createRewardsForAllEarners` +#### `createRewardsForAllEarners` ```solidity function createRewardsForAllEarners( RewardsSubmission[] calldata RewardsSubmissions -) - external +) + external onlyWhenNotPaused(PAUSED_REWARDS_FOR_ALL_SUBMISSION) onlyRewardsForAllSubmitter nonReentrant @@ -188,6 +198,51 @@ This method is identical in function to [`createAVSRewardsSubmission`](#createav *Requirements*: * See [`createAVSRewardsSubmission`](#createavsrewardssubmission) above. The only difference is that each calculated rewards submission hash MUST NOT already exist in the `isRewardsSubmissionForAllEarnersHash` mapping. +#### `createOperatorDirectedAVSRewardsSubmission` + +```solidity +function createOperatorDirectedAVSRewardsSubmission( + address avs, + OperatorDirectedRewardsSubmission[] calldata operatorDirectedRewardsSubmissions +) + external + onlyWhenNotPaused(PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION) + nonReentrant +``` + +AVS may make Rewards v2 submissions by calling `createOperatorDirectedAVSRewardsSubmission()` with any custom on-chain or off-chain logic to determine their rewards distribution strategy. This can be custom to the work performed by Operators during a certain period of time, can be a flat reward rate, or some other structure based on the AVS’s economic model. This would enable AVSs' flexibility in rewarding different operators for performance and other variables while maintaining the same easily calculable reward rate for stakers delegating to the same operator and strategy. The AVS can submit multiple performance-based rewards denominated in different tokens for even more flexibility. + +*Effects*: +* For each `OperatorDirectedRewardsSubmission` element + * Transfers `amount` of `token` from the `msg.sender` (`AVS`) to the `RewardsCoordinator` + * Hashes `msg.sender` (`AVS`), `nonce`, and `OperatorDirectedRewardsSubmission` struct to create a unique rewards hash and sets this value to `true` in the `isOperatorDirectedAVSRewardsSubmissionHash` mapping + * Increments `submissionNonce[msg.sender]` + * Emits an `OperatorDirectedAVSRewardsSubmissionCreated` event + +*Requirements*: +* Pause status MUST NOT be set: `PAUSED_OPERATOR_DIRECTED_AVS_REWARDS_SUBMISSION` +* Caller MUST BE the AVS +* Function call is not reentered +* For each `OperatorDirectedRewardsSubmission` element: + * Requirements from calling internal function `_validateOperatorDirectedRewardsSubmission()` + * `operatorDirectedRewardsSubmission.strategiesAndMultipliers.length > 0` + * `operatorDirectedRewardsSubmission.duration <= MAX_REWARDS_DURATION` + * `operatorDirectedRewardsSubmission.duration % calculationIntervalSeconds == 0` + * `operatorDirectedRewardsSubmission.startTimestamp % calculationIntervalSeconds == 0` + * `block.timestamp - MAX_RETROACTIVE_LENGTH <= operatorDirectedRewardsSubmission.startTimestamp` + * `GENESIS_REWARDS_TIMESTAMP <= operatorDirectedRewardsSubmission.startTimestamp` + * For each `operatorDirectedRewardsSubmission.strategiesAndMultipliers` element: + * Each `strategy` is whitelisted for deposit in the StrategyManager or is the `beaconChainETHStrategy` + * `rewardsSubmission.strategiesAndMultipliers` is sorted by ascending strategy address to prevent duplicate strategies + * `operatorDirectedRewardsSubmission.operatorRewards.length > 0` + * For each `operatorDirectedRewardsSubmission.operatorRewards` element: + * `operatorReward.operator != address(0)` + * `currOperatorAddress < operatorReward.operator` + * `operatorReward.amount > 0` + * `totalAmount <= MAX_REWARDS_AMOUNT`, where `totalAmount` is the sum of every `operatorReward.amount` + * `operatorDirectedRewardsSubmission.startTimestamp + operatorDirectedRewardsSubmission.duration < block.timestamp`, enforcing strictly retoractive rewards submissions + * `transferFrom` MUST succeed in transferring `amount` of `token` from `msg.sender` to the `RewardsCoordinator` + --- ### Distributing and Claiming Rewards @@ -197,20 +252,21 @@ The *rewards updater* calculates rewards distributions and submit claimable root * [`RewardsCoordinator.submitRoot`](#submitroot) * [`RewardsCoordinator.disableRoot`](#disableroot) -Earners configure and claim these rewards using the following functions: +Earners configure and claim these rewards using the following functions: * [`RewardsCoordinator.setClaimerFor`](#setclaimerfor) * [`RewardsCoordinator.processClaim`](#processclaim) +* [`RewardsCoordinator.processClaims`](#processclaims) #### `submitRoot` ```solidity function submitRoot( - bytes32 root, + bytes32 root, uint32 rewardsCalculationEndTimestamp -) +) external - onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) + onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) onlyRewardsUpdater ``` @@ -237,9 +293,9 @@ Called only by the `rewardsUpdater` address to create a new `DistributionRoot` i ```solidity function disableRoot( uint32 rootIndex -) +) external - onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) + onlyWhenNotPaused(PAUSED_SUBMIT_DISABLE_ROOTS) onlyRewardsUpdater ``` @@ -248,7 +304,7 @@ This is to add additional measures to prevent invalid roots posted to the contra *Effects*: * Sets the `disabled` field to True for the corresponding `DistributionRoot` -* `DistributionRoot` can no longer be claimed against in `processClaim` +* `DistributionRoot` can no longer be claimed against in `processClaim` * Emits a `DistributionRootDisabled` event *Requirements*: @@ -276,11 +332,11 @@ Called by an earner (Staker/Operator) to set a claimer address that can call `pr ```solidity function processClaim( - RewardsMerkleClaim calldata claim, + RewardsMerkleClaim calldata claim, address recipient -) +) external - onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) + onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) nonReentrant ``` @@ -299,7 +355,9 @@ The `RewardsMerkleClaim` struct contains the following fields (see [Rewards Merk * `IERC20 token`: the ERC20 token to be claimed * `uint256 amount`: the amount of the ERC20 token to be claimed -`processClaim` will first call `_checkClaim` to verify the merkle proofs against the `DistributionRoot` at the specified `rootIndex`. This is done by first performing a merkle proof verification of the earner's `EarnerTreeMerkleLeaf` against the `DistributionRoot` and then for each tokenIndex, verifying each token leaf against the earner's `earnerTokenRoot`. +`processClaim` is a simple wrapper function which calls out to the internal function `_processClaim`, which holds all of the necessary logic. + +`_processClaim` will first call `_checkClaim` to verify the merkle proofs against the `DistributionRoot` at the specified `rootIndex`. This is done by first performing a merkle proof verification of the earner's `EarnerTreeMerkleLeaf` against the `DistributionRoot` and then for each tokenIndex, verifying each token leaf against the earner's `earnerTokenRoot`. The caller must be the set claimer address in the `claimerFor` mapping or the earner themselves if the claimer is not set. @@ -322,18 +380,40 @@ After the claim is verified, for each token leaf, the difference between the cum * `claim.tokenTreeProofs[i]` MUST validate `claim.tokenLeaves[i]` against `claim.earnerLeaf.earnerTokenRoot` * If the `earner` specified in `claim.earnerLeaf.earner` has a designated `claimer` in `claimerFor[earner]`, `msg.sender` MUST be the `claimer` * Otherwise, `msg.sender` MUST be the `earner` -* For each `TokenTreeMerkleLeaf`, +* For each `TokenTreeMerkleLeaf`, * `tokenLeaf.cumulativeEarnings > cumulativeClaimed[earner][token]`: cumulativeEarnings must be gt than cumulativeClaimed. Trying to reclaim with the same proofs will revert because the claimed and earnings values will equal, breaking this requirement. * `tokenLeaf.token.safeTransfer(recipient, claimAmount)` MUST succeed +#### `processClaims` + +```solidity +function processClaims( + RewardsMerkleClaim[] calldata claims, + address recipient +) + external + onlyWhenNotPaused(PAUSED_PROCESS_CLAIM) + nonReentrant +``` + +`processClaims` is a simple wrapper function around `_processClaim`, calling it once for each claim provided. + +*Effects*: +* For each `RewardsMerkleClaim` element: see [`processClaim`](#processclaim) above. + +*Requirements* +* See [`processClaim`](#processclaim) above. + --- ### System Configuration * [`RewardsCoordinator.setActivationDelay`](#setactivationdelay) -* [`RewardsCoordinator.setGlobalOperatorCommission`](#setglobaloperatorcommission) +* [`RewardsCoordinator.setDefaultOperatorSplit`](#setDefaultOperatorSplit) * [`RewardsCoordinator.setRewardsUpdater`](#setrewardsupdater) * [`RewardsCoordinator.setRewardsForAllSubmitter`](#setrewardsforallsubmitter) +* [`RewardsCoordinator.setOperatorAVSsplit`](#setOperatorAVSsplit) +* [`RewardsCoordinator.setOperatorPIsplit`](#setOperatorPIsplit) #### `setActivationDelay` @@ -350,19 +430,19 @@ Allows the Owner to set the global `activationDelay`. The activation delay is th *Requirements*: * Caller MUST be the Owner -#### `setGlobalOperatorCommission` +#### `setDefaultOperatorSplit` ```solidity -function setGlobalOperatorCommission(uint16 _globalCommissionBips) external onlyOwner +function setDefaultOperatorSplit(uint16 split) external onlyOwner ``` -Allows the Owner to set the global operator commission in basis points. +Allows the Owner to set the default operator split in basis points. -This commission is *used off-chain* when calculating Operator earnings for a given rewards distribution. Operator commission is calculated as a percentage of the reward amount made out to each Operator. This commission is deducted from the reward amount, after which the remainder is used to calculate rewards made to any Stakers delegated to the Operator. +This split is *used off-chain* when calculating Operator earnings for a given rewards distribution. Operator split is calculated as a percentage of the reward amount made out to each Operator. This split is deducted from the reward amount, after which the remainder is used to calculate rewards made to any Stakers delegated to the Operator. *Effects*: -* Sets the `globalOperatorCommissionBips` -* Emits a `GlobalCommissionBipsSet` event +* Sets the `defaultOperatorSplitBips` +* Emits a `DefaultOperatorSplitBipsSet` event *Requirements*: * Caller MUST be the Owner @@ -397,6 +477,53 @@ Allows the Owner to update the `_submitter's` permissions in the `isRewardsForAl *Requirements*: * Caller MUST be the Owner +#### `setOperatorAVSsplit` + +```solidity +function setOperatorAVSSplit( + address operator, + address avs, + uint16 split +) + external + onlyWhenNotPaused(PAUSED_OPERATOR_AVS_SPLIT) +``` + +An Operator may, for a given AVS, set a split which will determine what percent of their attributed rewards are allocated to themselves. The remaining percentage will go to Stakers. + +The split will take effect after an `activationDelay` set by the contract owner. Note that once an operator initiates a split update, the `activationDelay` must pass before a new split update can be initiated. + +*Effects*: +* Updates `operatorSplit.activatedAt` to `block.timestamp + activationDelay` +* If the operator has not initialized yet, sets `operatorSplit.oldSplitBips` to `defaultOperatorSplitBips`. Else sets `operatorSplit.oldSplitBips` to the current `newSplitBips` +* Updates `operatorSplit.newSplitBips` to `split` +* Emits an `OperatorAVSSplitBipsSet` event + +*Requirements*: +* Caller MUST BE the operator +* Split MUST BE <= 10,000 bips (100%) +* Current `block.timestamp` MUST BE greater than current `operatorSplit.activatedAt`. + * Any pending split must have already completed prior to setting a new split. + +#### `setOperatorPIsplit` + +```solidity +function setOperatorPISplit( + address operator, + uint16 split +) + external + onlyWhenNotPaused(PAUSED_OPERATOR_PI_SPLIT) +``` + +Similar to [`setOperatorAVSSplit`](#setoperatoravssplit), Operators may set their split for [programmatic incentives](https://www.blog.eigenlayer.xyz/introducing-programmatic-incentives-v1/), allowing them to specify what percent of these rewards they will maintain and what percent will go to their Stakers. The `allocationDelay` also applies here, as well as the inability to reinitiate a split update before the delay passes. + +*Effects*: +* Same as [`setOperatorAVSSplit`](#setoperatoravssplit), but with `operatorPISplitBips` instead of `operatorAVSSplitBips`. + +*Requirements*: +* See [`setOperatorAVSSplit`](#setoperatoravssplit). + --- ### Rewards Merkle Tree Structure @@ -407,7 +534,7 @@ When submitting a new `DistributionRoot`, the rewards updater consolidates all ` When an earner or their designated claimer calls `processClaim`, they must provide a `RewardsMerkleClaim` struct that contains the necessary information to verify their claim against the latest `DistributionRoot`. The merkle proof verification is done in the internal `_checkClaim` helper function. This function verifies the merkle proof of the earner's `EarnerTreeMerkleLeaf` against the `DistributionRoot` and then for each tokenIndex, verifies each token leaf against the earner's `earnerTokenRoot`. -Claimers can selectively choose which token leaves to prove against and claim accumulated earnings. Each token reward claimed in a `processClaim` call will send tokens to the `recipient` address specified in the call. +Claimers can selectively choose which token leaves to prove against and claim accumulated earnings. Each token reward claimed in a `processClaim` call will send tokens to the `recipient` address specified in the call. The rewards merkle tree is structured in the diagram below: @@ -417,6 +544,6 @@ The rewards merkle tree is structured in the diagram below: ### Off Chain Calculation -Rewards are calculated via an off-chain data pipeline. The pipeline takes snapshots of core contract state at the `SNAPSHOT_CADENCE`, currently set to once per day. It then combines these snapshots with any active rewards to calculate what the single daily reward of an earner is. Every `CALCULATION_INTERVAL_SECONDS` rewards are accumulated up to `lastRewardsTimestamp + CALCULATION_INTERVAL_SECONDS` and posted on-chain by the entity with the `rewardsUpdater` role. +Rewards are calculated via an off-chain data pipeline. The pipeline takes snapshots of core contract state at the `SNAPSHOT_CADENCE`, currently set to once per day. It then combines these snapshots with any active rewards to calculate what the single daily reward of an earner is. Every `CALCULATION_INTERVAL_SECONDS` rewards are accumulated up to `lastRewardsTimestamp + CALCULATION_INTERVAL_SECONDS` and posted on-chain by the entity with the `rewardsUpdater` role. -`MAX_REWARDS_AMOUNT` is set to `1e38-1` given the precision bounds of the off-chain pipeline. An in-depth overview of the off-chain calculation can be found [here](https://hackmd.io/u-NHKEvtQ7m7CVDb4_42bA) \ No newline at end of file +`MAX_REWARDS_AMOUNT` is set to `1e38-1` given the precision bounds of the off-chain pipeline. An in-depth overview of the off-chain calculation can be found [here](https://hackmd.io/Fmjcckn1RoivWpPLRAPwBw) \ No newline at end of file