From c04c236e2901471dd990076ebe6c2663af38a80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 17 May 2022 16:17:49 -0300 Subject: [PATCH 01/47] feat: implement distribution of rewards to L1 and L2 using a Reservoir BREAKING CHANGE: remove setIssuanceRate from RewardsManager interface BREAKING CHANGE: RewardsDenied event now includes amount --- cli/commands/migrate.ts | 2 + config/graph.arbitrum-one.yml | 10 + config/graph.mainnet.yml | 16 +- contracts/governance/Managed.sol | 1 + contracts/l2/reservoir/L2Reservoir.sol | 114 +++ contracts/l2/reservoir/L2ReservoirStorage.sol | 13 + contracts/reservoir/IReservoir.sol | 58 ++ contracts/reservoir/L1Reservoir.sol | 294 +++++++ contracts/reservoir/L1ReservoirStorage.sol | 25 + contracts/reservoir/Reservoir.sol | 124 +++ contracts/reservoir/ReservoirStorage.sol | 18 + contracts/rewards/IRewardsManager.sol | 4 +- contracts/rewards/RewardsManager.sol | 198 ++--- contracts/rewards/RewardsManagerStorage.sol | 9 +- contracts/staking/Staking.sol | 22 +- contracts/tests/ReservoirMock.sol | 27 + contracts/tests/RewardsManagerMock.sol | 68 -- test/gateway/l1GraphTokenGateway.test.ts | 14 +- test/l2/l2GraphTokenGateway.test.ts | 2 + test/l2/l2Reservoir.test.ts | 364 ++++++++ test/lib/deployment.ts | 28 + test/lib/fixtures.ts | 33 +- test/lib/testHelpers.ts | 92 ++ test/reservoir/l1Reservoir.test.ts | 750 ++++++++++++++++ test/rewards/rewards.test.ts | 815 ++++++++++-------- 25 files changed, 2524 insertions(+), 577 deletions(-) create mode 100644 contracts/l2/reservoir/L2Reservoir.sol create mode 100644 contracts/l2/reservoir/L2ReservoirStorage.sol create mode 100644 contracts/reservoir/IReservoir.sol create mode 100644 contracts/reservoir/L1Reservoir.sol create mode 100644 contracts/reservoir/L1ReservoirStorage.sol create mode 100644 contracts/reservoir/Reservoir.sol create mode 100644 contracts/reservoir/ReservoirStorage.sol create mode 100644 contracts/tests/ReservoirMock.sol delete mode 100644 contracts/tests/RewardsManagerMock.sol create mode 100644 test/l2/l2Reservoir.test.ts create mode 100644 test/reservoir/l1Reservoir.test.ts diff --git a/cli/commands/migrate.ts b/cli/commands/migrate.ts index 335b6fa32..ad8c90148 100644 --- a/cli/commands/migrate.ts +++ b/cli/commands/migrate.ts @@ -36,6 +36,7 @@ let allContracts = [ 'AllocationExchange', 'L1GraphTokenGateway', 'BridgeEscrow', + 'L1Reservoir', ] const l2Contracts = [ @@ -55,6 +56,7 @@ const l2Contracts = [ 'DisputeManager', 'AllocationExchange', 'L2GraphTokenGateway', + 'L2Reservoir', ] export const migrate = async ( diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index e68d67521..66127e5a4 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L2Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -149,3 +152,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + L2Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "approveRewardsManager" + - fn: "syncAllContracts" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index 6eb08b233..ea11603fd 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L1Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -57,7 +60,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{RewardsManager.address}}" + minter: "${{L1Reservoir.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +134,6 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" @@ -158,3 +159,12 @@ contracts: controller: "${{Controller.address}}" calls: - fn: "syncAllContracts" + L1Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + dripInterval: 50400 + calls: + - fn: "approveRewardsManager" + - fn: "initialSnapshot" + - fn: "syncAllContracts" diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index ce18fb009..c543415e4 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -192,5 +192,6 @@ contract Managed { _syncContract("Staking"); _syncContract("GraphToken"); _syncContract("GraphTokenGateway"); + _syncContract("Reservoir"); } } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol new file mode 100644 index 000000000..6bdd9ca67 --- /dev/null +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../reservoir/IReservoir.sol"; +import "../../reservoir/Reservoir.sol"; +import "./L2ReservoirStorage.sol"; + +/** + * @title L2 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. + * It receives tokens for rewards from L1, and provides functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { + using SafeMath for uint256; + + event DripReceived(uint256 _normalizedTokenSupply); + event NextDripNonceUpdated(uint256 _nonce); + + /** + * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. + */ + modifier onlyL2Gateway() { + require(msg.sender == _resolveContract(keccak256("GraphTokenGateway")), "ONLY_GATEWAY"); + _; + } + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + */ + function initialize(address _controller) external onlyImpl { + Managed._initialize(_controller); + } + + /** + * @dev Update the next drip nonce + * To be used only as a backup option if the two layers get out of sync. + * @param _nonce Expected value for the nonce of the next drip message + */ + function setNextDripNonce(uint256 _nonce) external onlyGovernor { + nextDripNonce = _nonce; + emit NextDripNonceUpdated(_nonce); + } + + /** + * @dev Get new total rewards accumulated since the last drip. + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the normalized token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on L2 since the last drip + */ + function getNewRewards(uint256 blocknum) + public + view + override(Reservoir, IReservoir) + returns (uint256 deltaRewards) + { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + return 0; + } + deltaRewards = normalizedTokenSupplyCache + .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(normalizedTokenSupplyCache); + } + + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the normalizedTokenSupplyCache and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _normalizedTokenSupply, + uint256 _issuanceRate, + uint256 _nonce + ) external override onlyL2Gateway { + require(_nonce == nextDripNonce, "INVALID_NONCE"); + nextDripNonce = nextDripNonce.add(1); + if (_issuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(); + issuanceRate = _issuanceRate; + emit IssuanceRateUpdated(_issuanceRate); + } else { + snapshotAccumulatedRewards(); + } + normalizedTokenSupplyCache = _normalizedTokenSupply; + emit DripReceived(normalizedTokenSupplyCache); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + */ + function snapshotAccumulatedRewards() internal { + accumulatedLayerRewards = getAccumulatedRewards(block.number); + lastRewardsUpdateBlock = block.number; + } +} diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol new file mode 100644 index 000000000..c6ad73e1c --- /dev/null +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L2Reservoir + */ +contract L2ReservoirV1Storage { + // Snapshot of total GRT supply multiplied by L2 rewards fraction, received from L1 + uint256 public normalizedTokenSupplyCache; + // Expected nonce value for the next drip hook + uint256 public nextDripNonce; +} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol new file mode 100644 index 000000000..dfc64f14f --- /dev/null +++ b/contracts/reservoir/IReservoir.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @title Interface for the Rewards Reservoir + * @dev This is the shared interface between L1 and L2, for the contracts + * that hold rewards on each layers and provide functions to compute + * accumulated and new total rewards. + */ +interface IReservoir { + // Emitted when the issuance rate is updated + event IssuanceRateUpdated(uint256 _newValue); + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external; + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param blocknum Block number at which to calculate rewards + * @return totalRewards Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 blocknum) external view returns (uint256 totalRewards); + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 blocknum) external view returns (uint256 deltaRewards); +} + +/** + * @title Interface for the L2 Rewards Reservoir + * @dev This exposes a specific function for the L2Reservoir that is called + * as a callhook from L1 to L2, so that state can be updated when dripped rewards + * are bridged between layers. + */ +interface IL2Reservoir is IReservoir { + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the normalizedTokenSupplyCache and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _normalizedTokenSupply, + uint256 _issuanceRate, + uint256 _nonce + ) external; +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol new file mode 100644 index 000000000..3b020648d --- /dev/null +++ b/contracts/reservoir/L1Reservoir.sol @@ -0,0 +1,294 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../arbitrum/ITokenGateway.sol"; + +import "./IReservoir.sol"; +import "./Reservoir.sol"; +import "./L1ReservoirStorage.sol"; + +/** + * @title L1 Rewards Reservoir + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1. + * It provides a function to periodically drip rewards, and functions to compute accumulated and new + * total rewards at a particular block number. + */ +contract L1Reservoir is L1ReservoirV1Storage, Reservoir { + using SafeMath for uint256; + + // Emitted when the initial supply snapshot is taken after contract deployment + event InitialSnapshotTaken( + uint256 _blockNumber, + uint256 _tokenSupplyCache, + uint256 _mintedPendingRewards + ); + // Emitted when an issuance rate update is staged, to be applied on the next drip + event IssuanceRateStaged(uint256 _newValue); + // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip + event L2RewardsFractionStaged(uint256 _newValue); + // Emitted when the L2 rewards fraction is updated (during a drip) + event L2RewardsFractionUpdated(uint256 _newValue); + // Emitted when the drip interval is updated + event DripIntervalUpdated(uint256 _newValue); + // Emitted when new rewards are dripped and potentially sent to L2 + event RewardsDripped(uint256 _totalMinted, uint256 _sentToL2, uint256 _nextDeadline); + // Emitted when the address for the L2Reservoir is updated + event L2ReservoirAddressUpdated(address _l2ReservoirAddress); + + /** + * @dev Initialize this contract. + * The contract will be paused. + * @param _controller Address of the Controller that manages this contract + * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip + */ + function initialize(address _controller, uint256 _dripInterval) external onlyImpl { + Managed._initialize(_controller); + dripInterval = _dripInterval; + } + + /** + * @dev Sets the drip interval. + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function setDripInterval(uint256 _dripInterval) external onlyGovernor { + require(_dripInterval > 0, "Drip interval must be > 0"); + dripInterval = _dripInterval; + emit DripIntervalUpdated(_dripInterval); + } + + /** + * @dev Sets the issuance rate. + * The issuance rate is defined as a relative increase of the total supply per block, plus 1. + * This means that it needs to be greater than 1.0, any number under 1.0 is not + * allowed and an issuance rate of 1.0 means no issuance. + * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. + * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 + */ + function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { + require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); + nextIssuanceRate = _issuanceRate; + emit IssuanceRateStaged(_issuanceRate); + } + + /** + * @dev Sets the L2 rewards fraction. + * This is the portion of the indexer rewards that are sent to L2. + * The value is in fixed point at 1e18 and must be less than 1. + * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 + */ + function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { + require(_l2RewardsFraction <= TOKEN_DECIMALS, "L2 Rewards fraction must be <= 1"); + nextL2RewardsFraction = _l2RewardsFraction; + emit L2RewardsFractionStaged(_l2RewardsFraction); + } + + /** + * @dev Sets the L2 Reservoir address + * This is the address on L2 to which we send tokens for rewards. + * @param _l2ReservoirAddress New address for the L2Reservoir on L2 + */ + function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { + l2ReservoirAddress = _l2ReservoirAddress; + emit L2ReservoirAddressUpdated(_l2ReservoirAddress); + } + + /** + * @dev Computes the initial snapshot for token supply and mints any pending rewards + * This will initialize the tokenSupplyCache to the current GRT supply, after which + * we will keep an internal accounting only using newly minted rewards. This function + * will also mint any pending rewards to cover up to the current block for open allocations, + * to be computed off-chain. + * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function + */ + function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + lastRewardsUpdateBlock = block.number; + IGraphToken grt = graphToken(); + grt.mint(address(this), pendingRewards); + tokenSupplyCache = grt.totalSupply(); + emit InitialSnapshotTaken(block.number, tokenSupplyCache, pendingRewards); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + */ + function drip( + uint256 l2MaxGas, + uint256 l2GasPriceBid, + uint256 l2MaxSubmissionCost + ) external payable notPaused { + uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); + uint256 mintedRewardsActual = getNewGlobalRewards(block.number); + // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + + if (nextIssuanceRate != issuanceRate) { + rewardsManager().updateAccRewardsPerSignal(); + snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock + issuanceRate = nextIssuanceRate; + emit IssuanceRateUpdated(issuanceRate); + } else { + snapshotAccumulatedRewards(mintedRewardsActual); + } + + rewardsMintedUntilBlock = block.number.add(dripInterval); + // n = deltaR(t1, t0) + uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); + // N = n - eps + uint256 tokensToMint = newRewardsToDistribute.add(mintedRewardsActual).sub( + mintedRewardsTotal + ); + + if (tokensToMint > 0) { + graphToken().mint(address(this), tokensToMint); + } + + uint256 tokensToSendToL2 = 0; + if (l2RewardsFraction != nextL2RewardsFraction) { + tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( + TOKEN_DECIMALS + ); + if (mintedRewardsTotal > mintedRewardsActual) { + // eps > 0, i.e. t < t1_old + // Note this can fail if the old l2RewardsFraction is larger + // than the new, in which case we just have to wait until enough time has passed + // so that eps is small enough. + tokensToSendToL2 = tokensToSendToL2.sub( + l2RewardsFraction.mul(mintedRewardsTotal.sub(mintedRewardsActual)).div( + TOKEN_DECIMALS + ) + ); + } else { + tokensToSendToL2 = tokensToSendToL2.add( + l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( + TOKEN_DECIMALS + ) + ); + } + l2RewardsFraction = nextL2RewardsFraction; + emit L2RewardsFractionUpdated(l2RewardsFraction); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + l2MaxGas, + l2GasPriceBid, + l2MaxSubmissionCost + ); + } else if (l2RewardsFraction > 0) { + tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); + _sendNewTokensAndStateToL2( + tokensToSendToL2, + l2MaxGas, + l2GasPriceBid, + l2MaxSubmissionCost + ); + } else { + // Avoid locking funds in this contract if we don't need to + // send a message to L2. + require(msg.value == 0, "No eth value needed"); + } + emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); + } + + /** + * @dev Snapshot accumulated rewards on this layer + * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. + * We also update the tokenSupplyCache by adding the new total rewards on both layers. + * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block + */ + function snapshotAccumulatedRewards(uint256 globalDelta) internal { + tokenSupplyCache = tokenSupplyCache + globalDelta; + // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, + // to save gas + accumulatedLayerRewards = + accumulatedLayerRewards + + globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + lastRewardsUpdateBlock = block.number; + } + + /** + * @dev Send new tokens and a message with state to L2 + * This function will use the L1GraphTokenGateway to send tokens + * to L2, and will also encode a callhook to update state on the L2Reservoir. + * @param nTokens Number of tokens to send to L2 + * @param maxGas Max gas for the L2 retryable ticket execution + * @param gasPriceBid Gas price for the L2 retryable ticket execution + * @param maxSubmissionCost Max submission price for the L2 retryable ticket + */ + function _sendNewTokensAndStateToL2( + uint256 nTokens, + uint256 maxGas, + uint256 gasPriceBid, + uint256 maxSubmissionCost + ) internal { + uint256 normalizedSupply = l2RewardsFraction.mul(tokenSupplyCache).div(TOKEN_DECIMALS); + bytes memory extraData = abi.encodeWithSelector( + IL2Reservoir.receiveDrip.selector, + normalizedSupply, + issuanceRate, + nextDripNonce + ); + nextDripNonce = nextDripNonce.add(1); + bytes memory data = abi.encode(maxSubmissionCost, extraData); + IGraphToken grt = graphToken(); + ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + grt.approve(address(gateway), nTokens); + gateway.outboundTransfer{ value: msg.value }( + address(grt), + l2ReservoirAddress, + nTokens, + maxGas, + gasPriceBid, + data + ); + } + + /** + * @dev Get new total rewards on both layers at a particular block, since the last drip event + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the total token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on both layers since the last drip + */ + function getNewGlobalRewards(uint256 blocknum) public view returns (uint256 deltaRewards) { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + return 0; + } + deltaRewards = tokenSupplyCache + .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(tokenSupplyCache); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * This is deltaR_L1 = (1-lambda) * deltaR, where: + * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) + * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on Layer 1 since the last drip + */ + function getNewRewards(uint256 blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( + TOKEN_DECIMALS + ); + } +} diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol new file mode 100644 index 000000000..f8254d59f --- /dev/null +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +/** + * @dev Storage variables for the L1Reservoir + */ +contract L1ReservoirV1Storage { + // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 + uint256 public l2RewardsFraction; + // New fraction of total rewards to be sent by L2, to be applied on the next drip + uint256 public nextL2RewardsFraction; + // Address for the L2Reservoir to which we send rewards + address public l2ReservoirAddress; + // Block until the minted supplies should last before another drip is needed + uint256 public rewardsMintedUntilBlock; + // Snapshot of initial token supply plus accumulated global rewards + uint256 public tokenSupplyCache; + // New issuance rate to be applied on the next drip + uint256 public nextIssuanceRate; + // Interval for rewards drip, in blocks + uint256 public dripInterval; + // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering + uint256 public nextDripNonce; +} diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol new file mode 100644 index 000000000..ba19de015 --- /dev/null +++ b/contracts/reservoir/Reservoir.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../upgrades/GraphUpgradeable.sol"; + +import "./ReservoirStorage.sol"; +import "./IReservoir.sol"; + +/** + * @title Rewards Reservoir base contract + * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 1 or Layer 2. + * It provides functions to compute accumulated and new total rewards at a particular block number. + * This base contract provides functionality that is common to L1 and L2, to be extended on each layer. + */ +abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { + using SafeMath for uint256; + + uint256 private constant MAX_UINT256 = 2**256 - 1; + uint256 internal constant TOKEN_DECIMALS = 1e18; + uint256 internal constant MIN_ISSUANCE_RATE = 1e18; + + /** + * @dev Approve the RewardsManager to manage the reservoir's token funds + */ + function approveRewardsManager() external override onlyGovernor { + graphToken().approve(address(rewardsManager()), MAX_UINT256); + } + + /** + * @dev Get accumulated total rewards on this layer at a particular block + * @param blocknum Block number at which to calculate rewards + * @return totalRewards Accumulated total rewards on this layer + */ + function getAccumulatedRewards(uint256 blocknum) + public + view + override + returns (uint256 totalRewards) + { + // R(t) = R(t0) + (DeltaR(t, t0)) + totalRewards = accumulatedLayerRewards + getNewRewards(blocknum); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event. + * Must be implemented by the reservoir on each layer. + * @param blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on this layer since the last drip + */ + function getNewRewards(uint256 blocknum) + public + view + virtual + override + returns (uint256 deltaRewards); + + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function _pow( + uint256 x, + uint256 n, + uint256 base + ) internal pure returns (uint256 z) { + // solhint-disable-next-line no-inline-assembly + assembly { + switch x + case 0 { + switch n + case 0 { + z := base + } + default { + z := 0 + } + } + default { + switch mod(n, 2) + case 0 { + z := base + } + default { + z := x + } + let half := div(base, 2) // for rounding. + for { + n := div(n, 2) + } n { + n := div(n, 2) + } { + let xx := mul(x, x) + if iszero(eq(div(xx, x), x)) { + revert(0, 0) + } + let xxRound := add(xx, half) + if lt(xxRound, xx) { + revert(0, 0) + } + x := div(xxRound, base) + if mod(n, 2) { + let zx := mul(z, x) + if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + revert(0, 0) + } + let zxRound := add(zx, half) + if lt(zxRound, zx) { + revert(0, 0) + } + z := div(zxRound, base) + } + } + } + } + } +} diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol new file mode 100644 index 000000000..18f1cf9d4 --- /dev/null +++ b/contracts/reservoir/ReservoirStorage.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "./IReservoir.sol"; +import "../governance/Managed.sol"; + +/** + * @dev Base storage variables for the Reservoir on both layers + */ +contract ReservoirV1Storage is Managed { + // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. + uint256 public issuanceRate; + // Accumulated total rewards on the corresponding layer (L1 or L2) + uint256 public accumulatedLayerRewards; + // Last block at which rewards when updated, i.e. block at which the last drip happened or was received + uint256 public lastRewardsUpdateBlock; +} diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..f92ca422a 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,8 +15,6 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; - function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; // -- Denylist -- @@ -54,6 +52,8 @@ interface IRewardsManager { function takeRewards(address _allocationID) external returns (uint256); + function takeAndBurnRewards(address _allocationID) external; + // -- Hooks -- function onSubgraphSignalUpdate(bytes32 _subgraphDeploymentID) external returns (uint256); diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6d2e78965..0711b531d 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -11,6 +11,8 @@ import "../staking/libs/MathUtils.sol"; import "./RewardsManagerStorage.sol"; import "./IRewardsManager.sol"; +import "../reservoir/IReservoir.sol"; + /** * @title Rewards Manager Contract * @dev Tracks how inflationary GRT rewards should be handed out. Relies on the Curation contract @@ -28,7 +30,7 @@ import "./IRewardsManager.sol"; * These functions may overestimate the actual rewards due to changes in the total supply * until the actual takeRewards function is called. */ -contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant TOKEN_DECIMALS = 1e18; @@ -47,9 +49,24 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ); /** - * @dev Emitted when rewards are denied to an indexer. + * @dev Emitted when rewards are denied to an indexer (and therefore burned). */ - event RewardsDenied(address indexed indexer, address indexed allocationID, uint256 epoch); + event RewardsDenied( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); + + /** + * @dev Emitted when rewards for an indexer are burned . + */ + event RewardsBurned( + address indexed indexer, + address indexed allocationID, + uint256 epoch, + uint256 amount + ); /** * @dev Emitted when a subgraph is denied for claiming rewards. @@ -75,32 +92,6 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa // -- Config -- - /** - * @dev Sets the issuance rate. - * The issuance rate is defined as a percentage increase of the total supply per block. - * This means that it needs to be greater than 1.0, any number under 1.0 is not - * allowed and an issuance rate of 1.0 means no issuance. - * To accommodate a high precision the issuance rate is expressed in wei. - * @param _issuanceRate Issuance rate expressed in wei - */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); - } - - /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate - */ - function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - - // Called since `issuance rate` will change - updateAccRewardsPerSignal(); - - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); - } - /** * @dev Sets the subgraph oracle allowed to denegate distribution of rewards to subgraphs. * @param _subgraphAvailabilityOracle Address of the subgraph availability oracle @@ -188,32 +179,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Gets the issuance of rewards per signal since last updated. * - * Compound interest formula: `a = p(1 + r/n)^nt` - * The formula is simplified with `n = 1` as we apply the interest once every time step. - * The `r` is passed with +1 included. So for 10% instead of 0.1 it is 1.1 - * The simplified formula is `a = p * r^t` - * - * Notation: - * t: time steps are in blocks since last updated - * p: total supply of GRT tokens - * a: inflated amount of total supply for the period `t` when interest `r` is applied - * x: newly accrued rewards token for the period `t` + * The compound interest formula is applied in the Reservoir contract. + * This function will compare accumulated rewards at the current block + * with the value that was cached at accRewardsPerSignalLastBlockUpdated. * * @return newly accrued rewards per signal since last update */ function getNewRewardsPerSignal() public view override returns (uint256) { - // Calculate time steps - uint256 t = block.number.sub(accRewardsPerSignalLastBlockUpdated); - // Optimization to skip calculations if zero time steps elapsed - if (t == 0) { - return 0; - } - - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { - return 0; - } - // Zero issuance if no signalled tokens IGraphToken graphToken = graphToken(); uint256 signalledTokens = graphToken.balanceOf(address(curation())); @@ -221,16 +193,14 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa return 0; } - uint256 r = issuanceRate; - uint256 p = tokenSupplySnapshot; - uint256 a = p.mul(_pow(r, t, TOKEN_DECIMALS)).div(TOKEN_DECIMALS); - - // New issuance of tokens during time steps - uint256 x = a.sub(p); + uint256 accRewardsNow = reservoir().getAccumulatedRewards(block.number); // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number - return x.mul(TOKEN_DECIMALS).div(signalledTokens); + return + (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(TOKEN_DECIMALS).div( + signalledTokens + ); } /** @@ -314,7 +284,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); + accRewardsOnLastSignalUpdate = reservoir().getAccumulatedRewards(block.number); return accRewardsPerSignal; } @@ -401,7 +371,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Pull rewards from the contract for a particular allocation. * This function can only be called by the Staking contract. - * This function will mint the necessary tokens to reward based on the inflation calculation. + * This function will transfer the necessary tokens to reward based on the inflation calculation. * @param _allocationID Allocation * @return Assigned rewards amount */ @@ -415,90 +385,60 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa alloc.subgraphDeploymentID ); - // Do not do rewards on denied subgraph deployments ID - if (isDenied(alloc.subgraphDeploymentID)) { - emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch); - return 0; - } - // Calculate rewards accrued by this allocation uint256 rewards = _calcRewards( alloc.tokens, alloc.accRewardsPerAllocatedToken, accRewardsPerAllocatedToken ); - if (rewards > 0) { - // Mint directly to staking contract for the reward amount + if (!isDenied(alloc.subgraphDeploymentID)) { + // Transfer to staking contract for the reward amount // The staking contract will do bookkeeping of the reward and // assign in proportion to each stakeholder incentive - graphToken().mint(address(staking), rewards); + if (rewards > 0) { + graphToken().transferFrom(address(reservoir()), address(staking), rewards); + } + emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + } else { + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + } + emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return 0; } - emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); - return rewards; } /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x + * @dev Burn rewards for a particular allocation. + * This function can only be called by the Staking contract. + * This function will burn the necessary tokens to reward based on the inflation calculation. + * @param _allocationID Allocation */ - function _pow( - uint256 x, - uint256 n, - uint256 base - ) private pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } + function takeAndBurnRewards(address _allocationID) external override { + // Only Staking contract is authorized as caller + IStaking staking = staking(); + require(msg.sender == address(staking), "Caller must be the staking contract"); + + IStaking.Allocation memory alloc = staking.getAllocation(_allocationID); + uint256 accRewardsPerAllocatedToken = onSubgraphAllocationUpdate( + alloc.subgraphDeploymentID + ); + + // Calculate rewards accrued by this allocation + uint256 rewards = _calcRewards( + alloc.tokens, + alloc.accRewardsPerAllocatedToken, + accRewardsPerAllocatedToken + ); + if (rewards > 0) { + graphToken().burnFrom(address(reservoir()), rewards); + emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); } } + + function reservoir() internal view returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..d8a6284e5 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -8,7 +8,7 @@ import "../governance/Managed.sol"; contract RewardsManagerV1Storage is Managed { // -- State -- - uint256 public issuanceRate; + uint256 public issuanceRateDeprecated; uint256 public accRewardsPerSignal; uint256 public accRewardsPerSignalLastBlockUpdated; @@ -29,5 +29,10 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { contract RewardsManagerV3Storage is RewardsManagerV2Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated - uint256 public tokenSupplySnapshot; + uint256 public tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // Accumulated rewards at accRewardsPerSignalLastBlockUpdated + uint256 public accRewardsOnLastSignalUpdate; } diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 2bcc8d74d..672052ed8 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1218,11 +1218,12 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { // Process non-zero-allocation rewards tracking if (alloc.tokens > 0) { - // Distribute rewards if proof of indexing was presented by the indexer or operator + // Distribute rewards if proof of indexing was presented by the indexer or operator, + // otherwise the rewards will be burned from the reservoir. if (isIndexer && _poi != 0) { _distributeRewards(_allocationID, alloc.indexer); } else { - _updateRewards(alloc.subgraphDeploymentID); + _takeAndBurnRewards(_allocationID); } // Free allocated tokens from use @@ -1601,7 +1602,7 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { } // Automatically triggers update of rewards snapshot as allocation will change - // after this call. Take rewards mint tokens for the Staking contract to distribute + // after this call. Take rewards transfers tokens for the Staking contract to distribute // between indexer and delegators uint256 totalRewards = rewardsManager.takeRewards(_allocationID); if (totalRewards == 0) { @@ -1621,6 +1622,21 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { ); } + /** + * @dev Burn rewards for the closed allocation and update the allocation state. + * @param _allocationID Allocation + */ + function _takeAndBurnRewards(address _allocationID) private { + IRewardsManager rewardsManager = rewardsManager(); + if (address(rewardsManager) == address(0)) { + return; + } + + // Automatically triggers update of rewards snapshot as allocation will change + // after this call. + rewardsManager.takeAndBurnRewards(_allocationID); + } + /** * @dev Send rewards to the appropiate destination. * @param _graphToken Graph token diff --git a/contracts/tests/ReservoirMock.sol b/contracts/tests/ReservoirMock.sol new file mode 100644 index 000000000..2ce40b3f2 --- /dev/null +++ b/contracts/tests/ReservoirMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "../reservoir/Reservoir.sol"; + +// Mock contract used for testing rewards +contract ReservoirMock is Reservoir { + function getNewRewards(uint256) public view override returns (uint256 r) {} + + /** + * @dev Raises x to the power of n with scaling factor of base. + * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 + * @param x Base of the exponentiation + * @param n Exponent + * @param base Scaling factor + * @return z Exponential of n with base x + */ + function pow( + uint256 x, + uint256 n, + uint256 base + ) public pure returns (uint256 z) { + z = _pow(x, n, base); + } +} diff --git a/contracts/tests/RewardsManagerMock.sol b/contracts/tests/RewardsManagerMock.sol deleted file mode 100644 index cbd57b2d3..000000000 --- a/contracts/tests/RewardsManagerMock.sol +++ /dev/null @@ -1,68 +0,0 @@ -pragma solidity ^0.7.6; -pragma abicoder v2; - -// Mock contract used for testing rewards -contract RewardsManagerMock { - /** - * @dev Raises x to the power of n with scaling factor of base. - * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x - */ - function pow( - uint256 x, - uint256 n, - uint256 base - ) public pure returns (uint256 z) { - assembly { - switch x - case 0 { - switch n - case 0 { - z := base - } - default { - z := 0 - } - } - default { - switch mod(n, 2) - case 0 { - z := base - } - default { - z := x - } - let half := div(base, 2) // for rounding. - for { - n := div(n, 2) - } n { - n := div(n, 2) - } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { - revert(0, 0) - } - let xxRound := add(xx, half) - if lt(xxRound, xx) { - revert(0, 0) - } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { - revert(0, 0) - } - let zxRound := add(zx, half) - if lt(zxRound, zx) { - revert(0, 0) - } - z := div(zxRound, base) - } - } - } - } - } -} diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index a5a473d86..f14204816 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -29,6 +29,7 @@ describe('L1GraphTokenGateway', () => { let mockL2GRT: Account let mockL2Gateway: Account let pauseGuardian: Account + let mockL2Reservoir: Account let fixture: NetworkFixture let grt: GraphToken @@ -58,8 +59,16 @@ describe('L1GraphTokenGateway', () => { ) before(async function () { - ;[governor, tokenSender, l2Receiver, mockRouter, mockL2GRT, mockL2Gateway, pauseGuardian] = - await getAccounts() + ;[ + governor, + tokenSender, + l2Receiver, + mockRouter, + mockL2GRT, + mockL2Gateway, + pauseGuardian, + mockL2Reservoir, + ] = await getAccounts() fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) @@ -371,6 +380,7 @@ describe('L1GraphTokenGateway', () => { mockRouter.address, mockL2GRT.address, mockL2Gateway.address, + mockL2Reservoir.address, ) }) diff --git a/test/l2/l2GraphTokenGateway.test.ts b/test/l2/l2GraphTokenGateway.test.ts index f283aabe1..5cec09962 100644 --- a/test/l2/l2GraphTokenGateway.test.ts +++ b/test/l2/l2GraphTokenGateway.test.ts @@ -28,6 +28,7 @@ describe('L2GraphTokenGateway', () => { let mockL1GRT: Account let mockL1Gateway: Account let pauseGuardian: Account + let mockL1Reservoir: Account let fixture: NetworkFixture let arbSysMock: FakeContract @@ -59,6 +60,7 @@ describe('L2GraphTokenGateway', () => { mockL1Gateway, l2Receiver, pauseGuardian, + mockL1Reservoir, ] = await getAccounts() fixture = new NetworkFixture() diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts new file mode 100644 index 000000000..0dd54f23c --- /dev/null +++ b/test/l2/l2Reservoir.test.ts @@ -0,0 +1,364 @@ +import { expect } from 'chai' +import { BigNumber, constants, ContractTransaction, utils } from 'ethers' + +import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { BigNumber as BN } from 'bignumber.js' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, + getL2SignerFromL1, +} from '../lib/testHelpers' +import { L2Reservoir } from '../../build/types/L2Reservoir' + +import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' +import { L2GraphToken } from '../../build/types/L2GraphToken' + +const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] + +const dripAmount = toBN('5851557519569225000000000') +const dripNormalizedSupply = toGRT('10004000000') +const dripIssuanceRate = toBN('1000000023206889619') + +describe('L2Reservoir', () => { + let governor: Account + let testAccount1: Account + let mockRouter: Account + let mockL1GRT: Account + let mockL1Gateway: Account + let mockL1Reservoir: Account + let fixture: NetworkFixture + + let grt: L2GraphToken + let l2Reservoir: L2Reservoir + let l2GraphTokenGateway: L2GraphTokenGateway + + let fixtureContracts: L2FixtureContracts + + let normalizedSupply: BigNumber + let dripBlock: BigNumber + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l2Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l2Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const gatewayFinalizeTransfer = async (callhookData: string): Promise => { + const mockL1GatewayL2Alias = await getL2SignerFromL1(mockL1Gateway.address) + await testAccount1.signer.sendTransaction({ + to: await mockL1GatewayL2Alias.getAddress(), + value: utils.parseUnits('1', 'ether'), + }) + const data = utils.defaultAbiCoder.encode(['bytes', 'bytes'], ['0x', callhookData]) + const tx = l2GraphTokenGateway + .connect(mockL1GatewayL2Alias) + .finalizeInboundTransfer( + mockL1GRT.address, + mockL1Reservoir.address, + l2Reservoir.address, + dripAmount, + data, + ) + return tx + } + + const validGatewayFinalizeTransfer = async ( + callhookData: string, + ): Promise => { + const tx = await gatewayFinalizeTransfer(callhookData) + await expect(tx) + .emit(l2GraphTokenGateway, 'DepositFinalized') + .withArgs(mockL1GRT.address, mockL1Reservoir.address, l2Reservoir.address, dripAmount) + + await expect(tx).emit(grt, 'BridgeMinted').withArgs(l2Reservoir.address, dripAmount) + + // newly minted GRT + const receiverBalance = await grt.balanceOf(l2Reservoir.address) + await expect(receiverBalance).eq(dripAmount) + return tx + } + + before(async function () { + ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir] = + await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.loadL2(governor.signer) + ;({ grt, l2Reservoir, l2GraphTokenGateway } = fixtureContracts) + await fixture.configureL2Bridge( + governor.signer, + fixtureContracts, + mockRouter.address, + mockL1GRT.address, + mockL1Gateway.address, + mockL1Reservoir.address, + ) + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('setNextDripNonce', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setNextDripNonce(toBN('10')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets the next expected drip nonce', async function () { + const tx = l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('10')) + await expect(tx).emit(l2Reservoir, 'NextDripNonceUpdated').withArgs(toBN('10')) + await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) + }) + }) + describe('receiveDrip', async function () { + it('rejects the call when not called by the gateway', async function () { + const tx = l2Reservoir + .connect(governor.signer) + .receiveDrip(dripNormalizedSupply, dripIssuanceRate, toBN('0')) + await expect(tx).revertedWith('ONLY_GATEWAY') + }) + it('rejects the call when received out of order', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + // Incorrect nonce + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('2'), + ) + const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(tx2).revertedWith('CALLHOOK_FAILED') // Gateway overrides revert message + }) + it('updates the normalized supply cache', async function () { + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + }) + it('updates the normalized supply cache and issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate.add(1), + toBN('1'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts subsequent calls without changing issuance rate', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('1'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + it('accepts a different nonce set through setNextDripNonce', async function () { + normalizedSupply = dripNormalizedSupply + let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + ) + let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + + await l2Reservoir.connect(governor.signer).setNextDripNonce(toBN('2')) + receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply.add(1), + dripIssuanceRate, + toBN('2'), + ) + tx = await gatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( + dripNormalizedSupply.add(1), + ) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) + await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + normalizedSupply = dripNormalizedSupply + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + ISSUANCE_RATE_PER_BLOCK, + toBN('0'), + ) + await validGatewayFinalizeTransfer(receiveDripTx.data) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(normalizedSupply) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = normalizedSupply + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + normalizedSupply, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(normalizedSupply, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(normalizedSupply.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l2Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l2Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) +}) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index a6afe9dbb..4bdcbe629 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -22,6 +22,8 @@ import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { L2Reservoir } from '../../build/types/L2Reservoir' // Disable logging for tests logger.pause() @@ -299,3 +301,29 @@ export async function deployL2GRT( deployer, ) as unknown as L2GraphToken } + +export async function deployL1Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L1Reservoir', + [controller, defaults.rewards.dripInterval], + deployer, + ) as unknown as L1Reservoir +} + +export async function deployL2Reservoir( + deployer: Signer, + controller: string, + proxyAdmin: GraphProxyAdmin, +): Promise { + return network.deployContractWithProxy( + proxyAdmin, + 'L2Reservoir', + [controller], + deployer, + ) as unknown as L2Reservoir +} diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 8375a86a4..3cd9d9b57 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -19,8 +19,10 @@ import { ServiceRegistry } from '../../build/types/ServiceRegistry' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' import { BridgeEscrow } from '../../build/types/BridgeEscrow' +import { L1Reservoir } from '../../build/types/L1Reservoir' import { L2GraphTokenGateway } from '../../build/types/L2GraphTokenGateway' import { L2GraphToken } from '../../build/types/L2GraphToken' +import { L2Reservoir } from '../../build/types/L2Reservoir' export interface L1FixtureContracts { controller: Controller @@ -35,6 +37,7 @@ export interface L1FixtureContracts { proxyAdmin: GraphProxyAdmin l1GraphTokenGateway: L1GraphTokenGateway bridgeEscrow: BridgeEscrow + l1Reservoir: L1Reservoir } export interface L2FixtureContracts { @@ -49,6 +52,7 @@ export interface L2FixtureContracts { serviceRegistry: ServiceRegistry proxyAdmin: GraphProxyAdmin l2GraphTokenGateway: L2GraphTokenGateway + l2Reservoir: L2Reservoir } export interface ArbitrumL1Mocks { @@ -114,12 +118,15 @@ export class NetworkFixture { let l1GraphTokenGateway: L1GraphTokenGateway let l2GraphTokenGateway: L2GraphTokenGateway let bridgeEscrow: BridgeEscrow + let l1Reservoir: L1Reservoir + let l2Reservoir: L2Reservoir if (isL2) { l2GraphTokenGateway = await deployment.deployL2GraphTokenGateway( deployer, controller.address, proxyAdmin, ) + l2Reservoir = await deployment.deployL2Reservoir(deployer, controller.address, proxyAdmin) } else { l1GraphTokenGateway = await deployment.deployL1GraphTokenGateway( deployer, @@ -127,6 +134,7 @@ export class NetworkFixture { proxyAdmin, ) bridgeEscrow = await deployment.deployBridgeEscrow(deployer, controller.address, proxyAdmin) + l1Reservoir = await deployment.deployL1Reservoir(deployer, controller.address, proxyAdmin) } // Setup controller @@ -139,8 +147,10 @@ export class NetworkFixture { await controller.setContractProxy(utils.id('ServiceRegistry'), serviceRegistry.address) if (isL2) { await controller.setContractProxy(utils.id('GraphTokenGateway'), l2GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l2Reservoir.address) } else { await controller.setContractProxy(utils.id('GraphTokenGateway'), l1GraphTokenGateway.address) + await controller.setContractProxy(utils.id('Reservoir'), l1Reservoir.address) } // Setup contracts @@ -152,15 +162,22 @@ export class NetworkFixture { await staking.connect(deployer).syncAllContracts() if (isL2) { await l2GraphTokenGateway.connect(deployer).syncAllContracts() + await l2Reservoir.connect(deployer).syncAllContracts() } else { await l1GraphTokenGateway.connect(deployer).syncAllContracts() await bridgeEscrow.connect(deployer).syncAllContracts() + await l1Reservoir.connect(deployer).syncAllContracts() } await staking.connect(deployer).setSlasher(slasherAddress, true) await gns.connect(deployer).approveAll() - if (!isL2) { - await grt.connect(deployer).addMinter(rewardsManager.address) + if (isL2) { + await l2Reservoir.connect(deployer).approveRewardsManager() + } else { + await grt.connect(deployer).addMinter(l1Reservoir.address) + await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) + await l1Reservoir.connect(deployer).approveRewardsManager() + await l1Reservoir.connect(deployer).initialSnapshot(toBN(0)) } // Unpause the protocol @@ -179,6 +196,7 @@ export class NetworkFixture { serviceRegistry, proxyAdmin, l2GraphTokenGateway, + l2Reservoir, } as L2FixtureContracts } else { return { @@ -194,6 +212,7 @@ export class NetworkFixture { proxyAdmin, l1GraphTokenGateway, bridgeEscrow, + l1Reservoir, } as L1FixtureContracts } } @@ -232,6 +251,7 @@ export class NetworkFixture { mockRouterAddress: string, mockL2GRTAddress: string, mockL2GatewayAddress: string, + mockL2ReservoirAddress: string, ): Promise { // First configure the Arbitrum bridge mocks await arbitrumMocks.bridgeMock.connect(deployer).setInbox(arbitrumMocks.inboxMock.address, true) @@ -254,9 +274,18 @@ export class NetworkFixture { await l1FixtureContracts.l1GraphTokenGateway .connect(deployer) .setEscrowAddress(l1FixtureContracts.bridgeEscrow.address) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) await l1FixtureContracts.bridgeEscrow .connect(deployer) .approveAll(l1FixtureContracts.l1GraphTokenGateway.address) + await l1FixtureContracts.l1Reservoir + .connect(deployer) + .setL2ReservoirAddress(mockL2ReservoirAddress) + await l1FixtureContracts.l1GraphTokenGateway + .connect(deployer) + .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 87e04beb1..60d48dccf 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -133,6 +133,98 @@ export const applyL1ToL2Alias = (l1Address: string): string => { return l2AddressAsNumber.mod(mask).toHexString() } +// Core formula that gets accumulated rewards for a period of time +const getRewards = (p: BN, r: BN, t: BN): string => { + BN.config({ POW_PRECISION: 100 }) + return p.times(r.pow(t)).minus(p).precision(18).toString(10) +} + +// Tracks the accumulated rewards as supply changes across snapshots +// both at a global level (like the Reservoir) and per signal (like RewardsManager) +export class RewardsTracker { + totalSupply = BigNumber.from(0) + lastUpdatedBlock = BigNumber.from(0) + lastPerSignalUpdatedBlock = BigNumber.from(0) + accumulated = BigNumber.from(0) + accumulatedPerSignal = BigNumber.from(0) + accumulatedAtLastPerSignalUpdatedBlock = BigNumber.from(0) + issuanceRate = BigNumber.from(0) + + static async create( + initialSupply: BigNumber, + issuanceRate: BigNumber, + updatedBlock?: BigNumber, + ): Promise { + const lastUpdatedBlock = updatedBlock || (await latestBlock()) + const tracker = new RewardsTracker(initialSupply, issuanceRate, lastUpdatedBlock) + return tracker + } + + constructor(initialSupply: BigNumber, issuanceRate: BigNumber, updatedBlock: BigNumber) { + this.issuanceRate = issuanceRate + this.totalSupply = initialSupply + this.lastUpdatedBlock = updatedBlock + this.lastPerSignalUpdatedBlock = updatedBlock + } + + async snapshotRewards(initialSupply?: BigNumber, atBlock?: BigNumber): Promise { + const newRewards = await this.newRewards(atBlock) + this.accumulated = this.accumulated.add(newRewards) + this.totalSupply = initialSupply || this.totalSupply.add(newRewards) + this.lastUpdatedBlock = atBlock || (await latestBlock()) + return this.accumulated + } + + async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) + this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) + this.lastPerSignalUpdatedBlock = atBlock + return this.accumulatedPerSignal + } + + async elapsedBlocks(): Promise { + const currentBlock = await latestBlock() + return currentBlock.sub(this.lastUpdatedBlock) + } + + async newRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + const accRewards = await this.accRewards(atBlock) + const diff = accRewards.sub(this.accumulatedAtLastPerSignalUpdatedBlock) + if (totalSignal.eq(0)) { + return BigNumber.from(0) + } + return diff.mul(toGRT(1)).div(totalSignal) + } + + async accRewardsPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { + return this.accumulatedPerSignal.add(await this.newRewardsPerSignal(totalSignal, atBlock)) + } + + async newRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + const nBlocks = atBlock.sub(this.lastUpdatedBlock) + return this.accruedByElapsed(nBlocks) + } + + async accRewards(atBlock?: BigNumber): Promise { + if (!atBlock) { + atBlock = await latestBlock() + } + return this.accumulated.add(await this.newRewards(atBlock)) + } + + async accruedByElapsed(nBlocks: BigNumber | number): Promise { + const n = getRewards( + new BN(this.totalSupply.toString()), + new BN(this.issuanceRate.toString()).div(1e18), + new BN(nBlocks.toString()), + ) + return BigNumber.from(n) + } +} + // Adapted from: // https://github.com/livepeer/arbitrum-lpt-bridge/blob/e1a81edda3594e434dbcaa4f1ebc95b7e67ecf2a/test/utils/messaging.ts#L5 export async function getL2SignerFromL1(l1Address: string): Promise { diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts new file mode 100644 index 000000000..7a4920159 --- /dev/null +++ b/test/reservoir/l1Reservoir.test.ts @@ -0,0 +1,750 @@ +import { expect } from 'chai' +import { BigNumber, constants, utils } from 'ethers' + +import { defaults, deployContract } from '../lib/deployment' +import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' + +import { GraphToken } from '../../build/types/GraphToken' +import { ReservoirMock } from '../../build/types/ReservoirMock' +import { BigNumber as BN } from 'bignumber.js' + +import { + advanceBlocks, + getAccounts, + latestBlock, + toBN, + toGRT, + formatGRT, + Account, + RewardsTracker, + provider, +} from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { BridgeEscrow } from '../../build/types/BridgeEscrow' + +import path from 'path' +import { Artifacts } from 'hardhat/internal/artifacts' +import { Interface } from 'ethers/lib/utils' +import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +const ARTIFACTS_PATH = path.resolve('build/contracts') +const artifacts = new Artifacts(ARTIFACTS_PATH) +const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi +const l2ReservoirIface = new Interface(l2ReservoirAbi) + +const { AddressZero } = constants +const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] + +const maxGas = toBN('1000000') +const maxSubmissionCost = toBN('7') +const gasPriceBid = toBN('2') +const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) + +describe('L1Reservoir', () => { + let governor: Account + let testAccount1: Account + let mockRouter: Account + let mockL2GRT: Account + let mockL2Gateway: Account + let mockL2Reservoir: Account + let fixture: NetworkFixture + + let grt: GraphToken + let reservoirMock: ReservoirMock + let l1Reservoir: L1Reservoir + let bridgeEscrow: BridgeEscrow + let l1GraphTokenGateway: L1GraphTokenGateway + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber + let fixtureContracts: L1FixtureContracts + let arbitrumMocks: ArbitrumL1Mocks + + const ISSUANCE_RATE_PERIODS = toBN(4) // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block + + // Test accumulated rewards after nBlocksToAdvance, + // asking for the value at blockToQuery + const shouldGetNewRewards = async ( + initialSupply: BigNumber, + nBlocksToAdvance: BigNumber = ISSUANCE_RATE_PERIODS, + blockToQuery?: BigNumber, + expectedValue?: BigNumber, + round = true, + ) => { + // -- t0 -- + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK) + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + // Jump + await advanceBlocks(nBlocksToAdvance) + + // -- t1 -- + + // Contract calculation + if (!blockToQuery) { + blockToQuery = await latestBlock() + } + const contractAccrued = await l1Reservoir.getAccumulatedRewards(blockToQuery) + // Local calculation + if (expectedValue == null) { + expectedValue = await tracker.newRewards(blockToQuery) + } + + // Check + if (round) { + expect(toRound(contractAccrued.sub(startAccrued))).eq(toRound(expectedValue)) + } else { + expect(contractAccrued.sub(startAccrued)).eq(expectedValue) + } + + return expectedValue + } + + const sequentialDoubleDrip = async ( + blocksToAdvance: BigNumber, + dripInterval = defaults.rewards.dripInterval, + ) => { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + const supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + let expectedNextDeadline = dripBlock.add(dripInterval) + let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(await latestBlock()).eq(dripBlock) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, actualAmount) + await tracker.snapshotRewards() + + await advanceBlocks(blocksToAdvance) + + const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) + expectedNextDeadline = (await latestBlock()).add(dripInterval) + const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) + expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) + expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) + expect(toRound(await l1Reservoir.tokenSupplyCache())).to.eq(toRound(expectedSnapshottedSupply)) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(newAmount, toBN(0), expectedNextDeadline) + await expect(tx2).emit(grt, 'Transfer').withArgs(AddressZero, l1Reservoir.address, newAmount) + } + + before(async function () { + ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir] = + await getAccounts() + + fixture = new NetworkFixture() + fixtureContracts = await fixture.load(governor.signer) + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway } = fixtureContracts) + + arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) + await fixture.configureL1Bridge( + governor.signer, + arbitrumMocks, + fixtureContracts, + mockRouter.address, + mockL2GRT.address, + mockL2Gateway.address, + mockL2Reservoir.address, + ) + reservoirMock = (await deployContract( + 'ReservoirMock', + governor.signer, + )) as unknown as ReservoirMock + }) + + beforeEach(async function () { + await fixture.setUp() + }) + + afterEach(async function () { + await fixture.tearDown() + }) + + describe('configuration', function () { + describe('initial snapshot', function () { + it('rejects call if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('snapshots the total GRT supply', async function () { + const tx = l1Reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + const supply = await grt.totalSupply() + await expect(tx) + .emit(l1Reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), supply, toGRT('0')) + expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supply) + expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('mints pending rewards and includes them in the snapshot', async function () { + const pending = toGRT('10000000') + const tx = l1Reservoir.connect(governor.signer).initialSnapshot(pending) + const supply = await grt.totalSupply() + const expectedSupply = supply.add(pending) + await expect(tx) + .emit(l1Reservoir, 'InitialSnapshotTaken') + .withArgs(await latestBlock(), expectedSupply, pending) + expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(expectedSupply) + expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + }) + describe('issuance rate update', function () { + it('rejects setting issuance rate if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setIssuanceRate(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting issuance rate to less than minimum allowed', async function () { + const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).revertedWith('Issuance rate under minimum allowed') + }) + + it('should set issuance rate to minimum allowed', async function () { + const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + }) + + it('should set issuance rate to apply on next drip', async function () { + const newIssuanceRate = toGRT('1.00025') + let tx = l1Reservoir.connect(governor.signer).setIssuanceRate(newIssuanceRate) + await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(0) + expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) + tx = l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) + expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) + }) + }) + describe('drip interval update', function () { + it('rejects setting drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setDripInterval(toBN(40800)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting drip interval to zero', async function () { + const tx = l1Reservoir.connect(governor.signer).setDripInterval(toBN(0)) + await expect(tx).revertedWith('Drip interval must be > 0') + }) + + it('updates the drip interval', async function () { + const newInterval = toBN(40800) + const tx = l1Reservoir.connect(governor.signer).setDripInterval(newInterval) + await expect(tx).emit(l1Reservoir, 'DripIntervalUpdated').withArgs(newInterval) + expect(await l1Reservoir.dripInterval()).eq(newInterval) + }) + }) + describe('L2 reservoir address update', function () { + it('rejects setting L2 reservoir address if unauthorized', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .setL2ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('updates the L2 reservoir address', async function () { + const tx = l1Reservoir.connect(governor.signer).setL2ReservoirAddress(testAccount1.address) + await expect(tx) + .emit(l1Reservoir, 'L2ReservoirAddressUpdated') + .withArgs(testAccount1.address) + expect(await l1Reservoir.l2ReservoirAddress()).eq(testAccount1.address) + }) + }) + describe('L2 rewards fraction update', function () { + it('rejects setting L2 rewards fraction if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setL2RewardsFraction(toGRT('1.025')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('rejects setting L2 rewards fraction to more than 1', async function () { + const newValue = toGRT('1').add(1) + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).revertedWith('L2 Rewards fraction must be <= 1') + }) + + it('should set L2 rewards fraction to maximum allowed', async function () { + const newValue = toGRT('1') // this gets a bignumber with 1e18 + const tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(0) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + }) + + it('should set L2 rewards fraction to apply on next drip', async function () { + const newValue = toGRT('0.25') + let tx = l1Reservoir.connect(governor.signer).setL2RewardsFraction(newValue) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) + expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) + tx = l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) + expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) + }) + }) + }) + + // TODO test that rewardsManager.updateAccRewardsPerSignal is called when + // issuanceRate or l2RewardsFraction is updated + describe('drip', function () { + it('mints rewards for the next week', async function () { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + }) + it('has no effect if called a second time in the same block', async function () { + // Initial snapshot defines the first lastRewardsUpdateBlock + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + await provider().send('evm_setAutomine', [false]) + const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) + + const actualAmount = await grt.balanceOf(l1Reservoir.address) + expect(await latestBlock()).eq(dripBlock) // Just in case disabling automine stops working + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) + await expect(tx1) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount, toBN(0), expectedNextDeadline) + await expect(tx1) + .emit(grt, 'Transfer') + .withArgs(AddressZero, l1Reservoir.address, actualAmount) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(toBN(0), toBN(0), expectedNextDeadline) + await expect(tx2).not.emit(grt, 'Transfer') + }) + it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await expect(tx).revertedWith('No eth value needed') + }) + it('mints only a few more tokens if called on the next block', async function () { + await sequentialDoubleDrip(toBN(0)) + }) + it('mints the right amount of tokens if called before the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('50'), dripInterval) + }) + it('mints the right amount of tokens filling the gap if called after the drip period is over', async function () { + const dripInterval = toBN('100') + await l1Reservoir.connect(governor.signer).setDripInterval(dripInterval) + await sequentialDoubleDrip(toBN('150'), dripInterval) + }) + it('sends the specified fraction of the rewards with a callhook to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the outstanding amount if the L2 rewards fraction changes', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.8')) + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock + .div(2) + .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) + + const tx2 = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('1'), // Incremented nonce + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction + const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) + + const tx2 = await l1Reservoir + .connect(governor.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('1'), // Incremented nonce + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx2) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) + }) + + context('calculating rewards', async function () { + beforeEach(async function () { + // 5% minute rate (4 blocks) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + supplyBeforeDrip = await grt.totalSupply() + await l1Reservoir.drip(toBN(0), toBN(0), toBN(0)) + dripBlock = await latestBlock() + }) + + describe('getAccumulatedRewards', function () { + it('returns rewards accrued after some blocks', async function () { + await shouldGetNewRewards(supplyBeforeDrip) + }) + it('returns zero if evaluated at the block where reservoir had the first drip', async function () { + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock, + toBN(0), + false, + ) + }) + it('returns the supply times issuance rate one block after the first drip', async function () { + const expectedVal = supplyBeforeDrip + .mul(ISSUANCE_RATE_PER_BLOCK.sub(toGRT(1))) + .div(toGRT(1)) + await shouldGetNewRewards( + supplyBeforeDrip, + ISSUANCE_RATE_PERIODS, + dripBlock.add(1), + expectedVal, + false, + ) + }) + it('returns the rewards for a block some time in the future', async function () { + await shouldGetNewRewards(supplyBeforeDrip, toBN(1), dripBlock.add(10000)) + }) + }) + describe('getNewRewards', function () { + const computeDelta = function (t1: BigNumber, t0: BigNumber, lambda = toBN(0)): BigNumber { + const deltaT = new BN(t1.toString()).minus(new BN(t0.toString())) + const rate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + const supply = new BN(supplyBeforeDrip.toString()) + return toBN(supply.times(rate.pow(deltaT)).minus(supply).precision(18).toString(10)) + .mul(toGRT('1').sub(lambda)) + .div(toGRT('1')) + } + it('computes the rewards delta between the last drip block and the current block', async function () { + const t0 = dripBlock + const t1 = t0.add(200) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('returns zero rewards if the time delta is zero', async function () { + const t0 = dripBlock + const expectedVal = toBN('0') + expect(await l1Reservoir.getNewRewards(t0)).to.eq(expectedVal) + }) + it('computes the rewards delta between a past drip block and a future block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = t0.add(100) + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta between a past drip block and the current block', async function () { + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + const expectedVal = computeDelta(t1, t0) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + it('computes the rewards delta considering the L2 rewards fraction', async function () { + const lambda = toGRT('0.32') + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) + await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + supplyBeforeDrip = await l1Reservoir.tokenSupplyCache() // Has been updated accordingly + dripBlock = await latestBlock() + await advanceBlocks(20) + const t0 = dripBlock + const t1 = await latestBlock() + + const expectedVal = computeDelta(t1, t0, lambda) + expect(toRound(await l1Reservoir.getNewRewards(t1))).to.eq(toRound(expectedVal)) + }) + }) + }) + + describe('pow', function () { + it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { + const baseRatio = toGRT('0.000000004641377923') // 1% annual rate + const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks + const powPrecision = 14 // Compare up to this amount of significant digits + BN.config({ POW_PRECISION: 100 }) + for (let i = 0; i < 50; i = i + 4) { + const r = baseRatio.mul(i * 4).add(toGRT('1')) + const h = await reservoirMock.pow(r, timePeriods, toGRT('1')) + console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) + expect(new BN(h.toString()).precision(powPrecision).toString(10)).to.eq( + new BN(r.toString()) + .div(1e18) + .pow(timePeriods) + .times(1e18) + .precision(powPrecision) + .toString(10), + ) + } + }) + }) +}) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 23aee0dfa..53958ad70 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -1,15 +1,12 @@ import { expect } from 'chai' -import { constants, BigNumber } from 'ethers' -import { BigNumber as BN } from 'bignumber.js' +import { constants, BigNumber, ContractReceipt } from 'ethers' -import { deployContract } from '../lib/deployment' import { NetworkFixture } from '../lib/fixtures' import { Curation } from '../../build/types/Curation' import { EpochManager } from '../../build/types/EpochManager' import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' -import { RewardsManagerMock } from '../../build/types/RewardsManagerMock' import { Staking } from '../../build/types/Staking' import { @@ -24,7 +21,10 @@ import { Account, advanceToNextEpoch, provider, + RewardsTracker, } from '../lib/testHelpers' +import { L1Reservoir } from '../../build/types/L1Reservoir' +import { LogDescription } from 'ethers/lib/utils' const MAX_PPM = 1000000 @@ -48,7 +48,10 @@ describe('Rewards', () => { let epochManager: EpochManager let staking: Staking let rewardsManager: RewardsManager - let rewardsManagerMock: RewardsManagerMock + let l1Reservoir: L1Reservoir + + let supplyBeforeDrip: BigNumber + let dripBlock: BigNumber // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -62,64 +65,18 @@ describe('Rewards', () => { const metadata = HashZero - const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 5% rewards - const ISSUANCE_RATE_PER_BLOCK = toBN('1012272234429039270') // % increase every block - - // Core formula that gets accumulated rewards per signal for a period of time - const getRewardsPerSignal = (p: BN, r: BN, t: BN, s: BN): string => { - if (s.eq(0)) { - return '0' - } - return p.times(r.pow(t)).minus(p).div(s).toPrecision(18).toString() - } - - // Tracks the accumulated rewards as totalSignalled or supply changes across snapshots - class RewardsTracker { - totalSupply = BigNumber.from(0) - totalSignalled = BigNumber.from(0) - lastUpdatedBlock = BigNumber.from(0) - accumulated = BigNumber.from(0) - - static async create() { - const tracker = new RewardsTracker() - await tracker.snapshot() - return tracker - } - - async snapshot() { - this.accumulated = this.accumulated.add(await this.accrued()) - this.totalSupply = await grt.totalSupply() - this.totalSignalled = await grt.balanceOf(curation.address) - this.lastUpdatedBlock = await latestBlock() - return this - } - - async elapsedBlocks() { - const currentBlock = await latestBlock() - return currentBlock.sub(this.lastUpdatedBlock) - } - - async accrued() { - const nBlocks = await this.elapsedBlocks() - return this.accruedByElapsed(nBlocks) - } - - async accruedByElapsed(nBlocks: BigNumber | number) { - const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), - new BN(nBlocks.toString()), - new BN(this.totalSignalled.toString()), - ) - return toGRT(n) - } - } + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 0.05% rewards + const ISSUANCE_RATE_PER_BLOCK = toBN('1000122722344290393') // % increase every block // Test accumulated rewards per signal - const shouldGetNewRewardsPerSignal = async (nBlocks = ISSUANCE_RATE_PERIODS) => { + const shouldGetNewRewardsPerSignal = async ( + initialSupply: BigNumber, + nBlocks = ISSUANCE_RATE_PERIODS, + dripBlock?: BigNumber, + ) => { // -- t0 -- - const tracker = await RewardsTracker.create() - + const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Jump await advanceBlocks(nBlocks) @@ -128,28 +85,36 @@ describe('Rewards', () => { // Contract calculation const contractAccrued = await rewardsManager.getNewRewardsPerSignal() // Local calculation - const expectedAccrued = await tracker.accrued() + const expectedAccrued = await tracker.newRewardsPerSignal(await grt.balanceOf(curation.address)) // Check - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) return expectedAccrued } + const findRewardsManagerEvents = (receipt: ContractReceipt): Array => { + return receipt.logs + .map((l) => { + try { + return rewardsManager.interface.parseLog(l) + } catch { + return null + } + }) + .filter((l) => !!l) + } + before(async function () { ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() fixture = new NetworkFixture() - ;({ grt, curation, epochManager, staking, rewardsManager } = await fixture.load( + ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( governor.signer, )) - rewardsManagerMock = (await deployContract( - 'RewardsManagerMock', - governor.signer, - )) as unknown as RewardsManagerMock - // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + // await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { @@ -157,6 +122,8 @@ describe('Rewards', () => { await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + supplyBeforeDrip = await grt.totalSupply() }) beforeEach(async function () { @@ -168,32 +135,6 @@ describe('Rewards', () => { }) describe('configuration', function () { - describe('issuance rate update', function () { - it('reject set issuance rate if unauthorized', async function () { - const tx = rewardsManager.connect(indexer1.signer).setIssuanceRate(toGRT('1.025')) - await expect(tx).revertedWith('Caller must be Controller governor') - }) - - it('reject set issuance rate to less than minimum allowed', async function () { - const newIssuanceRate = toGRT('0.1') // this get a bignumber with 1e17 - const tx = rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - await expect(tx).revertedWith('Issuance rate under minimum allowed') - }) - - it('should set issuance rate to minimum allowed', async function () { - const newIssuanceRate = toGRT('1') // this get a bignumber with 1e18 - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - }) - - it('should set issuance rate', async function () { - const newIssuanceRate = toGRT('1.025') - await rewardsManager.connect(governor.signer).setIssuanceRate(newIssuanceRate) - expect(await rewardsManager.issuanceRate()).eq(newIssuanceRate) - expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) - }) - }) - describe('subgraph availability service', function () { it('reject set subgraph oracle if unauthorized', async function () { const tx = rewardsManager @@ -243,9 +184,100 @@ describe('Rewards', () => { }) context('issuing rewards', async function () { + interface DelegationParameters { + indexingRewardCut: BigNumber + queryFeeCut: BigNumber + cooldownBlocks: number + } + + async function setupIndexerAllocation() { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + + async function setupIndexerAllocationSignalingAfter() { + // Setup + await epochManager.setEpochLength(10) + + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + } + + async function setupIndexerAllocationWithDelegation( + tokensToDelegate: BigNumber, + delegationParams: DelegationParameters, + ) { + const tokensToAllocate = toGRT('12500') + + // Transfer some funds from the curator, I don't want to mint new tokens + await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) + await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + + // Stake and set delegation parameters + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .setDelegationParameters( + delegationParams.indexingRewardCut, + delegationParams.queryFeeCut, + delegationParams.cooldownBlocks, + ) + + // Delegate + await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Allocate + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + } + beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + dripBlock = await latestBlock() }) describe('getNewRewardsPerSignal', function () { @@ -262,7 +294,7 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, tokensToSignal, 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) it('accrued per signal when signalled tokens w/ many subgraphs', async function () { @@ -270,78 +302,112 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) // Update total signalled await curation.connect(curator2.signer).mint(subgraphDeploymentID2, toGRT('250'), 0) // Check - await shouldGetNewRewardsPerSignal() + await shouldGetNewRewardsPerSignal(supplyBeforeDrip, ISSUANCE_RATE_PERIODS, dripBlock) }) }) describe('updateAccRewardsPerSignal', function () { it('update the accumulated rewards per signal state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + tracker.snapshotPerSignal(prevSignal) // Update await rewardsManager.updateAccRewardsPerSignal() + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + const contractAccrued = await rewardsManager.accRewardsPerSignal() // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum, + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) it('update the accumulated rewards per signal state after many blocks', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) - // Snapshot - const tracker = await RewardsTracker.create() + // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, + // so we snapshot using the previous value + tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() + tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - // Check - const expectedAccrued = await tracker.accrued() - expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) + const blockNum = await latestBlock() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + blockNum.add(0), + ) + expect(toRound(contractAccrued)).eq(toRound(expectedAccrued)) }) }) describe('getAccRewardsForSubgraph', function () { it('accrued for each subgraph', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + let prevSignal = await grt.balanceOf(curation.address) // Curator1 - Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - const tracker1 = await RewardsTracker.create() + const sg1Snapshot = await tracker.snapshotPerSignal(prevSignal) // Curator2 - Update total signalled const signalled2 = toGRT('500') + prevSignal = await grt.balanceOf(curation.address) await curation.connect(curator2.signer).mint(subgraphDeploymentID2, signalled2, 0) - - // Snapshot - const tracker2 = await RewardsTracker.create() - await tracker1.snapshot() + const sg2Snapshot = await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - // Snapshot - await tracker1.snapshot() - await tracker2.snapshot() - // Calculate rewards - const rewardsPerSignal1 = await tracker1.accumulated - const rewardsPerSignal2 = await tracker2.accumulated - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) - const expectedRewardsSG2 = rewardsPerSignal2.mul(signalled2).div(WeiPerEther) + const rewardsPerSignal = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) + const expectedRewardsSG1 = rewardsPerSignal + .sub(sg1Snapshot) + .mul(signalled1) + .div(WeiPerEther) + const expectedRewardsSG2 = rewardsPerSignal + .sub(sg2Snapshot) + .mul(signalled2) + .div(WeiPerEther) // Get rewards from contract const contractRewardsSG1 = await rewardsManager.getAccRewardsForSubgraph( @@ -359,27 +425,35 @@ describe('Rewards', () => { describe('onSubgraphSignalUpdate', function () { it('update the accumulated rewards for subgraph state', async function () { + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + ISSUANCE_RATE_PER_BLOCK, + dripBlock, + ) + // Snapshot + const prevSignal = await grt.balanceOf(curation.address) // Update total signalled const signalled1 = toGRT('1500') await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) // Snapshot - const tracker1 = await RewardsTracker.create() + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.onSubgraphSignalUpdate(subgraphDeploymentID1) - + const snapshot = await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Check const contractRewardsSG1 = (await rewardsManager.subgraphs(subgraphDeploymentID1)) .accRewardsForSubgraph - const rewardsPerSignal1 = await tracker1.accrued() - const expectedRewardsSG1 = rewardsPerSignal1.mul(signalled1).div(WeiPerEther) + const expectedRewardsSG1 = snapshot.mul(signalled1).div(WeiPerEther) expect(toRound(expectedRewardsSG1)).eq(toRound(contractRewardsSG1)) const contractAccrued = await rewardsManager.accRewardsPerSignal() - const expectedAccrued = await tracker1.accrued() + const expectedAccrued = await tracker.accRewardsPerSignal( + await grt.balanceOf(curation.address), + ) expect(toRound(expectedAccrued)).eq(toRound(contractAccrued)) const contractBlockUpdated = await rewardsManager.accRewardsPerSignalLastBlockUpdated() @@ -430,11 +504,14 @@ describe('Rewards', () => { it('update the accumulated rewards for allocated tokens state', async function () { // Update total signalled const signalled1 = toGRT('1500') + // block = dripBlock await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // block = dripBlock + 1 // Allocate const tokensToAllocate = toGRT('12500') await staking.connect(indexer1.signer).stake(tokensToAllocate) + // block = dripBlock + 2 await staking .connect(indexer1.signer) .allocateFrom( @@ -445,19 +522,28 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - + // block = dripBlock + 3 // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) - - // Prepare expected results - // NOTE: calculated the expected result manually as the above code has 1 off block difference - // replace with a RewardsManagerMock - const expectedSubgraphRewards = toGRT('891695470') - const expectedRewardsAT = toGRT('51571') + // block = dripBlock + 7 // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) + // block = dripBlock + 8 + // Prepare expected results + // Expected total rewards: + // DeltaR_end = supplyBeforeDrip * r ^ 8 - supplyBeforeDrip + // DeltaR_end = 10004000000 GRT * (1000122722344290393 / 1e18)^8 - 10004000000 GRT = 9825934.397 + // The signal was minted at dripBlock + 1, so: + // DeltaR_start = supplyBeforeDrip * r ^ 1 - supplyBeforeDrip = 1227714.332 + + // And they all go to this subgraph, so subgraph rewards = DeltaR_end - DeltaR_start = 8598220.065 + const expectedSubgraphRewards = toGRT('8598220') + + // The allocation happened at dripBlock + 3, so rewards per allocated token are: + // ((supplyBeforeDrip * r ^ 8 - supplyBeforeDrip) - (supplyBeforeDrip * r ^ 3 - supplyBeforeDrip)) / 12500 = 491.387 + const expectedRewardsAT = toGRT('491') // Check on demand results saved const subgraph = await rewardsManager.subgraphs(subgraphDeploymentID1) const contractSubgraphRewards = await rewardsManager.getAccRewardsForSubgraph( @@ -506,111 +592,85 @@ describe('Rewards', () => { expect(expectedRewards).eq(contractRewards) }) }) - - describe('takeRewards', function () { - interface DelegationParameters { - indexingRewardCut: BigNumber - queryFeeCut: BigNumber - cooldownBlocks: number - } - - async function setupIndexerAllocation() { - // Setup + describe('takeAndBurnRewards', function () { + it('should burn rewards on closed allocation with POI zero', async function () { + // Align with the epoch boundary + // dripBlock (81) await epochManager.setEpochLength(10) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } - - async function setupIndexerAllocationSignalingAfter() { + // dripBlock + 1 + await advanceToNextEpoch(epochManager) + // dripBlock + 4 // Setup - await epochManager.setEpochLength(10) - - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - } + await setupIndexerAllocation() + // dripBlock + 7 + // Jump + await advanceToNextEpoch(epochManager) + // dripBlock + 14 - async function setupIndexerAllocationWithDelegation( - tokensToDelegate: BigNumber, - delegationParams: DelegationParameters, - ) { - const tokensToAllocate = toGRT('12500') + // Before state + const beforeTokenSupply = await grt.totalSupply() + const beforeIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) + const beforeStakingBalance = await grt.balanceOf(staking.address) - // Setup - await epochManager.setEpochLength(10) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. + const expectedIndexingRewards = toGRT('9834378') - // Transfer some funds from the curator, I don't want to mint new tokens - await grt.connect(curator1.signer).transfer(delegator.address, tokensToDelegate) - await grt.connect(delegator.signer).approve(staking.address, tokensToDelegate) + // Close allocation with POI zero, which should burn the rewards + const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) + const receipt = await tx.wait() - // Stake and set delegation parameters - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .setDelegationParameters( - delegationParams.indexingRewardCut, - delegationParams.queryFeeCut, - delegationParams.cooldownBlocks, - ) + const log = findRewardsManagerEvents(receipt)[0] + const event = log.args + expect(log.name).eq('RewardsBurned') + expect(event.indexer).eq(indexer1.address) + expect(event.allocationID).eq(allocationID1) + expect(event.epoch).eq(await epochManager.currentEpoch()) + expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) - // Delegate - await staking.connect(delegator.signer).delegate(indexer1.address, tokensToDelegate) + // After state + const afterTokenSupply = await grt.totalSupply() + const afterIndexer1Stake = await staking.getIndexerStakedTokens(indexer1.address) + const afterIndexer1Balance = await grt.balanceOf(indexer1.address) + const afterStakingBalance = await grt.balanceOf(staking.address) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Check that rewards are NOT put into indexer stake + const expectedIndexerStake = beforeIndexer1Stake - // Allocate - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - } + // Check stake should NOT have increased with the rewards staked + expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) + // Check indexer balance remains the same + expect(afterIndexer1Balance).eq(beforeIndexer1Balance) + // Check indexing rewards are kept in the staking contract + expect(toRound(afterStakingBalance)).eq(toRound(beforeStakingBalance)) + // Check that tokens have been burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound(afterTokenSupply.div(10))).eq( + toRound(beforeTokenSupply.sub(expectedIndexingRewards).div(10)), + ) + }) + }) + describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary + // dripBlock (81) + await epochManager.setEpochLength(10) + // dripBlock + 1 await advanceToNextEpoch(epochManager) + // dripBlock + 4 // Setup await setupIndexerAllocation() - + // dripBlock + 7 // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -620,19 +680,20 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + const expectedIndexingRewards = toGRT('9834378') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -646,7 +707,7 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake const expectedIndexerStake = beforeIndexer1Stake.add(expectedIndexingRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should have increased with the rewards staked expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexer balance remains the same @@ -655,8 +716,8 @@ describe('Rewards', () => { expect(toRound(afterStakingBalance)).eq( toRound(beforeStakingBalance.add(expectedIndexingRewards)), ) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('does not revert with an underflow if the minimum signal changes', async function () { @@ -716,6 +777,7 @@ describe('Rewards', () => { const destinationAddress = randomHexBytes(20) await staking.connect(indexer1.signer).setRewardsDestination(destinationAddress) + await epochManager.setEpochLength(10) // Align with the epoch boundary await advanceToNextEpoch(epochManager) // Setup @@ -732,19 +794,19 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 12500 = 122945.16 - // The final snapshot is when we close the allocation, that happens 9 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 9 - 10004000000) / 12500 = 92861.24 + // The first snapshot is after allocating, that is 7 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 + // The final snapshot is when we close the allocation, that happens 8 blocks later: + // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + const expectedIndexingRewards = toGRT('9834378') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() - const event = rewardsManager.interface.parseLog(receipt.logs[1]).args + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) @@ -758,7 +820,7 @@ describe('Rewards', () => { // Check that rewards are properly assigned const expectedIndexerStake = beforeIndexer1Stake - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) + // Check stake should not have changed expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) // Check indexing rewards are received by the rewards destination @@ -767,8 +829,8 @@ describe('Rewards', () => { ) // Check indexing rewards were not sent to the staking contract expect(afterStakingBalance).eq(beforeStakingBalance) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) it('should distribute rewards on closed allocation w/delegators', async function () { @@ -779,14 +841,18 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') - + // dripBlock (81) + await epochManager.setEpochLength(10) + // dripBlock + 1 // Align with the epoch boundary await advanceToNextEpoch(epochManager) + // dripBlock + 4 // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) - + // dripBlock + 11 // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -806,12 +872,12 @@ describe('Rewards', () => { // All the rewards in this subgraph go to this allocation. // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 + // The first snapshot is after allocating, that is 11 blocks after dripBlock: + // startRewardsPerToken = (10004000000 * 1.01227 ^ 11 - 10004000000) / 14500 = 931.94 // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 + // endRewardsPerToken = (10004000000 * 1.01227 ^ 15 - 10004000000) / 14500 = 1271.14 // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + const expectedIndexingRewards = toGRT('4918396') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -821,101 +887,151 @@ describe('Rewards', () => { // Check const expectedIndexerStake = beforeIndexer1Stake.add(indexerRewards) const expectedDelegatorsPoolTokens = beforeDelegationPool.tokens.add(delegatorsRewards) - const expectedTokenSupply = beforeTokenSupply.add(expectedIndexingRewards) expect(toRound(afterIndexer1Stake)).eq(toRound(expectedIndexerStake)) expect(toRound(afterDelegationPool.tokens)).eq(toRound(expectedDelegatorsPoolTokens)) - // Check that tokens have been minted - expect(toRound(afterTokenSupply)).eq(toRound(expectedTokenSupply)) + // Check that tokens have NOT been minted + expect(toRound(afterTokenSupply)).eq(toRound(beforeTokenSupply)) }) - it('should deny rewards if subgraph on denylist', async function () { + it('should deny and burn rewards if subgraph on denylist', async function () { // Setup + await epochManager.setEpochLength(10) await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) + await advanceToNextEpoch(epochManager) await setupIndexerAllocation() // Jump await advanceToNextEpoch(epochManager) + // This is the same amount as in the test above + // (see 'should distribute rewards on closed allocation and stake') + const expectedIndexingRewards = toGRT('9834378') + const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - await expect(tx) - .emit(rewardsManager, 'RewardsDenied') - .withArgs(indexer1.address, allocationID1, await epochManager.currentEpoch()) + await expect(tx).emit(rewardsManager, 'RewardsDenied') + const receipt = await (await tx).wait() + const logs = findRewardsManagerEvents(receipt) + expect(logs.length).to.eq(1) + expect(logs[0].name).to.eq('RewardsDenied') + const ev = logs[0].args + expect(ev.indexer).to.eq(indexer1.address) + expect(ev.allocationID).to.eq(allocationID1) + expect(ev.epoch).to.eq(await epochManager.currentEpoch()) + expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) + // Check that the rewards were burned + // We divide by 10 to accept numeric errors up to 10 GRT + expect(toRound((await grt.totalSupply()).div(10))).to.eq( + toRound(supplyBefore.sub(expectedIndexingRewards).div(10)), + ) }) }) - }) - describe('pow', function () { - it('exponentiation works under normal boundaries (annual rate from 1% to 700%, 90 days period)', async function () { - const baseRatio = toGRT('0.000000004641377923') // 1% annual rate - const timePeriods = (60 * 60 * 24 * 10) / 15 // 90 days in blocks - for (let i = 0; i < 50; i = i + 4) { - const r = baseRatio.mul(i * 4).add(toGRT('1')) - const h = await rewardsManagerMock.pow(r, timePeriods, toGRT('1')) - console.log('\tr:', formatGRT(r), '=> c:', formatGRT(h)) - } - }) - }) + describe('edge scenarios', function () { + it('close allocation on a subgraph that no longer have signal', async function () { + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - describe('edge scenarios', function () { - it('close allocation on a subgraph that no longer have signal', async function () { - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate + const tokensToAllocate = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToAllocate) + await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAllocate, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) - // Allocate - const tokensToAllocate = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToAllocate) - await staking - .connect(indexer1.signer) - .allocateFrom( - indexer1.address, + // Jump + await advanceToNextEpoch(epochManager) + + // Remove all signal from the subgraph + const curatorShares = await curation.getCuratorSignal( + curator1.address, subgraphDeploymentID1, - tokensToAllocate, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), ) + await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) - // Jump - await advanceToNextEpoch(epochManager) + // Close allocation. At this point rewards should be collected for that indexer + await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + }) + }) - // Remove all signal from the subgraph - const curatorShares = await curation.getCuratorSignal(curator1.address, subgraphDeploymentID1) - await curation.connect(curator1.signer).burn(subgraphDeploymentID1, curatorShares, 0) + describe('multiple allocations', function () { + it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { + // If rewards are not monotonically increasing, this can trigger + // a subtraction overflow error as seen in mainnet tx: + // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab + await advanceToNextEpoch(epochManager) - // Close allocation. At this point rewards should be collected for that indexer - await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) - }) - }) + // Setup + await epochManager.setEpochLength(10) - describe('multiple allocations', function () { - it('two allocations in the same block with a GRT burn in the middle should succeed', async function () { - // If rewards are not monotonically increasing, this can trigger - // a subtraction overflow error as seen in mainnet tx: - // 0xb6bf7bbc446720a7409c482d714aebac239dd62e671c3c94f7e93dd3a61835ab - await advanceToNextEpoch(epochManager) + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) - // Setup - await epochManager.setEpochLength(10) + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Allocate simultaneously, burning in the middle + const tokensToAlloc = toGRT('5000') + await provider().send('evm_setAutomine', [false]) + const tx1 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID1, + metadata, + await channelKey1.generateProof(indexer1.address), + ) + const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) + const tx3 = await staking + .connect(indexer1.signer) + .allocateFrom( + indexer1.address, + subgraphDeploymentID1, + tokensToAlloc, + allocationID2, + metadata, + await channelKey2.generateProof(indexer1.address), + ) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) + await provider().send('evm_mine', []) + await provider().send('evm_setAutomine', [true]) - // Allocate simultaneously, burning in the middle - const tokensToAlloc = toGRT('5000') - await provider().send('evm_setAutomine', [false]) - const tx1 = await staking - .connect(indexer1.signer) - .allocateFrom( + await expect(tx1).emit(staking, 'AllocationCreated') + await expect(tx2).emit(grt, 'Transfer') + await expect(tx3).emit(staking, 'AllocationCreated') + }) + it('two simultanous-similar allocations should get same amount of rewards', async function () { + await advanceToNextEpoch(epochManager) + + // Setup + await epochManager.setEpochLength(10) + + // Update total signalled + const signalled1 = toGRT('1500') + await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + + // Stake + const tokensToStake = toGRT('12500') + await staking.connect(indexer1.signer).stake(tokensToStake) + + // Allocate simultaneously + const tokensToAlloc = toGRT('5000') + const tx1 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -923,10 +1039,7 @@ describe('Rewards', () => { metadata, await channelKey1.generateProof(indexer1.address), ) - const tx2 = await grt.connect(indexer1.signer).burn(toGRT(1)) - const tx3 = await staking - .connect(indexer1.signer) - .allocateFrom( + const tx2 = await staking.populateTransaction.allocateFrom( indexer1.address, subgraphDeploymentID1, tokensToAlloc, @@ -934,61 +1047,31 @@ describe('Rewards', () => { metadata, await channelKey2.generateProof(indexer1.address), ) + await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) - - await expect(tx1).emit(staking, 'AllocationCreated') - await expect(tx2).emit(grt, 'Transfer') - await expect(tx3).emit(staking, 'AllocationCreated') - }) - it('two simultanous-similar allocations should get same amount of rewards', async function () { - await advanceToNextEpoch(epochManager) - - // Setup - await epochManager.setEpochLength(10) - - // Update total signalled - const signalled1 = toGRT('1500') - await curation.connect(curator1.signer).mint(subgraphDeploymentID1, signalled1, 0) + // Jump + await advanceToNextEpoch(epochManager) - // Stake - const tokensToStake = toGRT('12500') - await staking.connect(indexer1.signer).stake(tokensToStake) - - // Allocate simultaneously - const tokensToAlloc = toGRT('5000') - const tx1 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID1, - metadata, - await channelKey1.generateProof(indexer1.address), - ) - const tx2 = await staking.populateTransaction.allocateFrom( - indexer1.address, - subgraphDeploymentID1, - tokensToAlloc, - allocationID2, - metadata, - await channelKey2.generateProof(indexer1.address), - ) - await staking.connect(indexer1.signer).multicall([tx1.data, tx2.data]) - - // Jump - await advanceToNextEpoch(epochManager) - - // Close allocations simultaneously - const tx3 = await staking.populateTransaction.closeAllocation(allocationID1, randomHexBytes()) - const tx4 = await staking.populateTransaction.closeAllocation(allocationID2, randomHexBytes()) - const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) - - // Both allocations should receive the same amount of rewards - const receipt = await tx5.wait() - const event1 = rewardsManager.interface.parseLog(receipt.logs[1]).args - const event2 = rewardsManager.interface.parseLog(receipt.logs[5]).args - expect(event1.amount).eq(event2.amount) + // Close allocations simultaneously + const tx3 = await staking.populateTransaction.closeAllocation( + allocationID1, + randomHexBytes(), + ) + const tx4 = await staking.populateTransaction.closeAllocation( + allocationID2, + randomHexBytes(), + ) + const tx5 = await staking.connect(indexer1.signer).multicall([tx3.data, tx4.data]) + + // Both allocations should receive the same amount of rewards + const receipt = await tx5.wait() + const rewardsMgrEvents = findRewardsManagerEvents(receipt) + expect(rewardsMgrEvents.length).to.eq(2) + const event1 = rewardsMgrEvents[0].args + const event2 = rewardsMgrEvents[1].args + expect(event1.amount).to.not.eq(toBN(0)) + expect(event1.amount).to.eq(event2.amount) + }) }) }) }) From bef792e127cc0a124ab6eb48ce2b74591493ef40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:04:51 +0200 Subject: [PATCH 02/47] fix: document potential drip reverts if issuance rate is updated [L-01] --- contracts/reservoir/L1Reservoir.sol | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 3b020648d..b1058c8f3 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -125,6 +125,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 @@ -151,9 +154,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // n = deltaR(t1, t0) uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); // N = n - eps - uint256 tokensToMint = newRewardsToDistribute.add(mintedRewardsActual).sub( - mintedRewardsTotal - ); + uint256 tokensToMint; + { + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); + require( + newRewardsPlusMintedActual >= mintedRewardsTotal, + "Would mint negative tokens, wait before calling again" + ); + tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); + } if (tokensToMint > 0) { graphToken().mint(address(this), tokensToMint); From a34e508074cd8dbe487dafec19379f3e67fd3891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:14:25 +0200 Subject: [PATCH 03/47] fix: document drip revert when l2RewardsFraction changed [L-02] --- contracts/reservoir/L1Reservoir.sol | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b1058c8f3..a4c9a5937 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -127,7 +127,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * only be nonzero if l2RewardsFraction is nonzero. * Calling this function can revert if the issuance rate has recently been reduced, and the existing * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait - * until the drip amount becomes positive before calling the function again. + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 @@ -177,12 +179,17 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // eps > 0, i.e. t < t1_old // Note this can fail if the old l2RewardsFraction is larger // than the new, in which case we just have to wait until enough time has passed - // so that eps is small enough. - tokensToSendToL2 = tokensToSendToL2.sub( - l2RewardsFraction.mul(mintedRewardsTotal.sub(mintedRewardsActual)).div( - TOKEN_DECIMALS - ) + // so that eps is small enough. This also applies to the case where the new + // l2RewardsFraction is zero, since we still need to send one last message + // with the new values to the L2Reservoir. + uint256 l2OffsetAmount = l2RewardsFraction + .mul(mintedRewardsTotal.sub(mintedRewardsActual)) + .div(TOKEN_DECIMALS); + require( + tokensToSendToL2 > l2OffsetAmount, + "Negative amount would be sent to L2, wait before calling again" ); + tokensToSendToL2 = tokensToSendToL2.sub(l2OffsetAmount); } else { tokensToSendToL2 = tokensToSendToL2.add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( From d2e53fd6a33beb9f22f3035518823d8cf40276ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:40:36 +0200 Subject: [PATCH 04/47] fix: rename variables related to supply to issuanceBase to make it clear they're decoupled [L-03] --- contracts/l2/reservoir/L2Reservoir.sol | 18 +++++------ contracts/l2/reservoir/L2ReservoirStorage.sol | 2 -- contracts/reservoir/IReservoir.sol | 6 ++-- contracts/reservoir/L1Reservoir.sol | 18 +++++------ contracts/reservoir/L1ReservoirStorage.sol | 2 -- contracts/reservoir/ReservoirStorage.sol | 2 ++ test/l2/l2Reservoir.test.ts | 22 +++++-------- test/reservoir/l1Reservoir.test.ts | 32 +++++++++---------- 8 files changed, 47 insertions(+), 55 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 6bdd9ca67..8948f9ee1 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -18,7 +18,7 @@ import "./L2ReservoirStorage.sol"; contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; - event DripReceived(uint256 _normalizedTokenSupply); + event DripReceived(uint256 _issuanceBase); event NextDripNonceUpdated(uint256 _nonce); /** @@ -51,7 +51,7 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { /** * @dev Get new total rewards accumulated since the last drip. * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the normalized token supply snapshot at t0 + * - p is the issuance base at t0 (normalized by the L2 rewards fraction) * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param blocknum Block number at which to calculate rewards @@ -67,25 +67,25 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { return 0; } - deltaRewards = normalizedTokenSupplyCache + deltaRewards = issuanceBase .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) - .sub(normalizedTokenSupplyCache); + .sub(issuanceBase); } /** * @dev Receive dripped tokens from L1. * This function can only be called by the gateway, as it is * meant to be a callhook when receiving tokens from L1. It - * updates the normalizedTokenSupplyCache and issuanceRate, + * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order */ function receiveDrip( - uint256 _normalizedTokenSupply, + uint256 _issuanceBase, uint256 _issuanceRate, uint256 _nonce ) external override onlyL2Gateway { @@ -99,8 +99,8 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { } else { snapshotAccumulatedRewards(); } - normalizedTokenSupplyCache = _normalizedTokenSupply; - emit DripReceived(normalizedTokenSupplyCache); + issuanceBase = _issuanceBase; + emit DripReceived(issuanceBase); } /** diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index c6ad73e1c..ee7880343 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -6,8 +6,6 @@ pragma solidity ^0.7.6; * @dev Storage variables for the L2Reservoir */ contract L2ReservoirV1Storage { - // Snapshot of total GRT supply multiplied by L2 rewards fraction, received from L1 - uint256 public normalizedTokenSupplyCache; // Expected nonce value for the next drip hook uint256 public nextDripNonce; } diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index dfc64f14f..996f3fc2f 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -43,15 +43,15 @@ interface IL2Reservoir is IReservoir { * @dev Receive dripped tokens from L1. * This function can only be called by the gateway, as it is * meant to be a callhook when receiving tokens from L1. It - * updates the normalizedTokenSupplyCache and issuanceRate, + * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _normalizedTokenSupply Snapshot of total GRT supply multiplied by L2 rewards fraction + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order */ function receiveDrip( - uint256 _normalizedTokenSupply, + uint256 _issuanceBase, uint256 _issuanceRate, uint256 _nonce ) external; diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index a4c9a5937..33d4c965f 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -23,7 +23,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // Emitted when the initial supply snapshot is taken after contract deployment event InitialSnapshotTaken( uint256 _blockNumber, - uint256 _tokenSupplyCache, + uint256 _issuanceBase, uint256 _mintedPendingRewards ); // Emitted when an issuance rate update is staged, to be applied on the next drip @@ -102,7 +102,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Computes the initial snapshot for token supply and mints any pending rewards - * This will initialize the tokenSupplyCache to the current GRT supply, after which + * This will initialize the issuanceBase to the current GRT supply, after which * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, * to be computed off-chain. @@ -112,8 +112,8 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); grt.mint(address(this), pendingRewards); - tokenSupplyCache = grt.totalSupply(); - emit InitialSnapshotTaken(block.number, tokenSupplyCache, pendingRewards); + issuanceBase = grt.totalSupply(); + emit InitialSnapshotTaken(block.number, issuanceBase, pendingRewards); } /** @@ -224,11 +224,11 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. - * We also update the tokenSupplyCache by adding the new total rewards on both layers. + * We also update the issuanceBase by adding the new total rewards on both layers. * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ function snapshotAccumulatedRewards(uint256 globalDelta) internal { - tokenSupplyCache = tokenSupplyCache + globalDelta; + issuanceBase = issuanceBase + globalDelta; // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, // to save gas accumulatedLayerRewards = @@ -252,7 +252,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 gasPriceBid, uint256 maxSubmissionCost ) internal { - uint256 normalizedSupply = l2RewardsFraction.mul(tokenSupplyCache).div(TOKEN_DECIMALS); + uint256 normalizedSupply = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, normalizedSupply, @@ -288,10 +288,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { return 0; } - deltaRewards = tokenSupplyCache + deltaRewards = issuanceBase .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) - .sub(tokenSupplyCache); + .sub(issuanceBase); } /** diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index f8254d59f..90821c809 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -14,8 +14,6 @@ contract L1ReservoirV1Storage { address public l2ReservoirAddress; // Block until the minted supplies should last before another drip is needed uint256 public rewardsMintedUntilBlock; - // Snapshot of initial token supply plus accumulated global rewards - uint256 public tokenSupplyCache; // New issuance rate to be applied on the next drip uint256 public nextIssuanceRate; // Interval for rewards drip, in blocks diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index 18f1cf9d4..8e3e2591d 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -15,4 +15,6 @@ contract ReservoirV1Storage is Managed { uint256 public accumulatedLayerRewards; // Last block at which rewards when updated, i.e. block at which the last drip happened or was received uint256 public lastRewardsUpdateBlock; + // Base value for token issuance, set initially to GRT supply and afterwards using accumulated rewards to update + uint256 public issuanceBase; } diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 0dd54f23c..ebfe3f52e 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -172,7 +172,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -195,7 +195,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) }) @@ -208,7 +208,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -219,9 +219,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate.add(1)) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) @@ -235,7 +233,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -246,9 +244,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) @@ -262,7 +258,7 @@ describe('L2Reservoir', () => { ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) @@ -274,9 +270,7 @@ describe('L2Reservoir', () => { ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq( - dripNormalizedSupply.add(1), - ) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply.add(1)) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply.add(1)) await expect(await grt.balanceOf(l2Reservoir.address)).to.eq(dripAmount.mul(2)) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 7a4920159..07a55aa61 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -121,7 +121,7 @@ describe('L1Reservoir', () => { const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) await expect(tx1) .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) @@ -136,7 +136,7 @@ describe('L1Reservoir', () => { const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) expectedMintedAmount = (await tracker.accRewards(expectedNextDeadline)).sub(actualAmount) expect(toRound(newAmount)).to.eq(toRound(expectedMintedAmount)) - expect(toRound(await l1Reservoir.tokenSupplyCache())).to.eq(toRound(expectedSnapshottedSupply)) + expect(toRound(await l1Reservoir.issuanceBase())).to.eq(toRound(expectedSnapshottedSupply)) await expect(tx2) .emit(l1Reservoir, 'RewardsDripped') .withArgs(newAmount, toBN(0), expectedNextDeadline) @@ -189,7 +189,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), supply, toGRT('0')) expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supply) + expect(await l1Reservoir.issuanceBase()).to.eq(supply) expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) it('mints pending rewards and includes them in the snapshot', async function () { @@ -201,7 +201,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), expectedSupply, pending) expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(expectedSupply) + expect(await l1Reservoir.issuanceBase()).to.eq(expectedSupply) expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) }) @@ -324,7 +324,7 @@ describe('L1Reservoir', () => { const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) - expect(await l1Reservoir.tokenSupplyCache()).to.eq(supplyBeforeDrip) + expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) await expect(tx) .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) @@ -413,12 +413,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -463,12 +463,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -510,11 +510,11 @@ describe('L1Reservoir', () => { toRound(expectedNewMintedAmount), ) expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce ]) @@ -566,12 +566,12 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - let normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), ]) @@ -610,11 +610,11 @@ describe('L1Reservoir', () => { toRound(expectedNewMintedAmount), ) expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) - normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce ]) @@ -714,7 +714,7 @@ describe('L1Reservoir', () => { const lambda = toGRT('0.32') await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) - supplyBeforeDrip = await l1Reservoir.tokenSupplyCache() // Has been updated accordingly + supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) const t0 = dripBlock From 4ad867a846c01415f9217bc4086731d3a9aed27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 11 Jul 2022 21:47:05 +0200 Subject: [PATCH 05/47] fix: use issuanceBase check to prevent calling initialSnapshot twice [L-04] --- contracts/reservoir/L1Reservoir.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 33d4c965f..1990d4abf 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -105,10 +105,11 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This will initialize the issuanceBase to the current GRT supply, after which * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, - * to be computed off-chain. + * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function */ function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + require(issuanceBase == 0, "Cannot call this function more than once"); lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); grt.mint(address(this), pendingRewards); From a8144dff2cb4c2188f93ab2cd8797abe83ad4057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 14:14:47 +0200 Subject: [PATCH 06/47] test: fix tests after not allowing initialSnapshot to be called twice --- test/lib/fixtures.ts | 1 - test/reservoir/l1Reservoir.test.ts | 55 ++++++++----- test/rewards/rewards.test.ts | 124 +++++++++++++++++------------ 3 files changed, 106 insertions(+), 74 deletions(-) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 3cd9d9b57..255d8586a 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -177,7 +177,6 @@ export class NetworkFixture { await grt.connect(deployer).addMinter(l1Reservoir.address) await l1Reservoir.connect(deployer).setIssuanceRate(deployment.defaults.rewards.issuanceRate) await l1Reservoir.connect(deployer).approveRewardsManager() - await l1Reservoir.connect(deployer).initialSnapshot(toBN(0)) } // Unpause the protocol diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 07a55aa61..afc6233ca 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai' import { BigNumber, constants, utils } from 'ethers' -import { defaults, deployContract } from '../lib/deployment' +import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' import { GraphToken } from '../../build/types/GraphToken' @@ -26,6 +26,9 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' import { Interface } from 'ethers/lib/utils' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' +import { SubgraphDeploymentID } from '@graphprotocol/common-ts' +import { Controller } from '../../build/types/Controller' +import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi @@ -53,6 +56,8 @@ describe('L1Reservoir', () => { let l1Reservoir: L1Reservoir let bridgeEscrow: BridgeEscrow let l1GraphTokenGateway: L1GraphTokenGateway + let controller: Controller + let proxyAdmin: GraphProxyAdmin let supplyBeforeDrip: BigNumber let dripBlock: BigNumber @@ -103,8 +108,6 @@ describe('L1Reservoir', () => { blocksToAdvance: BigNumber, dripInterval = defaults.rewards.dripInterval, ) => { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) const supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -149,8 +152,10 @@ describe('L1Reservoir', () => { fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) - ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway } = fixtureContracts) + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin } = + fixtureContracts) + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) arbitrumMocks = await fixture.loadArbitrumL1Mocks(governor.signer) await fixture.configureL1Bridge( governor.signer, @@ -177,32 +182,45 @@ describe('L1Reservoir', () => { describe('configuration', function () { describe('initial snapshot', function () { + let reservoir: L1Reservoir + beforeEach(async function () { + // Deploy a new reservoir to avoid issues with initialSnapshot being called twice + reservoir = await deployL1Reservoir(governor.signer, controller.address, proxyAdmin) + await grt.connect(governor.signer).addMinter(reservoir.address) + }) + it('rejects call if unauthorized', async function () { - const tx = l1Reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) + const tx = reservoir.connect(testAccount1.signer).initialSnapshot(toGRT('1.025')) await expect(tx).revertedWith('Caller must be Controller governor') }) it('snapshots the total GRT supply', async function () { - const tx = l1Reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + const tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) const supply = await grt.totalSupply() await expect(tx) - .emit(l1Reservoir, 'InitialSnapshotTaken') + .emit(reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), supply, toGRT('0')) - expect(await grt.balanceOf(l1Reservoir.address)).to.eq(toGRT('0')) - expect(await l1Reservoir.issuanceBase()).to.eq(supply) - expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + expect(await grt.balanceOf(reservoir.address)).to.eq(toGRT('0')) + expect(await reservoir.issuanceBase()).to.eq(supply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) }) it('mints pending rewards and includes them in the snapshot', async function () { const pending = toGRT('10000000') - const tx = l1Reservoir.connect(governor.signer).initialSnapshot(pending) + const tx = reservoir.connect(governor.signer).initialSnapshot(pending) const supply = await grt.totalSupply() const expectedSupply = supply.add(pending) await expect(tx) - .emit(l1Reservoir, 'InitialSnapshotTaken') + .emit(reservoir, 'InitialSnapshotTaken') .withArgs(await latestBlock(), expectedSupply, pending) - expect(await grt.balanceOf(l1Reservoir.address)).to.eq(pending) - expect(await l1Reservoir.issuanceBase()).to.eq(expectedSupply) - expect(await l1Reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + expect(await grt.balanceOf(reservoir.address)).to.eq(pending) + expect(await reservoir.issuanceBase()).to.eq(expectedSupply) + expect(await reservoir.lastRewardsUpdateBlock()).to.eq(await latestBlock()) + }) + it('cannot be called more than once', async function () { + let tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).emit(reservoir, 'InitialSnapshotTaken') + tx = reservoir.connect(governor.signer).initialSnapshot(toGRT('0')) + await expect(tx).revertedWith('Cannot call this function more than once') }) }) describe('issuance rate update', function () { @@ -307,8 +325,6 @@ describe('L1Reservoir', () => { // issuanceRate or l2RewardsFraction is updated describe('drip', function () { it('mints rewards for the next week', async function () { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -330,8 +346,6 @@ describe('L1Reservoir', () => { .withArgs(actualAmount, toBN(0), expectedNextDeadline) }) it('has no effect if called a second time in the same block', async function () { - // Initial snapshot defines the first lastRewardsUpdateBlock - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -385,7 +399,6 @@ describe('L1Reservoir', () => { }) it('sends the specified fraction of the rewards with a callhook to L2', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -435,7 +448,6 @@ describe('L1Reservoir', () => { }) it('sends the outstanding amount if the L2 rewards fraction changes', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -538,7 +550,6 @@ describe('L1Reservoir', () => { }) it('sends the outstanding amount if the L2 rewards fraction stays constant', async function () { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 53958ad70..e2d141d95 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -9,6 +9,8 @@ import { GraphToken } from '../../build/types/GraphToken' import { RewardsManager } from '../../build/types/RewardsManager' import { Staking } from '../../build/types/Staking' +import { BigNumber as BN } from 'bignumber.js' + import { advanceBlocks, deriveChannelKey, @@ -112,10 +114,6 @@ describe('Rewards', () => { governor.signer, )) - // 5% minute rate (4 blocks) - // await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - // await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { await grt.connect(governor.signer).mint(wallet.address, toGRT('1000000')) @@ -273,6 +271,31 @@ describe('Rewards', () => { ) } + function calculatedExpectedRewards( + firstSnapshotBlocks: BN, + lastSnapshotBlocks: BN, + allocatedTokens: BN, + ): BigNumber { + const issuanceBase = new BN(10004000000) + const issuanceRate = new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18) + // All the rewards in this subgraph go to this allocation. + // Rewards per token will be (issuanceBase * issuanceRate^nBlocks - issuanceBase) / allocatedTokens + // The first snapshot is after allocating, that is lastSnapshotBlocks blocks after dripBlock: + const startRewardsPerToken = issuanceBase + .times(issuanceRate.pow(firstSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // The final snapshot is when we close the allocation, that happens 8 blocks later: + const endRewardsPerToken = issuanceBase + .times(issuanceRate.pow(lastSnapshotBlocks)) + .minus(issuanceBase) + .div(allocatedTokens) + // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * allocatedTokens. + return toGRT( + endRewardsPerToken.minus(startRewardsPerToken).times(allocatedTokens).toPrecision(18), + ) + } + beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) @@ -595,17 +618,15 @@ describe('Rewards', () => { describe('takeAndBurnRewards', function () { it('should burn rewards on closed allocation with POI zero', async function () { // Align with the epoch boundary - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup await setupIndexerAllocation() - // dripBlock + 7 + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -613,19 +634,18 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation with POI zero, which should burn the rewards const tx = await staking.connect(indexer1.signer).closeAllocation(allocationID1, HashZero) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + const log = findRewardsManagerEvents(receipt)[0] const event = log.args expect(log.name).eq('RewardsBurned') @@ -667,6 +687,7 @@ describe('Rewards', () => { // dripBlock + 4 // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // dripBlock + 7 // Jump await advanceToNextEpoch(epochManager) @@ -678,21 +699,19 @@ describe('Rewards', () => { const beforeIndexer1Balance = await grt.balanceOf(indexer1.address) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) + const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) @@ -782,6 +801,7 @@ describe('Rewards', () => { await advanceToNextEpoch(epochManager) // Setup await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) @@ -792,24 +812,22 @@ describe('Rewards', () => { const beforeDestinationBalance = await grt.balanceOf(destinationAddress) const beforeStakingBalance = await grt.balanceOf(staking.address) - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 7 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.0001227 ^ 7 - 10004000000) / 12500 = 687.77 - // The final snapshot is when we close the allocation, that happens 8 blocks later: - // endRewardsPerToken = (10004000000 * 1.0001227 ^ 15 - 10004000000) / 12500 = 1474.52 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('9834378') - // Close allocation. At this point rewards should be collected for that indexer const tx = await staking .connect(indexer1.signer) .closeAllocation(allocationID1, randomHexBytes()) const receipt = await tx.wait() + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) const event = findRewardsManagerEvents(receipt)[0].args expect(event.indexer).eq(indexer1.address) expect(event.allocationID).eq(allocationID1) expect(event.epoch).eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) expect(toRound(event.amount)).eq(toRound(expectedIndexingRewards)) // After state @@ -841,15 +859,15 @@ describe('Rewards', () => { cooldownBlocks: 5, } const tokensToDelegate = toGRT('2000') - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 + // Align with the epoch boundary await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup the allocation and delegators await setupIndexerAllocationWithDelegation(tokensToDelegate, delegationParams) - // dripBlock + 11 + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) + // Jump await advanceToNextEpoch(epochManager) // dripBlock + 14 @@ -861,6 +879,7 @@ describe('Rewards', () => { // Close allocation. At this point rewards should be collected for that indexer await staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // After state const afterTokenSupply = await grt.totalSupply() @@ -870,14 +889,12 @@ describe('Rewards', () => { // Check that rewards are put into indexer stake (only indexer cut) // Check that rewards are put into delegators pool accordingly - // All the rewards in this subgraph go to this allocation. - // Rewards per token will be (totalSupply * issuanceRate^nBlocks - totalSupply) / allocatedTokens - // The first snapshot is after allocating, that is 11 blocks after dripBlock: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 11 - 10004000000) / 14500 = 931.94 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 15 - 10004000000) / 14500 = 1271.14 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('4918396') + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(14500), + ) + // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -902,17 +919,16 @@ describe('Rewards', () => { await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) await advanceToNextEpoch(epochManager) await setupIndexerAllocation() + const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) - // This is the same amount as in the test above - // (see 'should distribute rewards on closed allocation and stake') - const expectedIndexingRewards = toGRT('9834378') const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer const tx = staking.connect(indexer1.signer).closeAllocation(allocationID1, randomHexBytes()) await expect(tx).emit(rewardsManager, 'RewardsDenied') + const lastSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) const receipt = await (await tx).wait() const logs = findRewardsManagerEvents(receipt) expect(logs.length).to.eq(1) @@ -921,6 +937,12 @@ describe('Rewards', () => { expect(ev.indexer).to.eq(indexer1.address) expect(ev.allocationID).to.eq(allocationID1) expect(ev.epoch).to.eq(await epochManager.currentEpoch()) + + const expectedIndexingRewards = calculatedExpectedRewards( + firstSnapshotBlocks, + lastSnapshotBlocks, + new BN(12500), + ) expect(toRound(ev.amount)).to.eq(toRound(expectedIndexingRewards)) // Check that the rewards were burned // We divide by 10 to accept numeric errors up to 10 GRT From 5c6802fee4dbdf0944c07e88b007336958de9933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 12:33:19 +0200 Subject: [PATCH 07/47] fix: document the need for drip after a param update [L-06] --- contracts/reservoir/L1Reservoir.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 1990d4abf..7a46e12dc 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -70,6 +70,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This means that it needs to be greater than 1.0, any number under 1.0 is not * allowed and an issuance rate of 1.0 means no issuance. * To accommodate a high precision the issuance rate is expressed in wei, i.e. fixed point at 1e18. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. * @param _issuanceRate Issuance rate expressed in wei / fixed point at 1e18 */ function setIssuanceRate(uint256 _issuanceRate) external onlyGovernor { @@ -82,6 +85,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Sets the L2 rewards fraction. * This is the portion of the indexer rewards that are sent to L2. * The value is in fixed point at 1e18 and must be less than 1. + * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, + * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure + * good synchronization between L1 and L2. * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 */ function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { From b6e149722de21df852938dca319771e24563e7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 12 Jul 2022 18:55:20 +0200 Subject: [PATCH 08/47] fix: validate L2Reservoir address on L1Reservoir [L-07] --- contracts/reservoir/L1Reservoir.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 7a46e12dc..35127f367 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -102,6 +102,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2ReservoirAddress New address for the L2Reservoir on L2 */ function setL2ReservoirAddress(address _l2ReservoirAddress) external onlyGovernor { + require(_l2ReservoirAddress != address(0), "INVALID_L2_RESERVOIR"); l2ReservoirAddress = _l2ReservoirAddress; emit L2ReservoirAddressUpdated(_l2ReservoirAddress); } From ad09ca007a7cc13b37c68124db5dc6a8653171e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 12:54:52 +0200 Subject: [PATCH 09/47] fix: rename normalizedSupply to l2IssuanceBase in L2 message to avoid confusion [L-10] --- contracts/reservoir/L1Reservoir.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 35127f367..98b4f7d4e 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -260,10 +260,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 gasPriceBid, uint256 maxSubmissionCost ) internal { - uint256 normalizedSupply = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); + uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, - normalizedSupply, + l2IssuanceBase, issuanceRate, nextDripNonce ); From 8cd3270643b33dfc38f51b1d1b7c03efc9b6263c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 13:21:35 +0200 Subject: [PATCH 10/47] fix: remove silent failure if rewardsManager is not set [L-12] --- contracts/staking/Staking.sol | 9 --------- 1 file changed, 9 deletions(-) diff --git a/contracts/staking/Staking.sol b/contracts/staking/Staking.sol index 672052ed8..37c972a53 100644 --- a/contracts/staking/Staking.sol +++ b/contracts/staking/Staking.sol @@ -1585,9 +1585,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _updateRewards(bytes32 _subgraphDeploymentID) private returns (uint256) { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return 0; - } return rewardsManager.onSubgraphAllocationUpdate(_subgraphDeploymentID); } @@ -1597,9 +1594,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _distributeRewards(address _allocationID, address _indexer) private { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return; - } // Automatically triggers update of rewards snapshot as allocation will change // after this call. Take rewards transfers tokens for the Staking contract to distribute @@ -1628,9 +1622,6 @@ contract Staking is StakingV2Storage, GraphUpgradeable, IStaking, Multicall { */ function _takeAndBurnRewards(address _allocationID) private { IRewardsManager rewardsManager = rewardsManager(); - if (address(rewardsManager) == address(0)) { - return; - } // Automatically triggers update of rewards snapshot as allocation will change // after this call. From 19dd31061d7a19fdc48e5720b8cf443ab0c9cd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 14:57:55 +0200 Subject: [PATCH 11/47] fix: general code improvements [N-05] --- contracts/l2/reservoir/L2Reservoir.sol | 8 +- contracts/reservoir/IReservoir.sol | 10 +-- contracts/reservoir/L1Reservoir.sol | 104 ++++++++++++------------- contracts/reservoir/Reservoir.sol | 60 +++++++------- contracts/rewards/RewardsManager.sol | 3 +- 5 files changed, 92 insertions(+), 93 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8948f9ee1..5619b7abd 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -54,21 +54,21 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * - p is the issuance base at t0 (normalized by the L2 rewards fraction) * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on L2 since the last drip */ - function getNewRewards(uint256 blocknum) + function getNewRewards(uint256 _blocknum) public view override(Reservoir, IReservoir) returns (uint256 deltaRewards) { uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } deltaRewards = issuanceBase - .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) .sub(issuanceBase); } diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 996f3fc2f..0bc9fba21 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -10,7 +10,7 @@ pragma solidity ^0.7.6; */ interface IReservoir { // Emitted when the issuance rate is updated - event IssuanceRateUpdated(uint256 _newValue); + event IssuanceRateUpdated(uint256 newValue); /** * @dev Approve the RewardsManager to manage the reservoir's token funds @@ -19,17 +19,17 @@ interface IReservoir { /** * @dev Get accumulated total rewards on this layer at a particular block - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return totalRewards Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 blocknum) external view returns (uint256 totalRewards); + function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256 totalRewards); /** * @dev Get new total rewards on this layer at a particular block, since the last drip event - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on this layer since the last drip */ - function getNewRewards(uint256 blocknum) external view returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) external view returns (uint256 deltaRewards); } /** diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 98b4f7d4e..02f1973e8 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -22,22 +22,22 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // Emitted when the initial supply snapshot is taken after contract deployment event InitialSnapshotTaken( - uint256 _blockNumber, - uint256 _issuanceBase, - uint256 _mintedPendingRewards + uint256 blockNumber, + uint256 issuanceBase, + uint256 mintedPendingRewards ); // Emitted when an issuance rate update is staged, to be applied on the next drip - event IssuanceRateStaged(uint256 _newValue); + event IssuanceRateStaged(uint256 newValue); // Emitted when an L2 rewards fraction update is staged, to be applied on the next drip - event L2RewardsFractionStaged(uint256 _newValue); + event L2RewardsFractionStaged(uint256 newValue); // Emitted when the L2 rewards fraction is updated (during a drip) - event L2RewardsFractionUpdated(uint256 _newValue); + event L2RewardsFractionUpdated(uint256 newValue); // Emitted when the drip interval is updated - event DripIntervalUpdated(uint256 _newValue); + event DripIntervalUpdated(uint256 newValue); // Emitted when new rewards are dripped and potentially sent to L2 - event RewardsDripped(uint256 _totalMinted, uint256 _sentToL2, uint256 _nextDeadline); + event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); // Emitted when the address for the L2Reservoir is updated - event L2ReservoirAddressUpdated(address _l2ReservoirAddress); + event L2ReservoirAddressUpdated(address l2ReservoirAddress); /** * @dev Initialize this contract. @@ -113,15 +113,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * we will keep an internal accounting only using newly minted rewards. This function * will also mint any pending rewards to cover up to the current block for open allocations, * to be computed off-chain. Can only be called once as it checks that the issuanceBase is zero. - * @param pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function + * @param _pendingRewards Pending rewards up to the current block for open allocations, to be minted by this function */ - function initialSnapshot(uint256 pendingRewards) external onlyGovernor { + function initialSnapshot(uint256 _pendingRewards) external onlyGovernor { require(issuanceBase == 0, "Cannot call this function more than once"); lastRewardsUpdateBlock = block.number; IGraphToken grt = graphToken(); - grt.mint(address(this), pendingRewards); + grt.mint(address(this), _pendingRewards); issuanceBase = grt.totalSupply(); - emit InitialSnapshotTaken(block.number, issuanceBase, pendingRewards); + emit InitialSnapshotTaken(block.number, issuanceBase, _pendingRewards); } /** @@ -138,14 +138,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * until the drip amount becomes positive before calling the function again. It can also revert * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we * should send now. - * @param l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 */ function drip( - uint256 l2MaxGas, - uint256 l2GasPriceBid, - uint256 l2MaxSubmissionCost + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost ) external payable notPaused { uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); @@ -209,17 +209,17 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit L2RewardsFractionUpdated(l2RewardsFraction); _sendNewTokensAndStateToL2( tokensToSendToL2, - l2MaxGas, - l2GasPriceBid, - l2MaxSubmissionCost + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost ); } else if (l2RewardsFraction > 0) { tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); _sendNewTokensAndStateToL2( tokensToSendToL2, - l2MaxGas, - l2GasPriceBid, - l2MaxSubmissionCost + _l2MaxGas, + _l2GasPriceBid, + _l2MaxSubmissionCost ); } else { // Avoid locking funds in this contract if we don't need to @@ -233,15 +233,15 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. * We also update the issuanceBase by adding the new total rewards on both layers. - * @param globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block + * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ - function snapshotAccumulatedRewards(uint256 globalDelta) internal { - issuanceBase = issuanceBase + globalDelta; - // Reimplementation of getAccumulatedRewards but reusing the globalDelta calculated above, + function snapshotAccumulatedRewards(uint256 _globalDelta) internal { + issuanceBase = issuanceBase + _globalDelta; + // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, // to save gas accumulatedLayerRewards = accumulatedLayerRewards + - globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + _globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); lastRewardsUpdateBlock = block.number; } @@ -249,16 +249,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @dev Send new tokens and a message with state to L2 * This function will use the L1GraphTokenGateway to send tokens * to L2, and will also encode a callhook to update state on the L2Reservoir. - * @param nTokens Number of tokens to send to L2 - * @param maxGas Max gas for the L2 retryable ticket execution - * @param gasPriceBid Gas price for the L2 retryable ticket execution - * @param maxSubmissionCost Max submission price for the L2 retryable ticket + * @param _nTokens Number of tokens to send to L2 + * @param _maxGas Max gas for the L2 retryable ticket execution + * @param _gasPriceBid Gas price for the L2 retryable ticket execution + * @param _maxSubmissionCost Max submission price for the L2 retryable ticket */ function _sendNewTokensAndStateToL2( - uint256 nTokens, - uint256 maxGas, - uint256 gasPriceBid, - uint256 maxSubmissionCost + uint256 _nTokens, + uint256 _maxGas, + uint256 _gasPriceBid, + uint256 _maxSubmissionCost ) internal { uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); bytes memory extraData = abi.encodeWithSelector( @@ -268,16 +268,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { nextDripNonce ); nextDripNonce = nextDripNonce.add(1); - bytes memory data = abi.encode(maxSubmissionCost, extraData); + bytes memory data = abi.encode(_maxSubmissionCost, extraData); IGraphToken grt = graphToken(); ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); - grt.approve(address(gateway), nTokens); + grt.approve(address(gateway), _nTokens); gateway.outboundTransfer{ value: msg.value }( address(grt), l2ReservoirAddress, - nTokens, - maxGas, - gasPriceBid, + _nTokens, + _maxGas, + _gasPriceBid, data ); } @@ -288,16 +288,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - p is the total token supply snapshot at t0 * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on both layers since the last drip */ - function getNewGlobalRewards(uint256 blocknum) public view returns (uint256 deltaRewards) { + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || blocknum == t0) { + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } deltaRewards = issuanceBase - .mul(_pow(issuanceRate, blocknum.sub(t0), TOKEN_DECIMALS)) + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) .div(TOKEN_DECIMALS) .sub(issuanceBase); } @@ -307,12 +307,12 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * This is deltaR_L1 = (1-lambda) * deltaR, where: * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on Layer 1 since the last drip */ - function getNewRewards(uint256 blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( - TOKEN_DECIMALS - ); + function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(_blocknum) + .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) + .div(TOKEN_DECIMALS); } } diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index ba19de015..84576f6d2 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -32,26 +32,26 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir /** * @dev Get accumulated total rewards on this layer at a particular block - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return totalRewards Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 blocknum) + function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256 totalRewards) { // R(t) = R(t0) + (DeltaR(t, t0)) - totalRewards = accumulatedLayerRewards + getNewRewards(blocknum); + totalRewards = accumulatedLayerRewards + getNewRewards(_blocknum); } /** * @dev Get new total rewards on this layer at a particular block, since the last drip event. * Must be implemented by the reservoir on each layer. - * @param blocknum Block number at which to calculate rewards + * @param _blocknum Block number at which to calculate rewards * @return deltaRewards New total rewards on this layer since the last drip */ - function getNewRewards(uint256 blocknum) + function getNewRewards(uint256 _blocknum) public view virtual @@ -59,63 +59,63 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir returns (uint256 deltaRewards); /** - * @dev Raises x to the power of n with scaling factor of base. + * @dev Raises _x to the power of _n with scaling factor of _base. * Based on: https://github.com/makerdao/dss/blob/master/src/pot.sol#L81 - * @param x Base of the exponentiation - * @param n Exponent - * @param base Scaling factor - * @return z Exponential of n with base x + * @param _x Base of the exponentiation + * @param _n Exponent + * @param _base Scaling factor + * @return z Exponential of _n with base _x */ function _pow( - uint256 x, - uint256 n, - uint256 base + uint256 _x, + uint256 _n, + uint256 _base ) internal pure returns (uint256 z) { // solhint-disable-next-line no-inline-assembly assembly { - switch x + switch _x case 0 { - switch n + switch _n case 0 { - z := base + z := _base } default { z := 0 } } default { - switch mod(n, 2) + switch mod(_n, 2) case 0 { - z := base + z := _base } default { - z := x + z := _x } - let half := div(base, 2) // for rounding. + let half := div(_base, 2) // for rounding. for { - n := div(n, 2) - } n { - n := div(n, 2) + _n := div(_n, 2) + } _n { + _n := div(_n, 2) } { - let xx := mul(x, x) - if iszero(eq(div(xx, x), x)) { + let xx := mul(_x, _x) + if iszero(eq(div(xx, _x), _x)) { revert(0, 0) } let xxRound := add(xx, half) if lt(xxRound, xx) { revert(0, 0) } - x := div(xxRound, base) - if mod(n, 2) { - let zx := mul(z, x) - if and(iszero(iszero(x)), iszero(eq(div(zx, x), z))) { + _x := div(xxRound, _base) + if mod(_n, 2) { + let zx := mul(z, _x) + if and(iszero(iszero(_x)), iszero(eq(div(zx, _x), z))) { revert(0, 0) } let zxRound := add(zx, half) if lt(zxRound, zx) { revert(0, 0) } - z := div(zxRound, base) + z := div(zxRound, _base) } } } diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 0711b531d..dea988448 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -399,6 +399,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa graphToken().transferFrom(address(reservoir()), address(staking), rewards); } emit RewardsAssigned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); + return rewards; } else { if (rewards > 0) { graphToken().burnFrom(address(reservoir()), rewards); @@ -406,8 +407,6 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa emit RewardsDenied(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); return 0; } - - return rewards; } /** From 6a50012685b957a6ab0f23206055f1005869ac09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 15:39:35 +0200 Subject: [PATCH 12/47] fix: use internal function to consistently set dripInterval [N-07] --- contracts/reservoir/L1Reservoir.sol | 88 ++++++++++++++++------------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 02f1973e8..8115212e7 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -47,7 +47,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { */ function initialize(address _controller, uint256 _dripInterval) external onlyImpl { Managed._initialize(_controller); - dripInterval = _dripInterval; + _setDripInterval(_dripInterval); } /** @@ -59,9 +59,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _dripInterval The new interval in blocks for which drip() will mint rewards */ function setDripInterval(uint256 _dripInterval) external onlyGovernor { - require(_dripInterval > 0, "Drip interval must be > 0"); - dripInterval = _dripInterval; - emit DripIntervalUpdated(_dripInterval); + _setDripInterval(_dripInterval); } /** @@ -229,6 +227,54 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); } + /** + * @dev Get new total rewards on both layers at a particular block, since the last drip event + * This is deltaR = p * r ^ (blocknum - t0) - p, where: + * - p is the total token supply snapshot at t0 + * - t0 is the last drip block, i.e. lastRewardsUpdateBlock + * - r is the issuanceRate + * @param _blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on both layers since the last drip + */ + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { + uint256 t0 = lastRewardsUpdateBlock; + if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { + return 0; + } + deltaRewards = issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); + } + + /** + * @dev Get new total rewards on this layer at a particular block, since the last drip event + * This is deltaR_L1 = (1-lambda) * deltaR, where: + * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) + * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction + * @param _blocknum Block number at which to calculate rewards + * @return deltaRewards New total rewards on Layer 1 since the last drip + */ + function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { + deltaRewards = getNewGlobalRewards(_blocknum) + .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) + .div(TOKEN_DECIMALS); + } + + /** + * @dev Sets the drip interval (internal). + * This is the time in the future (in blocks) for which drip() will mint rewards. + * Keep in mind that changing this value will require manually re-adjusting + * the reservoir's token balance, because the first call to drip might produce + * more or less tokens than needed. + * @param _dripInterval The new interval in blocks for which drip() will mint rewards + */ + function _setDripInterval(uint256 _dripInterval) internal { + require(_dripInterval > 0, "Drip interval must be > 0"); + dripInterval = _dripInterval; + emit DripIntervalUpdated(_dripInterval); + } + /** * @dev Snapshot accumulated rewards on this layer * We compute accumulatedLayerRewards and mark this block as the lastRewardsUpdateBlock. @@ -281,38 +327,4 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { data ); } - - /** - * @dev Get new total rewards on both layers at a particular block, since the last drip event - * This is deltaR = p * r ^ (blocknum - t0) - p, where: - * - p is the total token supply snapshot at t0 - * - t0 is the last drip block, i.e. lastRewardsUpdateBlock - * - r is the issuanceRate - * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on both layers since the last drip - */ - function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { - uint256 t0 = lastRewardsUpdateBlock; - if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { - return 0; - } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); - } - - /** - * @dev Get new total rewards on this layer at a particular block, since the last drip event - * This is deltaR_L1 = (1-lambda) * deltaR, where: - * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) - * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction - * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on Layer 1 since the last drip - */ - function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(_blocknum) - .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) - .div(TOKEN_DECIMALS); - } } From 965dbb3dd274fc349274169c3577bb2e0ec200f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 16:01:58 +0200 Subject: [PATCH 13/47] fix: remove named returns [N-08] --- contracts/l2/reservoir/L2Reservoir.sol | 13 +++++++------ contracts/reservoir/IReservoir.sol | 8 ++++---- contracts/reservoir/L1Reservoir.sol | 24 +++++++++++++----------- contracts/reservoir/Reservoir.sol | 26 +++++++++----------------- 4 files changed, 33 insertions(+), 38 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 5619b7abd..2a0673ba2 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -55,22 +55,23 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on L2 since the last drip + * @return New total rewards on L2 since the last drip */ function getNewRewards(uint256 _blocknum) public view override(Reservoir, IReservoir) - returns (uint256 deltaRewards) + returns (uint256) { uint256 t0 = lastRewardsUpdateBlock; if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); } /** diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 0bc9fba21..de25a2b80 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -20,16 +20,16 @@ interface IReservoir { /** * @dev Get accumulated total rewards on this layer at a particular block * @param _blocknum Block number at which to calculate rewards - * @return totalRewards Accumulated total rewards on this layer + * @return Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256 totalRewards); + function getAccumulatedRewards(uint256 _blocknum) external view returns (uint256); /** * @dev Get new total rewards on this layer at a particular block, since the last drip event * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on this layer since the last drip + * @return New total rewards on this layer since the last drip */ - function getNewRewards(uint256 _blocknum) external view returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) external view returns (uint256); } /** diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 8115212e7..b6a821dff 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -234,17 +234,18 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - t0 is the last drip block, i.e. lastRewardsUpdateBlock * - r is the issuanceRate * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on both layers since the last drip + * @return New total rewards on both layers since the last drip */ - function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256 deltaRewards) { + function getNewGlobalRewards(uint256 _blocknum) public view returns (uint256) { uint256 t0 = lastRewardsUpdateBlock; if (issuanceRate <= MIN_ISSUANCE_RATE || _blocknum == t0) { return 0; } - deltaRewards = issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) - .sub(issuanceBase); + return + issuanceBase + .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) + .div(TOKEN_DECIMALS) + .sub(issuanceBase); } /** @@ -253,12 +254,13 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * - deltaR is the new global rewards on both layers (see getNewGlobalRewards) * - lambda is the fraction of rewards sent to L2, i.e. l2RewardsFraction * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on Layer 1 since the last drip + * @return New total rewards on Layer 1 since the last drip */ - function getNewRewards(uint256 _blocknum) public view override returns (uint256 deltaRewards) { - deltaRewards = getNewGlobalRewards(_blocknum) - .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) - .div(TOKEN_DECIMALS); + function getNewRewards(uint256 _blocknum) public view override returns (uint256) { + return + getNewGlobalRewards(_blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( + TOKEN_DECIMALS + ); } /** diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 84576f6d2..048697bc9 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -33,30 +33,20 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir /** * @dev Get accumulated total rewards on this layer at a particular block * @param _blocknum Block number at which to calculate rewards - * @return totalRewards Accumulated total rewards on this layer + * @return Accumulated total rewards on this layer */ - function getAccumulatedRewards(uint256 _blocknum) - public - view - override - returns (uint256 totalRewards) - { + function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { // R(t) = R(t0) + (DeltaR(t, t0)) - totalRewards = accumulatedLayerRewards + getNewRewards(_blocknum); + return accumulatedLayerRewards + getNewRewards(_blocknum); } /** * @dev Get new total rewards on this layer at a particular block, since the last drip event. * Must be implemented by the reservoir on each layer. * @param _blocknum Block number at which to calculate rewards - * @return deltaRewards New total rewards on this layer since the last drip + * @return New total rewards on this layer since the last drip */ - function getNewRewards(uint256 _blocknum) - public - view - virtual - override - returns (uint256 deltaRewards); + function getNewRewards(uint256 _blocknum) public view virtual override returns (uint256); /** * @dev Raises _x to the power of _n with scaling factor of _base. @@ -64,13 +54,14 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir * @param _x Base of the exponentiation * @param _n Exponent * @param _base Scaling factor - * @return z Exponential of _n with base _x + * @return Exponential of _n with base _x */ function _pow( uint256 _x, uint256 _n, uint256 _base - ) internal pure returns (uint256 z) { + ) internal pure returns (uint256) { + uint256 z; // solhint-disable-next-line no-inline-assembly assembly { switch _x @@ -120,5 +111,6 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir } } } + return z; } } From f4bc6f98f46e6fd495eeb00076a766f29702db80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 16:13:51 +0200 Subject: [PATCH 14/47] fix: update some outdated docstrings and comments [N-09] --- contracts/reservoir/L1Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b6a821dff..c3a992b99 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -82,7 +82,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Sets the L2 rewards fraction. * This is the portion of the indexer rewards that are sent to L2. - * The value is in fixed point at 1e18 and must be less than 1. + * The value is in fixed point at 1e18 and must be less than or equal to 1. * Note: It is strongly recommended that the governor triggers a drip immediately after calling this, * including excess gas to guarantee that the L2 retryable ticket succeeds immediately, to ensure * good synchronization between L1 and L2. diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index dea988448..1b07c5559 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -277,6 +277,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa /** * @dev Updates the accumulated rewards per signal and save checkpoint block number. + * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal From 2c89ea79cfb3e3a9fa9c307e8a09dc2ed6cbcc2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 18:46:45 +0200 Subject: [PATCH 15/47] fix: document why some variables are not set during initialization [N-10] --- contracts/l2/reservoir/L2Reservoir.sol | 5 ++++- contracts/reservoir/L1Reservoir.sol | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 2a0673ba2..224d7fec4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -31,7 +31,10 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { /** * @dev Initialize this contract. - * The contract will be paused. + * The contract will be paused. Note that issuance parameters + * are not set here because they are set from L1 through the drip function. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately. * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index c3a992b99..55835269d 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -42,6 +42,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { /** * @dev Initialize this contract. * The contract will be paused. + * Note that the contract is designed to not accrue rewards until the first call + * to the drip function, that also requires the initial supply snapshot to be taken + * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction + * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. + * On the other hand, the l2ReservoirAddress is not expected to be known at initialization + * time and must therefore be set using setL2ReservoirAddress. + * The RewardsManager's address might also not be available in the controller at initialization + * time, so approveRewardsManager() must be called separately as well. * @param _controller Address of the Controller that manages this contract * @param _dripInterval Drip interval, i.e. time period for which rewards are minted each time we drip */ From c1336ef987be9f0cfa002a693d843c3d5ddd3bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 13 Jul 2022 18:57:45 +0200 Subject: [PATCH 16/47] fix: add missing getters to Managed [N-11] --- contracts/governance/Managed.sol | 9 +++++++++ contracts/l2/reservoir/L2Reservoir.sol | 2 +- contracts/reservoir/L1Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 4 ---- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/governance/Managed.sol b/contracts/governance/Managed.sol index c543415e4..20a9191e5 100644 --- a/contracts/governance/Managed.sol +++ b/contracts/governance/Managed.sol @@ -10,6 +10,7 @@ import "../rewards/IRewardsManager.sol"; import "../staking/IStaking.sol"; import "../token/IGraphToken.sol"; import "../arbitrum/ITokenGateway.sol"; +import "../reservoir/IReservoir.sol"; /** * @title Graph Managed contract @@ -154,6 +155,14 @@ contract Managed { return ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); } + /** + * @dev Return Reservoir (L1 or L2) interface. + * @return Reservoir contract registered with Controller + */ + function reservoir() internal view returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } + /** * @dev Resolve a contract address from the cache or the Controller if not found. * @return Address of the contract diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 224d7fec4..a9a410abf 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -25,7 +25,7 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. */ modifier onlyL2Gateway() { - require(msg.sender == _resolveContract(keccak256("GraphTokenGateway")), "ONLY_GATEWAY"); + require(msg.sender == address(graphTokenGateway()), "ONLY_GATEWAY"); _; } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 55835269d..0215e07ea 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -326,7 +326,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { nextDripNonce = nextDripNonce.add(1); bytes memory data = abi.encode(_maxSubmissionCost, extraData); IGraphToken grt = graphToken(); - ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + ITokenGateway gateway = graphTokenGateway(); grt.approve(address(gateway), _nTokens); gateway.outboundTransfer{ value: msg.value }( address(grt), diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 1b07c5559..eb2567b4f 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -437,8 +437,4 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa emit RewardsBurned(alloc.indexer, _allocationID, alloc.closedAtEpoch, rewards); } } - - function reservoir() internal view returns (IReservoir) { - return IReservoir(_resolveContract(keccak256("Reservoir"))); - } } From 86cddb8964a26e91f10db2d21cebe8d5c4e3378e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 12:45:34 +0200 Subject: [PATCH 17/47] fix: separate contracts into different files [N-13] --- contracts/l2/reservoir/IL2Reservoir.sol | 30 +++++++++++++++++++++++++ contracts/l2/reservoir/L2Reservoir.sol | 1 + contracts/reservoir/IReservoir.sol | 25 --------------------- contracts/reservoir/L1Reservoir.sol | 2 +- 4 files changed, 32 insertions(+), 26 deletions(-) create mode 100644 contracts/l2/reservoir/IL2Reservoir.sol diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol new file mode 100644 index 000000000..90b089ac9 --- /dev/null +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../reservoir/IReservoir.sol"; + +/** + * @title Interface for the L2 Rewards Reservoir + * @dev This exposes a specific function for the L2Reservoir that is called + * as a callhook from L1 to L2, so that state can be updated when dripped rewards + * are bridged between layers. + */ +interface IL2Reservoir is IReservoir { + /** + * @dev Receive dripped tokens from L1. + * This function can only be called by the gateway, as it is + * meant to be a callhook when receiving tokens from L1. It + * updates the issuanceBase and issuanceRate, + * and snapshots the accumulated rewards. If issuanceRate changes, + * it also triggers a snapshot of rewards per signal on the RewardsManager. + * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) + * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 + * @param _nonce Incrementing nonce to ensure messages are received in order + */ + function receiveDrip( + uint256 _issuanceBase, + uint256 _issuanceRate, + uint256 _nonce + ) external; +} diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index a9a410abf..67376648a 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -7,6 +7,7 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; +import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; /** diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index de25a2b80..2d3919147 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -31,28 +31,3 @@ interface IReservoir { */ function getNewRewards(uint256 _blocknum) external view returns (uint256); } - -/** - * @title Interface for the L2 Rewards Reservoir - * @dev This exposes a specific function for the L2Reservoir that is called - * as a callhook from L1 to L2, so that state can be updated when dripped rewards - * are bridged between layers. - */ -interface IL2Reservoir is IReservoir { - /** - * @dev Receive dripped tokens from L1. - * This function can only be called by the gateway, as it is - * meant to be a callhook when receiving tokens from L1. It - * updates the issuanceBase and issuanceRate, - * and snapshots the accumulated rewards. If issuanceRate changes, - * it also triggers a snapshot of rewards per signal on the RewardsManager. - * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) - * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 - * @param _nonce Incrementing nonce to ensure messages are received in order - */ - function receiveDrip( - uint256 _issuanceBase, - uint256 _issuanceRate, - uint256 _nonce - ) external; -} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0215e07ea..0636b9c99 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -7,7 +7,7 @@ import "@openzeppelin/contracts/math/SafeMath.sol"; import "../arbitrum/ITokenGateway.sol"; -import "./IReservoir.sol"; +import "../l2/reservoir/IL2Reservoir.sol"; import "./Reservoir.sol"; import "./L1ReservoirStorage.sol"; From d327b360b0ab341e9d0976418f89021bbde5ce72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 12:59:29 +0200 Subject: [PATCH 18/47] fix: rename some variables for clarity [N-14] --- contracts/l2/reservoir/L2Reservoir.sol | 4 ++-- contracts/reservoir/L1Reservoir.sol | 31 ++++++++++++++++---------- contracts/reservoir/Reservoir.sol | 2 +- contracts/rewards/RewardsManager.sol | 14 ++++++------ 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 67376648a..a317d02a4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -73,8 +73,8 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { } return issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) .sub(issuanceBase); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0636b9c99..5e98f2570 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -97,7 +97,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2RewardsFraction Fraction of rewards to send to L2, in wei / fixed point at 1e18 */ function setL2RewardsFraction(uint256 _l2RewardsFraction) external onlyGovernor { - require(_l2RewardsFraction <= TOKEN_DECIMALS, "L2 Rewards fraction must be <= 1"); + require( + _l2RewardsFraction <= FIXED_POINT_SCALING_FACTOR, + "L2 Rewards fraction must be <= 1" + ); nextL2RewardsFraction = _l2RewardsFraction; emit L2RewardsFractionStaged(_l2RewardsFraction); } @@ -187,7 +190,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 tokensToSendToL2 = 0; if (l2RewardsFraction != nextL2RewardsFraction) { tokensToSendToL2 = nextL2RewardsFraction.mul(newRewardsToDistribute).div( - TOKEN_DECIMALS + FIXED_POINT_SCALING_FACTOR ); if (mintedRewardsTotal > mintedRewardsActual) { // eps > 0, i.e. t < t1_old @@ -198,7 +201,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // with the new values to the L2Reservoir. uint256 l2OffsetAmount = l2RewardsFraction .mul(mintedRewardsTotal.sub(mintedRewardsActual)) - .div(TOKEN_DECIMALS); + .div(FIXED_POINT_SCALING_FACTOR); require( tokensToSendToL2 > l2OffsetAmount, "Negative amount would be sent to L2, wait before calling again" @@ -207,7 +210,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { } else { tokensToSendToL2 = tokensToSendToL2.add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( - TOKEN_DECIMALS + FIXED_POINT_SCALING_FACTOR ) ); } @@ -220,7 +223,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { _l2MaxSubmissionCost ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(TOKEN_DECIMALS); + tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); _sendNewTokensAndStateToL2( tokensToSendToL2, _l2MaxGas, @@ -251,8 +254,8 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { } return issuanceBase - .mul(_pow(issuanceRate, _blocknum.sub(t0), TOKEN_DECIMALS)) - .div(TOKEN_DECIMALS) + .mul(_pow(issuanceRate, _blocknum.sub(t0), FIXED_POINT_SCALING_FACTOR)) + .div(FIXED_POINT_SCALING_FACTOR) .sub(issuanceBase); } @@ -266,9 +269,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { */ function getNewRewards(uint256 _blocknum) public view override returns (uint256) { return - getNewGlobalRewards(_blocknum).mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div( - TOKEN_DECIMALS - ); + getNewGlobalRewards(_blocknum) + .mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)) + .div(FIXED_POINT_SCALING_FACTOR); } /** @@ -297,7 +300,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // to save gas accumulatedLayerRewards = accumulatedLayerRewards + - _globalDelta.mul(TOKEN_DECIMALS.sub(l2RewardsFraction)).div(TOKEN_DECIMALS); + _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( + FIXED_POINT_SCALING_FACTOR + ); lastRewardsUpdateBlock = block.number; } @@ -316,7 +321,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { uint256 _gasPriceBid, uint256 _maxSubmissionCost ) internal { - uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div(TOKEN_DECIMALS); + uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( + FIXED_POINT_SCALING_FACTOR + ); bytes memory extraData = abi.encodeWithSelector( IL2Reservoir.receiveDrip.selector, l2IssuanceBase, diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 048697bc9..6e15948f6 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -20,7 +20,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir using SafeMath for uint256; uint256 private constant MAX_UINT256 = 2**256 - 1; - uint256 internal constant TOKEN_DECIMALS = 1e18; + uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 internal constant MIN_ISSUANCE_RATE = 1e18; /** diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index eb2567b4f..15d2ff560 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -33,7 +33,7 @@ import "../reservoir/IReservoir.sol"; contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; - uint256 private constant TOKEN_DECIMALS = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 private constant MIN_ISSUANCE_RATE = 1e18; // -- Events -- @@ -198,7 +198,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // Get the new issuance per signalled token // We multiply the decimals to keep the precision as fixed-point number return - (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(TOKEN_DECIMALS).div( + (accRewardsNow.sub(accRewardsOnLastSignalUpdate)).mul(FIXED_POINT_SCALING_FACTOR).div( signalledTokens ); } @@ -232,7 +232,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -264,9 +264,9 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa return (0, accRewardsForSubgraph); } - uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph.mul(TOKEN_DECIMALS).div( - subgraphAllocatedTokens - ); + uint256 newRewardsPerAllocatedToken = newRewardsForSubgraph + .mul(FIXED_POINT_SCALING_FACTOR) + .div(subgraphAllocatedTokens); return ( subgraph.accRewardsPerAllocatedToken.add(newRewardsPerAllocatedToken), accRewardsForSubgraph @@ -366,7 +366,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa uint256 _endAccRewardsPerAllocatedToken ) private pure returns (uint256) { uint256 newAccrued = _endAccRewardsPerAllocatedToken.sub(_startAccRewardsPerAllocatedToken); - return newAccrued.mul(_tokens).div(TOKEN_DECIMALS); + return newAccrued.mul(_tokens).div(FIXED_POINT_SCALING_FACTOR); } /** From af81533b486118f1f21123e87a7b2248a5acfb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:29:04 +0200 Subject: [PATCH 19/47] fix: document the need to retry tickets if drip is received out-of-order [N-15] --- contracts/l2/reservoir/L2Reservoir.sol | 3 +++ contracts/reservoir/L1Reservoir.sol | 3 +++ 2 files changed, 6 insertions(+) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index a317d02a4..1bc93c912 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -85,6 +85,9 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 5e98f2570..547b3e618 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -147,6 +147,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * until the drip amount becomes positive before calling the function again. It can also revert * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 From f20124ad22adedad4866686c879de8bc002c98a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:37:32 +0200 Subject: [PATCH 20/47] fix: various typos [N-16] --- contracts/reservoir/IReservoir.sol | 2 +- contracts/reservoir/L1Reservoir.sol | 6 +++--- contracts/rewards/RewardsManager.sol | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index 2d3919147..a101952e0 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; /** * @title Interface for the Rewards Reservoir * @dev This is the shared interface between L1 and L2, for the contracts - * that hold rewards on each layers and provide functions to compute + * that hold rewards on each layer and provide functions to compute * accumulated and new total rewards. */ interface IReservoir { diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 547b3e618..21e3d750a 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -150,9 +150,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. - * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 - * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if L2RewardsFraction is > 0 + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 */ function drip( uint256 _l2MaxGas, diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 15d2ff560..d97963390 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -276,7 +276,7 @@ contract RewardsManager is RewardsManagerV4Storage, GraphUpgradeable, IRewardsMa // -- Updates -- /** - * @dev Updates the accumulated rewards per signal and save checkpoint block number. + * @dev Updates the accumulated rewards per signal and saves the checkpoint block number. * Also snapshots total accumulated rewards (`accRewardsOnLastSignalUpdate`). * Must be called before `issuanceRate` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() From 1d1e319443991f00626a674d895666d13d76f4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 13:56:47 +0200 Subject: [PATCH 21/47] fix: replace MAX_UINT256 with type().max [N-18] [N-20] --- contracts/reservoir/Reservoir.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 6e15948f6..26c5f02f2 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -19,7 +19,6 @@ import "./IReservoir.sol"; abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { using SafeMath for uint256; - uint256 private constant MAX_UINT256 = 2**256 - 1; uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; uint256 internal constant MIN_ISSUANCE_RATE = 1e18; @@ -27,7 +26,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir * @dev Approve the RewardsManager to manage the reservoir's token funds */ function approveRewardsManager() external override onlyGovernor { - graphToken().approve(address(rewardsManager()), MAX_UINT256); + graphToken().approve(address(rewardsManager()), type(uint256).max); } /** From 90316517bace73ce6bbc4120b5693e411c378347 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 14 Jul 2022 14:01:20 +0200 Subject: [PATCH 22/47] fix: remove an unused import [N-21] --- contracts/reservoir/ReservoirStorage.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index 8e3e2591d..b46e44d35 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -2,7 +2,6 @@ pragma solidity ^0.7.6; -import "./IReservoir.sol"; import "../governance/Managed.sol"; /** From e2e08d156cdffd0d53c4542caa446b28dba85625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:15:52 +0200 Subject: [PATCH 23/47] test: remove repeated addToCallhookWhitelist --- test/lib/fixtures.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 255d8586a..e00e7698b 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -282,9 +282,6 @@ export class NetworkFixture { await l1FixtureContracts.l1Reservoir .connect(deployer) .setL2ReservoirAddress(mockL2ReservoirAddress) - await l1FixtureContracts.l1GraphTokenGateway - .connect(deployer) - .addToCallhookWhitelist(l1FixtureContracts.l1Reservoir.address) await l1FixtureContracts.l1GraphTokenGateway.connect(deployer).setPaused(false) } From bbb3fadd682281785da43c8aa4ed2c25d2d41340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:05:56 +0200 Subject: [PATCH 24/47] fix: use SafeMath more consistently --- contracts/reservoir/L1Reservoir.sol | 8 ++++---- contracts/reservoir/Reservoir.sol | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 21e3d750a..674e127ef 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -298,14 +298,14 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _globalDelta New global rewards (i.e. rewards on L1 and L2) since the last update block */ function snapshotAccumulatedRewards(uint256 _globalDelta) internal { - issuanceBase = issuanceBase + _globalDelta; + issuanceBase = issuanceBase.add(_globalDelta); // Reimplementation of getAccumulatedRewards but reusing the _globalDelta calculated above, // to save gas - accumulatedLayerRewards = - accumulatedLayerRewards + + accumulatedLayerRewards = accumulatedLayerRewards.add( _globalDelta.mul(FIXED_POINT_SCALING_FACTOR.sub(l2RewardsFraction)).div( FIXED_POINT_SCALING_FACTOR - ); + ) + ); lastRewardsUpdateBlock = block.number; } diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index 26c5f02f2..d2a0bf6cb 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -36,7 +36,7 @@ abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir */ function getAccumulatedRewards(uint256 _blocknum) public view override returns (uint256) { // R(t) = R(t0) + (DeltaR(t, t0)) - return accumulatedLayerRewards + getNewRewards(_blocknum); + return accumulatedLayerRewards.add(getNewRewards(_blocknum)); } /** From e2766b32c8a10991e9a137117b10fc99f7ce3863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Sun, 5 Jun 2022 15:17:14 -0700 Subject: [PATCH 25/47] feat: keeper reward for reservoir drip through token issuance --- contracts/l2/reservoir/IL2Reservoir.sol | 11 +- contracts/l2/reservoir/L2Reservoir.sol | 15 +- contracts/l2/reservoir/L2ReservoirStorage.sol | 5 + contracts/reservoir/L1Reservoir.sol | 58 +++++- contracts/reservoir/L1ReservoirStorage.sol | 7 + test/l2/l2Reservoir.test.ts | 28 ++- test/reservoir/l1Reservoir.test.ts | 170 ++++++++++++++---- test/rewards/rewards.test.ts | 6 +- 8 files changed, 254 insertions(+), 46 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 90b089ac9..5fd2fc861 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -18,13 +18,22 @@ interface IL2Reservoir is IReservoir { * updates the issuanceBase and issuanceRate, * and snapshots the accumulated rewards. If issuanceRate changes, * it also triggers a snapshot of rewards per signal on the RewardsManager. + * Note that the transaction might revert if it's received out-of-order, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (tx.origin) * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( uint256 _issuanceBase, uint256 _issuanceRate, - uint256 _nonce + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper ) external; } diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 1bc93c912..3b1dd131d 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -16,7 +16,7 @@ import "./L2ReservoirStorage.sol"; * It receives tokens for rewards from L1, and provides functions to compute accumulated and new * total rewards at a particular block number. */ -contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { +contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; event DripReceived(uint256 _issuanceBase); @@ -88,14 +88,20 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { * Note that the transaction might revert if it's received out-of-order, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. + * A keeper reward will be sent to the keeper that dripped on L1, and part of it + * to whoever redeemed the current retryable ticket (tx.origin) * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( uint256 _issuanceBase, uint256 _issuanceRate, - uint256 _nonce + uint256 _nonce, + uint256 _keeperReward, + address _l1Keeper ) external override onlyL2Gateway { require(_nonce == nextDripNonce, "INVALID_NONCE"); nextDripNonce = nextDripNonce.add(1); @@ -108,6 +114,11 @@ contract L2Reservoir is L2ReservoirV1Storage, Reservoir, IL2Reservoir { snapshotAccumulatedRewards(); } issuanceBase = _issuanceBase; + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); + IGraphToken grt = graphToken(); + // solhint-disable-next-line avoid-tx-origin + grt.transfer(tx.origin, _l2KeeperReward); + grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); emit DripReceived(issuanceBase); } diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index ee7880343..4e8c6825d 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -9,3 +9,8 @@ contract L2ReservoirV1Storage { // Expected nonce value for the next drip hook uint256 public nextDripNonce; } + +contract L2ReservoirV2Storage is L2ReservoirV1Storage { + // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) + uint256 public l2KeeperRewardFraction; +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 674e127ef..0e9059afa 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -17,7 +17,7 @@ import "./L1ReservoirStorage.sol"; * It provides a function to periodically drip rewards, and functions to compute accumulated and new * total rewards at a particular block number. */ -contract L1Reservoir is L1ReservoirV1Storage, Reservoir { +contract L1Reservoir is L1ReservoirV2Storage, Reservoir { using SafeMath for uint256; // Emitted when the initial supply snapshot is taken after contract deployment @@ -38,6 +38,10 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { event RewardsDripped(uint256 totalMinted, uint256 sentToL2, uint256 nextDeadline); // Emitted when the address for the L2Reservoir is updated event L2ReservoirAddressUpdated(address l2ReservoirAddress); + // Emitted when drip reward per block is updated + event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); + // Emitted when minDripInterval is updated + event MinDripIntervalUpdated(uint256 minDripInterval); /** * @dev Initialize this contract. @@ -105,6 +109,26 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { emit L2RewardsFractionStaged(_l2RewardsFraction); } + /** + * @dev Sets the drip reward per block + * This is the reward in GRT provided to the keeper that calls drip() + * @param _dripRewardPerBlock GRT accrued for each block after the threshold + */ + function setDripRewardPerBlock(uint256 _dripRewardPerBlock) external onlyGovernor { + dripRewardPerBlock = _dripRewardPerBlock; + emit DripRewardPerBlockUpdated(_dripRewardPerBlock); + } + + /** + * @dev Sets the minimum drip interval + * This is the minimum number of blocks between two successful drips + * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed + */ + function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { + minDripInterval = _minDripInterval; + emit MinDripIntervalUpdated(_minDripInterval); + } + /** * @dev Sets the L2 Reservoir address * This is the address on L2 to which we send tokens for rewards. @@ -153,16 +177,23 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) */ function drip( uint256 _l2MaxGas, uint256 _l2GasPriceBid, uint256 _l2MaxSubmissionCost + address _keeperRewardBeneficiary ) external payable notPaused { + require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); + uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + uint256 keeperReward = dripRewardPerBlock.mul( + block.number.sub(lastRewardsUpdateBlock).sub(minDripInterval) + ); if (nextIssuanceRate != issuanceRate) { rewardsManager().updateAccRewardsPerSignal(); snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock @@ -178,7 +209,7 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { // N = n - eps uint256 tokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual).add(keeperReward); require( newRewardsPlusMintedActual >= mintedRewardsTotal, "Would mint negative tokens, wait before calling again" @@ -186,8 +217,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); } + IGraphToken grt = graphToken(); if (tokensToMint > 0) { - graphToken().mint(address(this), tokensToMint); + grt.mint(address(this), tokensToMint); } uint256 tokensToSendToL2 = 0; @@ -223,7 +255,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToSendToL2, _l2MaxGas, _l2GasPriceBid, - _l2MaxSubmissionCost + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); @@ -231,12 +265,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { tokensToSendToL2, _l2MaxGas, _l2GasPriceBid, - _l2MaxSubmissionCost + _l2MaxSubmissionCost, + keeperReward, + _keeperRewardBeneficiary ); } else { // Avoid locking funds in this contract if we don't need to // send a message to L2. require(msg.value == 0, "No eth value needed"); + // If we don't send rewards to L2, pay the keeper reward in L1 + grt.transfer(_keeperRewardBeneficiary, keeperReward); } emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); } @@ -317,12 +355,16 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { * @param _maxGas Max gas for the L2 retryable ticket execution * @param _gasPriceBid Gas price for the L2 retryable ticket execution * @param _maxSubmissionCost Max submission price for the L2 retryable ticket + * @param _keeperReward Tokens to assign as keeper reward for calling drip + * @param _keeper Address of the keeper that will be rewarded */ function _sendNewTokensAndStateToL2( uint256 _nTokens, uint256 _maxGas, uint256 _gasPriceBid, - uint256 _maxSubmissionCost + uint256 _maxSubmissionCost, + uint256 _keeperReward, + address _keeper ) internal { uint256 l2IssuanceBase = l2RewardsFraction.mul(issuanceBase).div( FIXED_POINT_SCALING_FACTOR @@ -331,7 +373,9 @@ contract L1Reservoir is L1ReservoirV1Storage, Reservoir { IL2Reservoir.receiveDrip.selector, l2IssuanceBase, issuanceRate, - nextDripNonce + nextDripNonce, + _keeperReward, + _keeper ); nextDripNonce = nextDripNonce.add(1); bytes memory data = abi.encode(_maxSubmissionCost, extraData); diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 90821c809..04bb2aa31 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -21,3 +21,10 @@ contract L1ReservoirV1Storage { // Auto-incrementing nonce that will be used when sending rewards to L2, to ensure ordering uint256 public nextDripNonce; } + +contract L1ReservoirV2Storage is L1ReservoirV1Storage { + // Minimum number of blocks since last drip for a new drip to be allowed + uint256 public minDripInterval; + // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold + uint256 public dripRewardPerBlock; +} diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index ebfe3f52e..55c339c60 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -160,7 +160,13 @@ describe('L2Reservoir', () => { it('rejects the call when not called by the gateway', async function () { const tx = l2Reservoir .connect(governor.signer) - .receiveDrip(dripNormalizedSupply, dripIssuanceRate, toBN('0')) + .receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + toBN('0'), + testAccount1.address, + ) await expect(tx).revertedWith('ONLY_GATEWAY') }) it('rejects the call when received out of order', async function () { @@ -169,6 +175,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -181,6 +189,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate.add(1), toBN('2'), + toBN('0'), + testAccount1.address, ) const tx2 = gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -192,6 +202,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -205,6 +217,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -216,6 +230,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate.add(1), toBN('1'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -230,6 +246,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -241,6 +259,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate, toBN('1'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -255,6 +275,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, dripIssuanceRate, toBN('0'), + toBN('0'), + testAccount1.address, ) let tx = await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -267,6 +289,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply.add(1), dripIssuanceRate, toBN('2'), + toBN('0'), + testAccount1.address, ) tx = await gatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() @@ -285,6 +309,8 @@ describe('L2Reservoir', () => { dripNormalizedSupply, ISSUANCE_RATE_PER_BLOCK, toBN('0'), + toBN('0'), + testAccount1.address, ) await validGatewayFinalizeTransfer(receiveDripTx.data) dripBlock = await latestBlock() diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index afc6233ca..86801149b 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -35,7 +35,7 @@ const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi const l2ReservoirIface = new Interface(l2ReservoirAbi) const { AddressZero } = constants -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] const maxGas = toBN('1000000') const maxSubmissionCost = toBN('7') @@ -49,6 +49,7 @@ describe('L1Reservoir', () => { let mockL2GRT: Account let mockL2Gateway: Account let mockL2Reservoir: Account + let keeper: Account let fixture: NetworkFixture let grt: GraphToken @@ -120,7 +121,9 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) let expectedNextDeadline = dripBlock.add(dripInterval) let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx1 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) @@ -133,7 +136,9 @@ describe('L1Reservoir', () => { await advanceBlocks(blocksToAdvance) - const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx2 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) expectedNextDeadline = (await latestBlock()).add(dripInterval) const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) @@ -147,7 +152,7 @@ describe('L1Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir] = + ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir, keeper] = await getAccounts() fixture = new NetworkFixture() @@ -248,7 +253,7 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(0) expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - tx = l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + tx = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) }) @@ -313,12 +318,25 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'L2RewardsFractionStaged').withArgs(newValue) expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) tx = l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) }) }) + describe('minimum drip interval update', function () { + it('rejects setting minimum drip interval if unauthorized', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + + it('sets the minimum drip interval', async function () { + const newValue = toBN('200') + const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) + await expect(tx).emit(l1Reservoir, 'MinDripIntervalUpdated').withArgs(newValue) + expect(await l1Reservoir.minDripInterval()).eq(newValue) + }) + }) }) // TODO test that rewardsManager.updateAccRewardsPerSignal is called when @@ -337,7 +355,9 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - const tx = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + const tx = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) @@ -345,7 +365,7 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount, toBN(0), expectedNextDeadline) }) - it('has no effect if called a second time in the same block', async function () { + it('cannot be called more than once per minDripInterval', async function () { supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) @@ -358,14 +378,16 @@ describe('L1Reservoir', () => { expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) - await provider().send('evm_setAutomine', [false]) - const tx1 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - const tx2 = await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) - await provider().send('evm_mine', []) - await provider().send('evm_setAutomine', [true]) + + const tx1 = await l1Reservoir + .connect(keeper.signer) + .drip(toBN(0), toBN(0), toBN(0), keeper.address) + + const minInterval = toBN('200') + await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) const actualAmount = await grt.balanceOf(l1Reservoir.address) - expect(await latestBlock()).eq(dripBlock) // Just in case disabling automine stops working + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) await expect(tx1) .emit(l1Reservoir, 'RewardsDripped') @@ -373,15 +395,24 @@ describe('L1Reservoir', () => { await expect(tx1) .emit(grt, 'Transfer') .withArgs(AddressZero, l1Reservoir.address, actualAmount) - await expect(tx2) - .emit(l1Reservoir, 'RewardsDripped') - .withArgs(toBN(0), toBN(0), expectedNextDeadline) - await expect(tx2).not.emit(grt, 'Transfer') + + const tx2 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') + + // We've had 1 block since the last drip so far, so we jump to one block before the interval is done + await advanceBlocks(minInterval.sub(2)) + const tx3 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') + + await advanceBlocks(1) + // Now we're over the interval so we can drip again + const tx4 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await expect(tx4).emit(l1Reservoir, 'RewardsDripped') }) it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { const tx = l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) await expect(tx).revertedWith('No eth value needed') }) it('mints only a few more tokens if called on the next block', async function () { @@ -413,8 +444,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -434,6 +465,69 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, + ]) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + }) + it('sends the specified fraction of the rewards with a keeper reward to L2', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) + await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) + // lastRewardsUpdateBlock is set to block.number with initialSnapshot + await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + await advanceBlocks(toBN('4')) + + // now we're at lastRewardsUpdateBlock + minDripInterval + 3, so keeper reward should be: + // dripRewardPerBlock * 3 + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) + const expectedMintedAmount = expectedMintedRewards.add(toGRT('9')) + const expectedSentToL2 = expectedMintedRewards.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + normalizedTokenSupply, + issuanceRate, + toBN('0'), + toGRT('9'), // keeper reward + keeper.address, ]) const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -462,8 +556,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -483,6 +577,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, ]) let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -511,8 +607,8 @@ describe('L1Reservoir', () => { .add(expectedTotalRewards.sub(rewardsUntilSecondDripBlock).mul(8).div(10)) const tx2 = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -529,6 +625,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, ]) expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -564,8 +662,8 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -585,6 +683,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('0'), + toBN('0'), + keeper.address, ]) let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -610,8 +710,8 @@ describe('L1Reservoir', () => { const expectedNewTotalSentToL2 = expectedTotalRewards.div(2) const tx2 = await l1Reservoir - .connect(governor.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -628,6 +728,8 @@ describe('L1Reservoir', () => { l2IssuanceBase, issuanceRate, toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, ]) expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, @@ -654,7 +756,7 @@ describe('L1Reservoir', () => { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) supplyBeforeDrip = await grt.totalSupply() - await l1Reservoir.drip(toBN(0), toBN(0), toBN(0)) + await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -724,7 +826,9 @@ describe('L1Reservoir', () => { it('computes the rewards delta considering the L2 rewards fraction', async function () { const lambda = toGRT('0.32') await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) - await l1Reservoir.drip(maxGas, gasPriceBid, maxSubmissionCost, { value: defaultEthValue }) + await l1Reservoir + .connect(keeper.signer) + .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index e2d141d95..7a002a49d 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -42,6 +42,7 @@ describe('Rewards', () => { let indexer1: Account let indexer2: Account let oracle: Account + let keeper: Account let fixture: NetworkFixture @@ -107,7 +108,8 @@ describe('Rewards', () => { } before(async function () { - ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle] = await getAccounts() + ;[delegator, governor, curator1, curator2, indexer1, indexer2, oracle, keeper] = + await getAccounts() fixture = new NetworkFixture() ;({ grt, curation, epochManager, staking, rewardsManager, l1Reservoir } = await fixture.load( @@ -299,7 +301,7 @@ describe('Rewards', () => { beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - await l1Reservoir.connect(governor.signer).drip(toBN(0), toBN(0), toBN(0)) + await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) From fc42c3f65d88bf0528a21fd062036ac3cdd560b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 7 Jun 2022 21:09:35 -0700 Subject: [PATCH 26/47] fix: don't use tx.origin as it will not work --- contracts/l2/reservoir/L2Reservoir.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 3b1dd131d..96a344f67 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -114,11 +114,15 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { snapshotAccumulatedRewards(); } issuanceBase = _issuanceBase; - uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); IGraphToken grt = graphToken(); - // solhint-disable-next-line avoid-tx-origin - grt.transfer(tx.origin, _l2KeeperReward); - grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + // We'd like to reward the keeper that redeemed the tx in L2 + // but this won't work right now as tx.origin will actually be the L1 sender. + // uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); + // // solhint-disable-next-line avoid-tx-origin + // grt.transfer(tx.origin, _l2KeeperReward); + // grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + // So for now we just send all the rewards to teh L1 keeper: + grt.transfer(_l1Keeper, _keeperReward); emit DripReceived(issuanceBase); } From e69c095c720b115a3786da824ecc81e2f805568e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 21 Jun 2022 15:37:33 +0300 Subject: [PATCH 27/47] fix: only allow indexers, their operators, or whitelisted addresses to call drip (needs tests) --- contracts/reservoir/L1Reservoir.sol | 122 ++++++++++++++++++++- contracts/reservoir/L1ReservoirStorage.sol | 2 + 2 files changed, 121 insertions(+), 3 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0e9059afa..566181f43 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -42,6 +42,29 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { event DripRewardPerBlockUpdated(uint256 dripRewardPerBlock); // Emitted when minDripInterval is updated event MinDripIntervalUpdated(uint256 minDripInterval); + // Emitted when a new allowedDripper is added + event AllowedDripperAdded(address dripper); + // Emitted when an allowedDripper is revoked + event AllowedDripperRevoked(address dripper); + + /** + * @dev Checks that the sender is an indexer with stake on the Staking contract, + * or that the sender is an address whitelisted by governance to call. + */ + modifier onlyIndexerOrAllowedDripper() { + require(allowedDrippers[msg.sender] || _isIndexer(msg.sender), "UNAUTHORIZED"); + _; + } + + /** + * @dev Checks that the sender is an operator for the specified indexer + * (also checks that the specified indexer is, indeed, an indexer). + * @param _indexer Indexer for which the sender must be an operator + */ + modifier onlyIndexerOperator(address _indexer) { + require(_isIndexer(_indexer) && staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED"); + _; + } /** * @dev Initialize this contract. @@ -140,6 +163,24 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { emit L2ReservoirAddressUpdated(_l2ReservoirAddress); } + /** + * @dev Grants an address permission to call drip() + * @param _dripper Address that will be an allowed dripper + */ + function grantDripPermission(address _dripper) external onlyGovernor { + allowedDrippers[_dripper] = true; + emit AllowedDripperAdded(_dripper); + } + + /** + * @dev Revokes an address' permission to call drip() + * @param _dripper Address that will not be an allowed dripper anymore + */ + function revokeDripPermission(address _dripper) external onlyGovernor { + allowedDrippers[_dripper] = false; + emit AllowedDripperRevoked(_dripper); + } + /** * @dev Computes the initial snapshot for token supply and mints any pending rewards * This will initialize the issuanceBase to the current GRT supply, after which @@ -164,7 +205,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. - * The call value must be equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must * only be nonzero if l2RewardsFraction is nonzero. * Calling this function can revert if the issuance rate has recently been reduced, and the existing * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait @@ -174,17 +215,83 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. + * This function with an additional parameter is only provided so that indexer operators can call it, + * specifying the indexer for which they are an operator. * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + * @param _indexer Indexer for whom the sender must be an authorized Operator */ function drip( uint256 _l2MaxGas, uint256 _l2GasPriceBid, - uint256 _l2MaxSubmissionCost + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary, + address _indexer + ) external payable notPaused onlyIndexerOperator(_indexer) { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2 + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, address _keeperRewardBeneficiary - ) external payable notPaused { + ) external payable notPaused onlyIndexerOrAllowedDripper { + _drip(_l2MaxGas, _l2GasPriceBid, _l2MaxSubmissionCost, _keeperRewardBeneficiary); + } + + /** + * @dev Drip indexer rewards for layers 1 and 2, private implementation. + * This function will mint enough tokens to cover all indexer rewards for the next + * dripInterval number of blocks. If the l2RewardsFraction is > 0, it will also send + * tokens and a callhook to the L2Reservoir, through the GRT Arbitrum bridge. + * Any staged changes to issuanceRate or l2RewardsFraction will be applied when this function + * is called. If issuanceRate changes, it also triggers a snapshot of rewards per signal on the RewardsManager. + * The call value must be greater than or equal to l2MaxSubmissionCost + (l2MaxGas * l2GasPriceBid), and must + * only be nonzero if l2RewardsFraction is nonzero. + * Calling this function can revert if the issuance rate has recently been reduced, and the existing + * tokens are sufficient to cover the full pending period. In this case, it's necessary to wait + * until the drip amount becomes positive before calling the function again. It can also revert + * if the l2RewardsFraction has been updated and the amount already sent to L2 is more than what we + * should send now. + * Note that the transaction on the L2 side might revert if it's received out-of-order by the L2Reservoir, + * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed + * again once the ticket for previous drip has been redeemed. + * @param _l2MaxGas Max gas for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2GasPriceBid Gas price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _l2MaxSubmissionCost Max submission price for the L2 retryable ticket, only needed if l2RewardsFraction is > 0 + * @param _keeperRewardBeneficiary Address to which to credit keeper reward (will be redeemed in L2 if l2RewardsFraction is nonzero) + */ + function _drip( + uint256 _l2MaxGas, + uint256 _l2GasPriceBid, + uint256 _l2MaxSubmissionCost, + address _keeperRewardBeneficiary + ) private { require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); @@ -391,4 +498,13 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { data ); } + + /** + * @dev Checks if an address is an indexer with stake in the Staking contract + * @param _indexer Address that will be checked + */ + function _isIndexer(address _indexer) internal view returns (bool) { + IStaking staking = staking(); + return staking.hasStake(_indexer); + } } diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 04bb2aa31..92b9d5107 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -27,4 +27,6 @@ contract L1ReservoirV2Storage is L1ReservoirV1Storage { uint256 public minDripInterval; // Drip reward in GRT for each block since lastRewardsUpdateBlock + dripRewardThreshold uint256 public dripRewardPerBlock; + // True for addresses that are allowed to call drip() + mapping(address => bool) public allowedDrippers; } From c274636d30d3086736446efb7c7ea407cead4ee1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Tue, 21 Jun 2022 17:21:47 +0300 Subject: [PATCH 28/47] test: fix rewards and reservoir tests after restricting drip callers --- contracts/reservoir/L1Reservoir.sol | 13 +- test/reservoir/l1Reservoir.test.ts | 208 +++++++++++++++++++++++----- test/rewards/rewards.test.ts | 20 ++- 3 files changed, 195 insertions(+), 46 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 566181f43..30fa4d82a 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -316,7 +316,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // N = n - eps uint256 tokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual).add(keeperReward); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute + .add(mintedRewardsActual) + .add(keeperReward); require( newRewardsPlusMintedActual >= mintedRewardsTotal, "Would mint negative tokens, wait before calling again" @@ -348,9 +350,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { tokensToSendToL2 > l2OffsetAmount, "Negative amount would be sent to L2, wait before calling again" ); - tokensToSendToL2 = tokensToSendToL2.sub(l2OffsetAmount); + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).sub(l2OffsetAmount); } else { - tokensToSendToL2 = tokensToSendToL2.add( + tokensToSendToL2 = tokensToSendToL2.add(keeperReward).add( l2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)).div( FIXED_POINT_SCALING_FACTOR ) @@ -367,7 +369,10 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint.mul(l2RewardsFraction).div(FIXED_POINT_SCALING_FACTOR); + tokensToSendToL2 = tokensToMint + .mul(l2RewardsFraction) + .div(FIXED_POINT_SCALING_FACTOR) + .add(keeperReward); _sendNewTokensAndStateToL2( tokensToSendToL2, _l2MaxGas, diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 86801149b..9c272c847 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -1,5 +1,5 @@ import { expect } from 'chai' -import { BigNumber, constants, utils } from 'ethers' +import { BigNumber, constants } from 'ethers' import { defaults, deployContract, deployL1Reservoir } from '../lib/deployment' import { ArbitrumL1Mocks, L1FixtureContracts, NetworkFixture } from '../lib/fixtures' @@ -17,7 +17,6 @@ import { formatGRT, Account, RewardsTracker, - provider, } from '../lib/testHelpers' import { L1Reservoir } from '../../build/types/L1Reservoir' import { BridgeEscrow } from '../../build/types/BridgeEscrow' @@ -26,9 +25,9 @@ import path from 'path' import { Artifacts } from 'hardhat/internal/artifacts' import { Interface } from 'ethers/lib/utils' import { L1GraphTokenGateway } from '../../build/types/L1GraphTokenGateway' -import { SubgraphDeploymentID } from '@graphprotocol/common-ts' import { Controller } from '../../build/types/Controller' import { GraphProxyAdmin } from '../../build/types/GraphProxyAdmin' +import { Staking } from '../../build/types/Staking' const ARTIFACTS_PATH = path.resolve('build/contracts') const artifacts = new Artifacts(ARTIFACTS_PATH) const l2ReservoirAbi = artifacts.readArtifactSync('L2Reservoir').abi @@ -45,6 +44,7 @@ const defaultEthValue = maxSubmissionCost.add(maxGas.mul(gasPriceBid)) describe('L1Reservoir', () => { let governor: Account let testAccount1: Account + let testAccount2: Account let mockRouter: Account let mockL2GRT: Account let mockL2Gateway: Account @@ -59,6 +59,7 @@ describe('L1Reservoir', () => { let l1GraphTokenGateway: L1GraphTokenGateway let controller: Controller let proxyAdmin: GraphProxyAdmin + let staking: Staking let supplyBeforeDrip: BigNumber let dripBlock: BigNumber @@ -123,7 +124,7 @@ describe('L1Reservoir', () => { let expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const tx1 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(await latestBlock()).eq(dripBlock) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) @@ -138,7 +139,7 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const newAmount = (await grt.balanceOf(l1Reservoir.address)).sub(actualAmount) expectedNextDeadline = (await latestBlock()).add(dripInterval) const expectedSnapshottedSupply = supplyBeforeDrip.add(await tracker.accRewards()) @@ -152,12 +153,20 @@ describe('L1Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL2GRT, mockL2Gateway, mockL2Reservoir, keeper] = - await getAccounts() + ;[ + governor, + testAccount1, + mockRouter, + mockL2GRT, + mockL2Gateway, + mockL2Reservoir, + keeper, + testAccount2, + ] = await getAccounts() fixture = new NetworkFixture() fixtureContracts = await fixture.load(governor.signer) - ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin } = + ;({ grt, l1Reservoir, bridgeEscrow, l1GraphTokenGateway, controller, proxyAdmin, staking } = fixtureContracts) await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) @@ -171,6 +180,7 @@ describe('L1Reservoir', () => { mockL2Gateway.address, mockL2Reservoir.address, ) + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) reservoirMock = (await deployContract( 'ReservoirMock', governor.signer, @@ -253,7 +263,9 @@ describe('L1Reservoir', () => { await expect(tx).emit(l1Reservoir, 'IssuanceRateStaged').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(0) expect(await l1Reservoir.nextIssuanceRate()).eq(newIssuanceRate) - tx = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + tx = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx).emit(l1Reservoir, 'IssuanceRateUpdated').withArgs(newIssuanceRate) expect(await l1Reservoir.issuanceRate()).eq(newIssuanceRate) }) @@ -319,7 +331,13 @@ describe('L1Reservoir', () => { expect(await l1Reservoir.nextL2RewardsFraction()).eq(newValue) tx = l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) await expect(tx).emit(l1Reservoir, 'L2RewardsFractionUpdated').withArgs(newValue) expect(await l1Reservoir.l2RewardsFraction()).eq(newValue) }) @@ -337,11 +355,74 @@ describe('L1Reservoir', () => { expect(await l1Reservoir.minDripInterval()).eq(newValue) }) }) + describe('allowed drippers whitelist', function () { + it('only allows the governor to add a dripper', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + .grantDripPermission(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('only allows the governor to revoke a dripper', async function () { + const tx = l1Reservoir.connect(testAccount1.signer).revokeDripPermission(keeper.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('allows adding an address to the allowed drippers', async function () { + const tx = l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperAdded').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(true) + }) + it('allows removing an address from the allowed drippers', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir.connect(governor.signer).revokeDripPermission(testAccount1.address) + await expect(tx).emit(l1Reservoir, 'AllowedDripperRevoked').withArgs(testAccount1.address) + expect(await l1Reservoir.allowedDrippers(testAccount1.address)).eq(false) + }) + }) }) // TODO test that rewardsManager.updateAccRewardsPerSignal is called when // issuanceRate or l2RewardsFraction is updated describe('drip', function () { + it('cannot be called by an unauthorized address', async function () { + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).revertedWith('UNAUTHORIZED') + }) + it('can be called by an indexer', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('can be called by a whitelisted address', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) + it('can be called by an indexer operator using an extra parameter', async function () { + const stakedAmount = toGRT('100000') + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).emit(l1Reservoir, 'RewardsDripped') + }) it('mints rewards for the next week', async function () { supplyBeforeDrip = await grt.totalSupply() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) @@ -357,7 +438,7 @@ describe('L1Reservoir', () => { const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) const tx = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const actualAmount = await grt.balanceOf(l1Reservoir.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount)) expect(await l1Reservoir.issuanceBase()).to.eq(supplyBeforeDrip) @@ -381,7 +462,7 @@ describe('L1Reservoir', () => { const tx1 = await l1Reservoir .connect(keeper.signer) - .drip(toBN(0), toBN(0), toBN(0), keeper.address) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) const minInterval = toBN('200') await l1Reservoir.connect(governor.signer).setMinDripInterval(minInterval) @@ -396,23 +477,35 @@ describe('L1Reservoir', () => { .emit(grt, 'Transfer') .withArgs(AddressZero, l1Reservoir.address, actualAmount) - const tx2 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx2).revertedWith('WAIT_FOR_MIN_INTERVAL') // We've had 1 block since the last drip so far, so we jump to one block before the interval is done await advanceBlocks(minInterval.sub(2)) - const tx3 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx3 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx3).revertedWith('WAIT_FOR_MIN_INTERVAL') await advanceBlocks(1) // Now we're over the interval so we can drip again - const tx4 = l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + const tx4 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) await expect(tx4).emit(l1Reservoir, 'RewardsDripped') }) it('prevents locking eth in the contract if l2RewardsFraction is 0', async function () { const tx = l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) await expect(tx).revertedWith('No eth value needed') }) it('mints only a few more tokens if called on the next block', async function () { @@ -445,7 +538,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -483,29 +582,37 @@ describe('L1Reservoir', () => { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) await l1Reservoir.connect(governor.signer).setDripRewardPerBlock(toGRT('3')) await l1Reservoir.connect(governor.signer).setMinDripInterval(toBN('2')) - // lastRewardsUpdateBlock is set to block.number with initialSnapshot - await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) + await advanceBlocks(toBN('4')) - // now we're at lastRewardsUpdateBlock + minDripInterval + 3, so keeper reward should be: - // dripRewardPerBlock * 3 supplyBeforeDrip = await grt.totalSupply() + const issuanceBase = await l1Reservoir.issuanceBase() const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) expect(startAccrued).to.eq(0) const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const expectedKeeperReward = dripBlock + .sub(await l1Reservoir.lastRewardsUpdateBlock()) + .sub(toBN('2')) + .mul(toGRT('3')) const tracker = await RewardsTracker.create( - supplyBeforeDrip, + issuanceBase, defaults.rewards.issuanceRate, dripBlock, ) expect(await tracker.accRewards(dripBlock)).to.eq(0) const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) const expectedMintedRewards = await tracker.accRewards(expectedNextDeadline) - const expectedMintedAmount = expectedMintedRewards.add(toGRT('9')) - const expectedSentToL2 = expectedMintedRewards.div(2) + const expectedMintedAmount = expectedMintedRewards.add(expectedKeeperReward) + const expectedSentToL2 = expectedMintedRewards.div(2).add(expectedKeeperReward) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) @@ -518,15 +625,15 @@ describe('L1Reservoir', () => { .emit(l1Reservoir, 'RewardsDripped') .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) - const normalizedTokenSupply = (await l1Reservoir.tokenSupplyCache()) + const l2IssuanceBase = (await l1Reservoir.issuanceBase()) .mul(await l1Reservoir.l2RewardsFraction()) .div(toGRT('1')) const issuanceRate = await l1Reservoir.issuanceRate() const expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ - normalizedTokenSupply, + l2IssuanceBase, issuanceRate, toBN('0'), - toGRT('9'), // keeper reward + expectedKeeperReward, keeper.address, ]) const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( @@ -557,7 +664,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -608,7 +721,13 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -663,7 +782,13 @@ describe('L1Reservoir', () => { const expectedSentToL2 = expectedMintedAmount.div(2) const tx = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const actualAmount = await grt.balanceOf(l1Reservoir.address) const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) @@ -702,7 +827,6 @@ describe('L1Reservoir', () => { supplyBeforeDrip = await grt.totalSupply() const secondDripBlock = (await latestBlock()).add(1) const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) - const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) // The amount sent to L2 should cover up to the new drip block with the old fraction, @@ -711,7 +835,13 @@ describe('L1Reservoir', () => { const tx2 = await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) const newActualAmount = await grt.balanceOf(l1Reservoir.address) const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) expect(toRound(newActualAmount)).to.eq( @@ -756,7 +886,9 @@ describe('L1Reservoir', () => { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) supplyBeforeDrip = await grt.totalSupply() - await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -828,7 +960,13 @@ describe('L1Reservoir', () => { await l1Reservoir.connect(governor.signer).setL2RewardsFraction(lambda) await l1Reservoir .connect(keeper.signer) - .drip(maxGas, gasPriceBid, maxSubmissionCost, keeper.address, { value: defaultEthValue }) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) supplyBeforeDrip = await l1Reservoir.issuanceBase() // Has been updated accordingly dripBlock = await latestBlock() await advanceBlocks(20) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index 7a002a49d..afffe8cfc 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -122,6 +122,7 @@ describe('Rewards', () => { await grt.connect(wallet.signer).approve(staking.address, toGRT('1000000')) await grt.connect(wallet.signer).approve(curation.address, toGRT('1000000')) } + await l1Reservoir.connect(governor.signer).grantDripPermission(keeper.address) await l1Reservoir.connect(governor.signer).initialSnapshot(toBN(0)) supplyBeforeDrip = await grt.totalSupply() }) @@ -301,7 +302,9 @@ describe('Rewards', () => { beforeEach(async function () { // 5% minute rate (4 blocks) await l1Reservoir.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) - await l1Reservoir.connect(keeper.signer).drip(toBN(0), toBN(0), toBN(0), keeper.address) + await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), keeper.address) dripBlock = await latestBlock() }) @@ -682,18 +685,15 @@ describe('Rewards', () => { describe('takeRewards', function () { it('should distribute rewards on closed allocation and stake', async function () { // Align with the epoch boundary - // dripBlock (81) await epochManager.setEpochLength(10) - // dripBlock + 1 await advanceToNextEpoch(epochManager) - // dripBlock + 4 + // Setup await setupIndexerAllocation() const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) - // dripBlock + 7 + // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -872,7 +872,7 @@ describe('Rewards', () => { // Jump await advanceToNextEpoch(epochManager) - // dripBlock + 14 + // dripBlock + 13 // Before state const beforeTokenSupply = await grt.totalSupply() @@ -914,17 +914,23 @@ describe('Rewards', () => { it('should deny and burn rewards if subgraph on denylist', async function () { // Setup + // dripBlock (82) await epochManager.setEpochLength(10) + // dripBlock + 1 await rewardsManager .connect(governor.signer) .setSubgraphAvailabilityOracle(governor.address) + // dripBlock + 2 await rewardsManager.connect(governor.signer).setDenied(subgraphDeploymentID1, true) + // dripBlock + 3 (epoch boundary!) await advanceToNextEpoch(epochManager) + // dripBlock + 13 await setupIndexerAllocation() const firstSnapshotBlocks = new BN((await latestBlock()).sub(dripBlock).toString()) // Jump await advanceToNextEpoch(epochManager) + // dripBlock + 23 const supplyBefore = await grt.totalSupply() // Close allocation. At this point rewards should be collected for that indexer From 51db5f9123fb611905358d90c96f93ba33163fbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 23 Jun 2022 17:54:23 +0300 Subject: [PATCH 29/47] test: add a test for the keeper reward delivery in L2 --- test/l2/l2Reservoir.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 55c339c60..bfa7026a8 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -106,6 +106,7 @@ describe('L2Reservoir', () => { const validGatewayFinalizeTransfer = async ( callhookData: string, + keeperReward = toGRT('0'), ): Promise => { const tx = await gatewayFinalizeTransfer(callhookData) await expect(tx) @@ -116,7 +117,7 @@ describe('L2Reservoir', () => { // newly minted GRT const receiverBalance = await grt.balanceOf(l2Reservoir.address) - await expect(receiverBalance).eq(dripAmount) + await expect(receiverBalance).eq(dripAmount.sub(keeperReward)) return tx } @@ -211,6 +212,26 @@ describe('L2Reservoir', () => { await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) }) + it('delivers the keeper reward to the beneficiary address', async function () { + normalizedSupply = dripNormalizedSupply + const reward = toBN('15') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, reward) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) + }) it('updates the normalized supply cache and issuance rate', async function () { normalizedSupply = dripNormalizedSupply let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( From a54a629c47ce8cd5d95967cc062cb9aa173f476c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 27 Jun 2022 15:31:39 +0300 Subject: [PATCH 30/47] fix: provide part of the keeper reward to L2 redeemer --- contracts/l2/reservoir/L2Reservoir.sol | 58 +++++++++++++-- contracts/l2/reservoir/L2ReservoirStorage.sol | 2 + test/l2/l2Reservoir.test.ts | 73 ++++++++++++++++++- 3 files changed, 123 insertions(+), 10 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 96a344f67..8813ab73a 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -4,12 +4,22 @@ pragma solidity ^0.7.6; pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; +import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; +interface IArbTxWithRedeemer { + /** + * @notice Gets the redeemer of the current retryable redeem attempt. + * Returns the zero address if the current transaction is not a retryable redeem attempt. + * If this is an auto-redeem, returns the fee refund address of the retryable. + */ + function getCurrentRedeemer() external view returns (address); +} + /** * @title L2 Rewards Reservoir * @dev This contract acts as a reservoir/vault for the rewards to be distributed on Layer 2. @@ -19,8 +29,12 @@ import "./L2ReservoirStorage.sol"; contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; + address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; + event DripReceived(uint256 _issuanceBase); event NextDripNonceUpdated(uint256 _nonce); + event L1ReservoirAddressUpdated(address _l1ReservoirAddress); + event L2KeeperRewardFractionUpdated(uint256 _l2KeeperRewardFraction); /** * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. @@ -52,6 +66,29 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { emit NextDripNonceUpdated(_nonce); } + /** + * @dev Sets the L1 Reservoir address + * This is the address on L1 that will appear as redeemer when a ticket + * was auto-redeemed. + * @param _l1ReservoirAddress New address for the L1Reservoir on L1 + */ + function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { + l1ReservoirAddress = _l1ReservoirAddress; + emit L1ReservoirAddressUpdated(_l1ReservoirAddress); + } + + /** + * @dev Sets the L2 keeper reward fraction + * This is the fraction of the keeper reward that will be sent to the redeemer on L2 + * if the retryable ticket is not auto-redeemed + * @param _l2KeeperRewardFraction New value for the fraction, with fixed point at 1e18 + */ + function setL2KeeperRewardFraction(uint256 _l2KeeperRewardFraction) external onlyGovernor { + require(_l2KeeperRewardFraction <= FIXED_POINT_SCALING_FACTOR, "INVALID_VALUE"); + l2KeeperRewardFraction = _l2KeeperRewardFraction; + emit L2KeeperRewardFractionUpdated(_l2KeeperRewardFraction); + } + /** * @dev Get new total rewards accumulated since the last drip. * This is deltaR = p * r ^ (blocknum - t0) - p, where: @@ -115,14 +152,19 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { } issuanceBase = _issuanceBase; IGraphToken grt = graphToken(); - // We'd like to reward the keeper that redeemed the tx in L2 - // but this won't work right now as tx.origin will actually be the L1 sender. - // uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(TOKEN_DECIMALS); - // // solhint-disable-next-line avoid-tx-origin - // grt.transfer(tx.origin, _l2KeeperReward); - // grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); - // So for now we just send all the rewards to teh L1 keeper: - grt.transfer(_l1Keeper, _keeperReward); + + // Part of the reward always goes to whoever redeemed the ticket in L2, + // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir + address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); + if (redeemer != l1ReservoirAddress) { + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(FIXED_POINT_SCALING_FACTOR); + grt.transfer(redeemer, _l2KeeperReward); + grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); + } else { + // In an auto-redeem, we just send all the rewards to teh L1 keeper: + grt.transfer(_l1Keeper, _keeperReward); + } + emit DripReceived(issuanceBase); } diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index 4e8c6825d..28c562901 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -13,4 +13,6 @@ contract L2ReservoirV1Storage { contract L2ReservoirV2Storage is L2ReservoirV1Storage { // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) uint256 public l2KeeperRewardFraction; + // Address of the L1Reservoir on L1, used to check if a ticket was auto-redeemed + address public l1ReservoirAddress; } diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index bfa7026a8..0551c1e13 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -4,6 +4,7 @@ import { BigNumber, constants, ContractTransaction, utils } from 'ethers' import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' import { BigNumber as BN } from 'bignumber.js' +import { FakeContract, smock } from '@defi-wonderland/smock' import { advanceBlocks, @@ -30,11 +31,13 @@ const dripIssuanceRate = toBN('1000000023206889619') describe('L2Reservoir', () => { let governor: Account let testAccount1: Account + let testAccount2: Account let mockRouter: Account let mockL1GRT: Account let mockL1Gateway: Account let mockL1Reservoir: Account let fixture: NetworkFixture + let arbTxMock: FakeContract let grt: L2GraphToken let l2Reservoir: L2Reservoir @@ -122,7 +125,7 @@ describe('L2Reservoir', () => { } before(async function () { - ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir] = + ;[governor, testAccount1, mockRouter, mockL1GRT, mockL1Gateway, mockL1Reservoir, testAccount2] = await getAccounts() fixture = new NetworkFixture() @@ -136,6 +139,11 @@ describe('L2Reservoir', () => { mockL1Gateway.address, mockL1Reservoir.address, ) + + arbTxMock = await smock.fake('IArbTxWithRedeemer', { + address: '0x000000000000000000000000000000000000006E', + }) + arbTxMock.getCurrentRedeemer.returns(mockL1Reservoir.address) }) beforeEach(async function () { @@ -157,7 +165,42 @@ describe('L2Reservoir', () => { await expect(await l2Reservoir.nextDripNonce()).to.eq(toBN('10')) }) }) + + describe('setL1ReservoirAddress', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir + .connect(testAccount1.signer) + .setL1ReservoirAddress(testAccount1.address) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) + await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) + await expect(await l2Reservoir.l1ReservoirAddress()).to.eq(testAccount1.address) + }) + }) + + describe('setL2KeeperRewardFraction', async function () { + it('rejects unauthorized calls', async function () { + const tx = l2Reservoir.connect(testAccount1.signer).setL2KeeperRewardFraction(toBN(1)) + await expect(tx).revertedWith('Caller must be Controller governor') + }) + it('rejects invalid values (> 1)', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('1.000001')) + await expect(tx).revertedWith('INVALID_VALUE') + }) + it('sets the L1Reservoir address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.999')) + await expect(tx).emit(l2Reservoir, 'L2KeeperRewardFractionUpdated').withArgs(toGRT('0.999')) + await expect(await l2Reservoir.l2KeeperRewardFraction()).to.eq(toGRT('0.999')) + }) + }) + describe('receiveDrip', async function () { + beforeEach(async function () { + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.2')) + await l2Reservoir.connect(governor.signer).setL1ReservoirAddress(mockL1Reservoir.address) + }) it('rejects the call when not called by the gateway', async function () { const tx = l2Reservoir .connect(governor.signer) @@ -224,7 +267,7 @@ describe('L2Reservoir', () => { ) const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) dripBlock = await latestBlock() - await expect(await l2Reservoir.normalizedTokenSupplyCache()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) await expect(tx) @@ -232,6 +275,32 @@ describe('L2Reservoir', () => { .withArgs(l2Reservoir.address, testAccount1.address, reward) await expect(await grt.balanceOf(testAccount1.address)).to.eq(reward) }) + it('delivers part of the keeper reward to the L2 redeemer', async function () { + arbTxMock.getCurrentRedeemer.returns(testAccount2.address) + await l2Reservoir.connect(governor.signer).setL2KeeperRewardFraction(toGRT('0.25')) + normalizedSupply = dripNormalizedSupply + const reward = toGRT('16') + const receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( + dripNormalizedSupply, + dripIssuanceRate, + toBN('0'), + reward, + testAccount1.address, + ) + const tx = await validGatewayFinalizeTransfer(receiveDripTx.data, reward) + dripBlock = await latestBlock() + await expect(await l2Reservoir.issuanceBase()).to.eq(dripNormalizedSupply) + await expect(await l2Reservoir.issuanceRate()).to.eq(dripIssuanceRate) + await expect(tx).emit(l2Reservoir, 'DripReceived').withArgs(dripNormalizedSupply) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount1.address, toGRT('12')) + await expect(tx) + .emit(grt, 'Transfer') + .withArgs(l2Reservoir.address, testAccount2.address, toGRT('4')) + await expect(await grt.balanceOf(testAccount1.address)).to.eq(toGRT('12')) + await expect(await grt.balanceOf(testAccount2.address)).to.eq(toGRT('4')) + }) it('updates the normalized supply cache and issuance rate', async function () { normalizedSupply = dripNormalizedSupply let receiveDripTx = await l2Reservoir.populateTransaction.receiveDrip( From e27590846c666c057532403f2debf3f851a90bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:20:52 +0200 Subject: [PATCH 31/47] fix: clean up comments about redeemer --- contracts/l2/reservoir/IL2Reservoir.sol | 3 ++- contracts/l2/reservoir/L2Reservoir.sol | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 5fd2fc861..696e55797 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -22,7 +22,8 @@ interface IL2Reservoir is IReservoir { * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (tx.origin) + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8813ab73a..e52507f34 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -126,7 +126,8 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * because it checks an incrementing nonce. If that is the case, the retryable ticket can be redeemed * again once the ticket for previous drip has been redeemed. * A keeper reward will be sent to the keeper that dripped on L1, and part of it - * to whoever redeemed the current retryable ticket (tx.origin) + * to whoever redeemed the current retryable ticket (as reported by ArbRetryableTx.getCurrentRedeemer) if + * the ticket is not auto-redeemed. * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order @@ -157,7 +158,9 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); if (redeemer != l1ReservoirAddress) { - uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div(FIXED_POINT_SCALING_FACTOR); + uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( + FIXED_POINT_SCALING_FACTOR + ); grt.transfer(redeemer, _l2KeeperReward); grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); } else { From 6d0c39d4ec7ffdb4cc61e96d82889d078f32af61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 13:28:46 +0200 Subject: [PATCH 32/47] fix: more documentation details --- contracts/l2/reservoir/L2Reservoir.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index e52507f34..16f60e9e4 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -31,10 +31,10 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; - event DripReceived(uint256 _issuanceBase); - event NextDripNonceUpdated(uint256 _nonce); - event L1ReservoirAddressUpdated(address _l1ReservoirAddress); - event L2KeeperRewardFractionUpdated(uint256 _l2KeeperRewardFraction); + event DripReceived(uint256 issuanceBase); + event NextDripNonceUpdated(uint256 nonce); + event L1ReservoirAddressUpdated(address l1ReservoirAddress); + event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); /** * @dev Checks that the sender is the L2GraphTokenGateway as configured on the Controller. @@ -50,6 +50,10 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * are not set here because they are set from L1 through the drip function. * The RewardsManager's address might also not be available in the controller at initialization * time, so approveRewardsManager() must be called separately. + * The l1ReservoirAddress must also be set separately through setL1ReservoirAddress + * for the same reason. + * In the same vein, the l2KeeperRewardFraction is assumed to be zero at initialization, + * so it must be set through setL2KeeperRewardFraction. * @param _controller Address of the Controller that manages this contract */ function initialize(address _controller) external onlyImpl { From e7271334306fffa336e26dd4003ef0c7532bac8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:02:09 +0200 Subject: [PATCH 33/47] fix: use safe math for minDripInterval --- contracts/l2/reservoir/IL2Reservoir.sol | 2 +- contracts/l2/reservoir/L2Reservoir.sol | 4 ++-- contracts/reservoir/L1Reservoir.sol | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index 696e55797..b8a2311c9 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -27,7 +27,7 @@ interface IL2Reservoir is IReservoir { * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 16f60e9e4..8803fac68 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -135,7 +135,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * @param _issuanceBase Base value for token issuance (approximation for token supply times L2 rewards fraction) * @param _issuanceRate Rewards issuance rate, using fixed point at 1e18, and including a +1 * @param _nonce Incrementing nonce to ensure messages are received in order - * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx + * @param _keeperReward Keeper reward to distribute between keeper that called drip and keeper that redeemed the retryable tx * @param _l1Keeper Address of the keeper that called drip in L1 */ function receiveDrip( @@ -168,7 +168,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { grt.transfer(redeemer, _l2KeeperReward); grt.transfer(_l1Keeper, _keeperReward.sub(_l2KeeperReward)); } else { - // In an auto-redeem, we just send all the rewards to teh L1 keeper: + // In an auto-redeem, we just send all the rewards to the L1 keeper: grt.transfer(_l1Keeper, _keeperReward); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 30fa4d82a..807b56914 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -73,6 +73,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * to the drip function, that also requires the initial supply snapshot to be taken * using initialSnapshot. For this reason, issuanceRate and l2RewardsFraction * are not initialized here and instead need a call to setIssuanceRate and setL2RewardsFraction. + * The same applies to minDripInterval (set through setMinDripInterval) and dripRewardPerBlock + * (set through setDripRewardPerBlock). * On the other hand, the l2ReservoirAddress is not expected to be known at initialization * time and must therefore be set using setL2ReservoirAddress. * The RewardsManager's address might also not be available in the controller at initialization @@ -292,7 +294,10 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { uint256 _l2MaxSubmissionCost, address _keeperRewardBeneficiary ) private { - require(block.number > lastRewardsUpdateBlock + minDripInterval, "WAIT_FOR_MIN_INTERVAL"); + require( + block.number > lastRewardsUpdateBlock.add(minDripInterval), + "WAIT_FOR_MIN_INTERVAL" + ); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); From c4d583a618c868ab48a4f966db3f78699313c464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 15 Jul 2022 14:46:21 +0200 Subject: [PATCH 34/47] fix: validate input when granting/revoking drip permission --- contracts/reservoir/L1Reservoir.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 807b56914..d9fb22f0e 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -170,6 +170,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _dripper Address that will be an allowed dripper */ function grantDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(!allowedDrippers[_dripper], "ALREADY_A_DRIPPER"); allowedDrippers[_dripper] = true; emit AllowedDripperAdded(_dripper); } @@ -179,6 +181,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _dripper Address that will not be an allowed dripper anymore */ function revokeDripPermission(address _dripper) external onlyGovernor { + require(_dripper != address(0), "INVALID_ADDRESS"); + require(allowedDrippers[_dripper], "NOT_A_DRIPPER"); allowedDrippers[_dripper] = false; emit AllowedDripperRevoked(_dripper); } From 304d055ea4d452a0e92c0eebe50f45f093fdd2f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 28 Jul 2022 16:09:49 +0200 Subject: [PATCH 35/47] fix: docs and inheritance for IArbTxWithRedeemer --- contracts/l2/reservoir/L2Reservoir.sol | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 8803fac68..839362ccd 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -11,7 +11,12 @@ import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; import "./L2ReservoirStorage.sol"; -interface IArbTxWithRedeemer { +/** + * @dev ArbRetryableTx with additional interface to query the current redeemer. + * This is being added by the Arbitrum team but hasn't made it into the arbos-precompiles + * package yet. + */ +interface IArbTxWithRedeemer is ArbRetryableTx { /** * @notice Gets the redeemer of the current retryable redeem attempt. * Returns the zero address if the current transaction is not a retryable redeem attempt. From a570c8c7eaeffb185c90bc1082c38c57e6801ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Fri, 5 Aug 2022 14:17:51 +0200 Subject: [PATCH 36/47] fix: remove minDripInterval from the drip keeper reward calculation [L-01] --- contracts/reservoir/L1Reservoir.sol | 4 +--- test/reservoir/l1Reservoir.test.ts | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index d9fb22f0e..0e6fc5b47 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -307,9 +307,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { uint256 mintedRewardsActual = getNewGlobalRewards(block.number); // eps = (signed int) mintedRewardsTotal - mintedRewardsActual - uint256 keeperReward = dripRewardPerBlock.mul( - block.number.sub(lastRewardsUpdateBlock).sub(minDripInterval) - ); + uint256 keeperReward = dripRewardPerBlock.mul(block.number.sub(lastRewardsUpdateBlock)); if (nextIssuanceRate != issuanceRate) { rewardsManager().updateAccRewardsPerSignal(); snapshotAccumulatedRewards(mintedRewardsActual); // This updates lastRewardsUpdateBlock diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 9c272c847..0e5fdf415 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -592,7 +592,6 @@ describe('L1Reservoir', () => { const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction const expectedKeeperReward = dripBlock .sub(await l1Reservoir.lastRewardsUpdateBlock()) - .sub(toBN('2')) .mul(toGRT('3')) const tracker = await RewardsTracker.create( issuanceBase, From 24328623a1f60d8b8d4c813bbd9bef314af0e675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 8 Aug 2022 16:48:46 +0200 Subject: [PATCH 37/47] fix: use L2 alias of l1ReservoirAddress when comparing getCurrentRedeemer [H-01] --- contracts/l2/reservoir/L2Reservoir.sol | 3 ++- test/l2/l2Reservoir.test.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 839362ccd..2b8cd80fe 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -6,6 +6,7 @@ pragma abicoder v2; import "@openzeppelin/contracts/math/SafeMath.sol"; import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; +import "../../arbitrum/AddressAliasHelper.sol"; import "../../reservoir/IReservoir.sol"; import "../../reservoir/Reservoir.sol"; import "./IL2Reservoir.sol"; @@ -166,7 +167,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { // Part of the reward always goes to whoever redeemed the ticket in L2, // unless this was an autoredeem, in which case the "redeemer" is the sender, i.e. L1Reservoir address redeemer = IArbTxWithRedeemer(ARB_TX_ADDRESS).getCurrentRedeemer(); - if (redeemer != l1ReservoirAddress) { + if (redeemer != AddressAliasHelper.applyL1ToL2Alias(l1ReservoirAddress)) { uint256 _l2KeeperReward = _keeperReward.mul(l2KeeperRewardFraction).div( FIXED_POINT_SCALING_FACTOR ); diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 0551c1e13..b7cfd2101 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -16,6 +16,7 @@ import { Account, RewardsTracker, getL2SignerFromL1, + applyL1ToL2Alias, } from '../lib/testHelpers' import { L2Reservoir } from '../../build/types/L2Reservoir' @@ -143,7 +144,7 @@ describe('L2Reservoir', () => { arbTxMock = await smock.fake('IArbTxWithRedeemer', { address: '0x000000000000000000000000000000000000006E', }) - arbTxMock.getCurrentRedeemer.returns(mockL1Reservoir.address) + arbTxMock.getCurrentRedeemer.returns(applyL1ToL2Alias(mockL1Reservoir.address)) }) beforeEach(async function () { From f1c95304d71751a6064b72bb692cee070ebd3970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Wed, 17 Aug 2022 13:30:58 +0200 Subject: [PATCH 38/47] fix: don't include keeper reward twice when computing what to send to L2 [H-03] [L-03] --- contracts/reservoir/L1Reservoir.sol | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 0e6fc5b47..8f62fb951 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -321,22 +321,18 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // n = deltaR(t1, t0) uint256 newRewardsToDistribute = getNewGlobalRewards(rewardsMintedUntilBlock); // N = n - eps - uint256 tokensToMint; + uint256 rewardsTokensToMint; { - uint256 newRewardsPlusMintedActual = newRewardsToDistribute - .add(mintedRewardsActual) - .add(keeperReward); + uint256 newRewardsPlusMintedActual = newRewardsToDistribute.add(mintedRewardsActual); require( - newRewardsPlusMintedActual >= mintedRewardsTotal, - "Would mint negative tokens, wait before calling again" + newRewardsPlusMintedActual > mintedRewardsTotal, + "Would mint negative or zero tokens, wait before calling again" ); - tokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); + rewardsTokensToMint = newRewardsPlusMintedActual.sub(mintedRewardsTotal); } IGraphToken grt = graphToken(); - if (tokensToMint > 0) { - grt.mint(address(this), tokensToMint); - } + grt.mint(address(this), rewardsTokensToMint.add(keeperReward)); uint256 tokensToSendToL2 = 0; if (l2RewardsFraction != nextL2RewardsFraction) { @@ -376,7 +372,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { _keeperRewardBeneficiary ); } else if (l2RewardsFraction > 0) { - tokensToSendToL2 = tokensToMint + tokensToSendToL2 = rewardsTokensToMint .mul(l2RewardsFraction) .div(FIXED_POINT_SCALING_FACTOR) .add(keeperReward); @@ -395,7 +391,11 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // If we don't send rewards to L2, pay the keeper reward in L1 grt.transfer(_keeperRewardBeneficiary, keeperReward); } - emit RewardsDripped(tokensToMint, tokensToSendToL2, rewardsMintedUntilBlock); + emit RewardsDripped( + rewardsTokensToMint.add(keeperReward), + tokensToSendToL2, + rewardsMintedUntilBlock + ); } /** From 26a49222a58464ad2a38c1e214ada0688f753649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 15:39:09 +0200 Subject: [PATCH 39/47] test: add test to ensure no DoS if l2RewardsFraction is zeroed [H-04] --- test/reservoir/l1Reservoir.test.ts | 139 +++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 0e5fdf415..64eedbf36 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -878,6 +878,145 @@ describe('L1Reservoir', () => { expectedNewNextDeadline, ) }) + + it('reverts for a while but can be called again later if L2 fraction goes to zero', async function () { + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0.5')) + + // First drip call, sending half the rewards to L2 + supplyBeforeDrip = await grt.totalSupply() + const startAccrued = await l1Reservoir.getAccumulatedRewards(await latestBlock()) + expect(startAccrued).to.eq(0) + const dripBlock = (await latestBlock()).add(1) // We're gonna drip in the next transaction + const tracker = await RewardsTracker.create( + supplyBeforeDrip, + defaults.rewards.issuanceRate, + dripBlock, + ) + expect(await tracker.accRewards(dripBlock)).to.eq(0) + const expectedNextDeadline = dripBlock.add(defaults.rewards.dripInterval) + const expectedMintedAmount = await tracker.accRewards(expectedNextDeadline) + const expectedSentToL2 = expectedMintedAmount.div(2) + const tx = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const actualAmount = await grt.balanceOf(l1Reservoir.address) + const escrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(actualAmount)).to.eq(toRound(expectedMintedAmount.sub(expectedSentToL2))) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedMintedAmount), + ) + expect(toRound(escrowedAmount)).to.eq(toRound(expectedSentToL2)) + await expect(tx) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs(actualAmount.add(escrowedAmount), escrowedAmount, expectedNextDeadline) + + let l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + const issuanceRate = await l1Reservoir.issuanceRate() + let expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('0'), + toBN('0'), + keeper.address, + ]) + let expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + escrowedAmount, + expectedCallhookData, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(1), expectedL2Data) + + await tracker.snapshotRewards() + + await l1Reservoir.connect(governor.signer).setL2RewardsFraction(toGRT('0')) + + // Second attempt to drip immediately afterwards will revert, because we + // would have to send negative tokens to L2 to compensate + const tx2 = l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + await expect(tx2).revertedWith( + 'Negative amount would be sent to L2, wait before calling again', + ) + + await advanceBlocks(await l1Reservoir.dripInterval()) + + // Now we should be able to drip again, and a small amount will be sent to L2 + // to cover the few blocks since the drip interval was over + supplyBeforeDrip = await grt.totalSupply() + const secondDripBlock = (await latestBlock()).add(1) + const expectedNewNextDeadline = secondDripBlock.add(defaults.rewards.dripInterval) + const rewardsUntilSecondDripBlock = await tracker.accRewards(secondDripBlock) + const expectedTotalRewards = await tracker.accRewards(expectedNewNextDeadline) + const expectedNewMintedAmount = expectedTotalRewards.sub(expectedMintedAmount) + // The amount sent to L2 should cover up to the new drip block with the old fraction, + // and from then onwards with the new fraction, that is zero + const expectedNewTotalSentToL2 = rewardsUntilSecondDripBlock.div(2) + + const tx3 = await l1Reservoir + .connect(keeper.signer) + ['drip(uint256,uint256,uint256,address)']( + maxGas, + gasPriceBid, + maxSubmissionCost, + keeper.address, + { value: defaultEthValue }, + ) + const newActualAmount = await grt.balanceOf(l1Reservoir.address) + const newEscrowedAmount = await grt.balanceOf(bridgeEscrow.address) + expect(toRound(newActualAmount)).to.eq( + toRound(expectedTotalRewards.sub(expectedNewTotalSentToL2)), + ) + expect(toRound((await grt.totalSupply()).sub(supplyBeforeDrip))).to.eq( + toRound(expectedNewMintedAmount), + ) + expect(toRound(newEscrowedAmount)).to.eq(toRound(expectedNewTotalSentToL2)) + l2IssuanceBase = (await l1Reservoir.issuanceBase()) + .mul(await l1Reservoir.l2RewardsFraction()) + .div(toGRT('1')) + expectedCallhookData = l2ReservoirIface.encodeFunctionData('receiveDrip', [ + l2IssuanceBase, + issuanceRate, + toBN('1'), // Incremented nonce + toBN('0'), + keeper.address, + ]) + expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + l1Reservoir.address, + mockL2Reservoir.address, + newEscrowedAmount.sub(escrowedAmount), + expectedCallhookData, + ) + await expect(tx3) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(l1Reservoir.address, mockL2Gateway.address, toBN(2), expectedL2Data) + await expect(tx3) + .emit(l1Reservoir, 'RewardsDripped') + .withArgs( + newActualAmount.add(newEscrowedAmount).sub(actualAmount.add(escrowedAmount)), + newEscrowedAmount.sub(escrowedAmount), + expectedNewNextDeadline, + ) + }) }) context('calculating rewards', async function () { From 8869e69d57cb3a037aac62791c06259b6f3b852b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 15:40:10 +0200 Subject: [PATCH 40/47] test: optimize functions to advance blocks and fix some race conditions --- test/lib/testHelpers.ts | 2 +- test/rewards/rewards.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/lib/testHelpers.ts b/test/lib/testHelpers.ts index 60d48dccf..998d0d335 100644 --- a/test/lib/testHelpers.ts +++ b/test/lib/testHelpers.ts @@ -178,7 +178,7 @@ export class RewardsTracker { async snapshotPerSignal(totalSignal: BigNumber, atBlock?: BigNumber): Promise { this.accumulatedPerSignal = await this.accRewardsPerSignal(totalSignal, atBlock) this.accumulatedAtLastPerSignalUpdatedBlock = await this.accRewards(atBlock) - this.lastPerSignalUpdatedBlock = atBlock + this.lastPerSignalUpdatedBlock = atBlock || (await latestBlock()) return this.accumulatedPerSignal } diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index afffe8cfc..40ed21bf5 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -79,7 +79,7 @@ describe('Rewards', () => { ) => { // -- t0 -- const tracker = await RewardsTracker.create(initialSupply, ISSUANCE_RATE_PER_BLOCK, dripBlock) - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) // Jump await advanceBlocks(nBlocks) @@ -353,11 +353,11 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, // so we snapshot using the previous value - tracker.snapshotPerSignal(prevSignal) + await tracker.snapshotPerSignal(prevSignal) // Update await rewardsManager.updateAccRewardsPerSignal() - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() @@ -382,14 +382,14 @@ describe('Rewards', () => { await curation.connect(curator1.signer).mint(subgraphDeploymentID1, toGRT('1000'), 0) // Minting signal triggers onSubgraphSignalUpgrade before pulling the GRT, // so we snapshot using the previous value - tracker.snapshotPerSignal(prevSignal) + await tracker.snapshotPerSignal(prevSignal) // Jump await advanceBlocks(ISSUANCE_RATE_PERIODS) // Update await rewardsManager.updateAccRewardsPerSignal() - tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) + await tracker.snapshotPerSignal(await grt.balanceOf(curation.address)) const contractAccrued = await rewardsManager.accRewardsPerSignal() const blockNum = await latestBlock() From 7207a2362b5756645b1d252cc1168e4c3461f5d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 16:25:19 +0200 Subject: [PATCH 41/47] fix: add some missing validation on reservoirs [M-01] --- contracts/l2/reservoir/L2Reservoir.sol | 1 + contracts/reservoir/L1Reservoir.sol | 4 ++++ test/l2/l2Reservoir.test.ts | 4 ++++ test/reservoir/l1Reservoir.test.ts | 20 +++++++++++++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 2b8cd80fe..1d6dddf2e 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -83,6 +83,7 @@ contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { * @param _l1ReservoirAddress New address for the L1Reservoir on L1 */ function setL1ReservoirAddress(address _l1ReservoirAddress) external onlyGovernor { + require(_l1ReservoirAddress != address(0), "INVALID_L1_RESERVOIR"); l1ReservoirAddress = _l1ReservoirAddress; emit L1ReservoirAddressUpdated(_l1ReservoirAddress); } diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 8f62fb951..aa25f308d 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -150,6 +150,7 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _minDripInterval Minimum number of blocks since last drip for drip to be allowed */ function setMinDripInterval(uint256 _minDripInterval) external onlyGovernor { + require(_minDripInterval < dripInterval, "MUST_BE_LT_DRIP_INTERVAL"); minDripInterval = _minDripInterval; emit MinDripIntervalUpdated(_minDripInterval); } @@ -302,6 +303,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { block.number > lastRewardsUpdateBlock.add(minDripInterval), "WAIT_FOR_MIN_INTERVAL" ); + // Note we only validate that the beneficiary is nonzero, as the caller might + // want to send the reward to an address that is different to the indexer/dripper's address. + require(_keeperRewardBeneficiary != address(0), "INVALID_BENEFICIARY"); uint256 mintedRewardsTotal = getNewGlobalRewards(rewardsMintedUntilBlock); uint256 mintedRewardsActual = getNewGlobalRewards(block.number); diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index b7cfd2101..26a9ccc75 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -174,6 +174,10 @@ describe('L2Reservoir', () => { .setL1ReservoirAddress(testAccount1.address) await expect(tx).revertedWith('Caller must be Controller governor') }) + it('rejects setting a zero address', async function () { + const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(constants.AddressZero) + await expect(tx).revertedWith('INVALID_L1_RESERVOIR') + }) it('sets the L1Reservoir address', async function () { const tx = l2Reservoir.connect(governor.signer).setL1ReservoirAddress(testAccount1.address) await expect(tx).emit(l2Reservoir, 'L1ReservoirAddressUpdated').withArgs(testAccount1.address) diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 64eedbf36..2ea01ac6d 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -347,7 +347,18 @@ describe('L1Reservoir', () => { const tx = l1Reservoir.connect(testAccount1.signer).setMinDripInterval(toBN('200')) await expect(tx).revertedWith('Caller must be Controller governor') }) - + it('rejects setting minimum drip interval if equal to dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval(await l1Reservoir.dripInterval()) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) + it('rejects setting minimum drip interval if larger than dripInterval', async function () { + const tx = l1Reservoir + .connect(governor.signer) + .setMinDripInterval((await l1Reservoir.dripInterval()).add(1)) + await expect(tx).revertedWith('MUST_BE_LT_DRIP_INTERVAL') + }) it('sets the minimum drip interval', async function () { const newValue = toBN('200') const tx = l1Reservoir.connect(governor.signer).setMinDripInterval(newValue) @@ -406,6 +417,13 @@ describe('L1Reservoir', () => { ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), testAccount1.address) await expect(tx).emit(l1Reservoir, 'RewardsDripped') }) + it('cannot be called with a zero address for the keeper reward beneficiary', async function () { + await l1Reservoir.connect(governor.signer).grantDripPermission(testAccount1.address) + const tx = l1Reservoir + .connect(testAccount1.signer) + ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) + await expect(tx).revertedWith('INVALID_BENEFICIARY') + }) it('can be called by an indexer operator using an extra parameter', async function () { const stakedAmount = toGRT('100000') await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) From 117cb4a13dd07f833226b7fc31a6302ba63d2519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 17:59:10 +0200 Subject: [PATCH 42/47] fix: add some missing docstrings [L-04] --- contracts/l2/reservoir/L2Reservoir.sol | 5 +++++ contracts/l2/reservoir/L2ReservoirStorage.sol | 6 +++++- contracts/reservoir/L1ReservoirStorage.sol | 6 +++++- contracts/reservoir/Reservoir.sol | 2 ++ contracts/reservoir/ReservoirStorage.sol | 2 +- 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 1d6dddf2e..9a341b6b5 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -35,11 +35,16 @@ interface IArbTxWithRedeemer is ArbRetryableTx { contract L2Reservoir is L2ReservoirV2Storage, Reservoir, IL2Reservoir { using SafeMath for uint256; + // Address for the ArbRetryableTx interface provided by Arbitrum address public constant ARB_TX_ADDRESS = 0x000000000000000000000000000000000000006E; + // Emitted when a rewards drip is received from L1 event DripReceived(uint256 issuanceBase); + // Emitted when the next drip nonce is manually updated by governance event NextDripNonceUpdated(uint256 nonce); + // Emitted when the L1Reservoir's address is updated event L1ReservoirAddressUpdated(address l1ReservoirAddress); + // Emitted when the L2 keeper reward fraction is updated event L2KeeperRewardFractionUpdated(uint256 l2KeeperRewardFraction); /** diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol index 28c562901..4d469f889 100644 --- a/contracts/l2/reservoir/L2ReservoirStorage.sol +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -3,13 +3,17 @@ pragma solidity ^0.7.6; /** - * @dev Storage variables for the L2Reservoir + * @dev Storage variables for the L2Reservoir, version 1 */ contract L2ReservoirV1Storage { // Expected nonce value for the next drip hook uint256 public nextDripNonce; } +/** + * @dev Storage variables for the L2Reservoir, version 2 + * This version adds some variables needed when introducing the keeper reward. + */ contract L2ReservoirV2Storage is L2ReservoirV1Storage { // Fraction of the keeper reward to send to the retryable tx redeemer in L2 (fixed point 1e18) uint256 public l2KeeperRewardFraction; diff --git a/contracts/reservoir/L1ReservoirStorage.sol b/contracts/reservoir/L1ReservoirStorage.sol index 92b9d5107..9f60249bd 100644 --- a/contracts/reservoir/L1ReservoirStorage.sol +++ b/contracts/reservoir/L1ReservoirStorage.sol @@ -3,7 +3,7 @@ pragma solidity ^0.7.6; /** - * @dev Storage variables for the L1Reservoir + * @dev Storage variables for the L1Reservoir, version 1 */ contract L1ReservoirV1Storage { // Fraction of total rewards to be sent by L2, expressed in fixed point at 1e18 @@ -22,6 +22,10 @@ contract L1ReservoirV1Storage { uint256 public nextDripNonce; } +/** + * @dev Storage variables for the L1Reservoir, version 2 + * This version adds some variables that are needed when introducing keeper rewards. + */ contract L1ReservoirV2Storage is L1ReservoirV1Storage { // Minimum number of blocks since last drip for a new drip to be allowed uint256 public minDripInterval; diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol index d2a0bf6cb..fff7f108a 100644 --- a/contracts/reservoir/Reservoir.sol +++ b/contracts/reservoir/Reservoir.sol @@ -19,7 +19,9 @@ import "./IReservoir.sol"; abstract contract Reservoir is GraphUpgradeable, ReservoirV1Storage, IReservoir { using SafeMath for uint256; + // Scaling factor for all fixed point arithmetics uint256 internal constant FIXED_POINT_SCALING_FACTOR = 1e18; + // Minimum issuance rate (expressed in fixed point at 1e18) uint256 internal constant MIN_ISSUANCE_RATE = 1e18; /** diff --git a/contracts/reservoir/ReservoirStorage.sol b/contracts/reservoir/ReservoirStorage.sol index b46e44d35..b964d87b0 100644 --- a/contracts/reservoir/ReservoirStorage.sol +++ b/contracts/reservoir/ReservoirStorage.sol @@ -5,7 +5,7 @@ pragma solidity ^0.7.6; import "../governance/Managed.sol"; /** - * @dev Base storage variables for the Reservoir on both layers + * @dev Base storage variables for the Reservoir on both layers, version 1 */ contract ReservoirV1Storage is Managed { // Relative increase of the total supply per block, plus 1, expressed in fixed point at 1e18. From f2e1f8178b529e3f7a73ede25079a57f42b9c3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:14:06 +0200 Subject: [PATCH 43/47] fix: use a single-condition requires for the drip auth check [L-05] --- contracts/reservoir/L1Reservoir.sol | 3 ++- test/reservoir/l1Reservoir.test.ts | 40 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index aa25f308d..9c7613700 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -62,7 +62,8 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { * @param _indexer Indexer for which the sender must be an operator */ modifier onlyIndexerOperator(address _indexer) { - require(_isIndexer(_indexer) && staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED"); + require(_isIndexer(_indexer), "UNAUTHORIZED_INVALID_INDEXER"); + require(staking().isOperator(msg.sender, _indexer), "UNAUTHORIZED_INVALID_OPERATOR"); _; } diff --git a/test/reservoir/l1Reservoir.test.ts b/test/reservoir/l1Reservoir.test.ts index 2ea01ac6d..261a36c39 100644 --- a/test/reservoir/l1Reservoir.test.ts +++ b/test/reservoir/l1Reservoir.test.ts @@ -45,6 +45,7 @@ describe('L1Reservoir', () => { let governor: Account let testAccount1: Account let testAccount2: Account + let testAccount3: Account let mockRouter: Account let mockL2GRT: Account let mockL2Gateway: Account @@ -162,6 +163,7 @@ describe('L1Reservoir', () => { mockL2Reservoir, keeper, testAccount2, + testAccount3, ] = await getAccounts() fixture = new NetworkFixture() @@ -424,7 +426,43 @@ describe('L1Reservoir', () => { ['drip(uint256,uint256,uint256,address)'](toBN(0), toBN(0), toBN(0), constants.AddressZero) await expect(tx).revertedWith('INVALID_BENEFICIARY') }) - it('can be called by an indexer operator using an extra parameter', async function () { + it('(operator variant) cannot be called with an invalid indexer', async function () { + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount1.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_INDEXER') + }) + it('(operator variant) cannot be called by someone who is not an operator for the right indexer', async function () { + const stakedAmount = toGRT('100000') + // testAccount1 is a valid indexer + await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) + await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount1.signer).stake(stakedAmount) + // testAccount2 is an operator for testAccount1's indexer + await staking.connect(testAccount1.signer).setOperator(testAccount2.address, true) + // testAccount3 is another valid indexer + await grt.connect(governor.signer).mint(testAccount3.address, stakedAmount) + await grt.connect(testAccount3.signer).approve(staking.address, stakedAmount) + await staking.connect(testAccount3.signer).stake(stakedAmount) + // But testAccount2 is not an operator for testAccount3's indexer + const tx = l1Reservoir + .connect(testAccount2.signer) + ['drip(uint256,uint256,uint256,address,address)']( + toBN(0), + toBN(0), + toBN(0), + testAccount1.address, + testAccount3.address, + ) + await expect(tx).revertedWith('UNAUTHORIZED_INVALID_OPERATOR') + }) + it('(operator variant) can be called by an indexer operator using an extra parameter', async function () { const stakedAmount = toGRT('100000') await grt.connect(governor.signer).mint(testAccount1.address, stakedAmount) await grt.connect(testAccount1.signer).approve(staking.address, stakedAmount) From 33f7ec21794853c2549861a2b5bfa879e593af7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:48:34 +0200 Subject: [PATCH 44/47] fix: add indexed params to dripper change events [N-01] --- contracts/reservoir/L1Reservoir.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index 9c7613700..b57d38fa1 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -43,9 +43,9 @@ contract L1Reservoir is L1ReservoirV2Storage, Reservoir { // Emitted when minDripInterval is updated event MinDripIntervalUpdated(uint256 minDripInterval); // Emitted when a new allowedDripper is added - event AllowedDripperAdded(address dripper); + event AllowedDripperAdded(address indexed dripper); // Emitted when an allowedDripper is revoked - event AllowedDripperRevoked(address dripper); + event AllowedDripperRevoked(address indexed dripper); /** * @dev Checks that the sender is an indexer with stake on the Staking contract, From fb3ed11c7a375e8124e5f184d141d4e929c5e0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Mon, 22 Aug 2022 18:45:43 +0200 Subject: [PATCH 45/47] fix: use explicit imports in relevant reservoir contracts [N-02] --- contracts/l2/reservoir/IL2Reservoir.sol | 2 +- contracts/l2/reservoir/L2Reservoir.sol | 18 ++++++++++-------- contracts/reservoir/L1Reservoir.sol | 13 ++++++++----- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/contracts/l2/reservoir/IL2Reservoir.sol b/contracts/l2/reservoir/IL2Reservoir.sol index b8a2311c9..8144efae2 100644 --- a/contracts/l2/reservoir/IL2Reservoir.sol +++ b/contracts/l2/reservoir/IL2Reservoir.sol @@ -2,7 +2,7 @@ pragma solidity ^0.7.6; -import "../../reservoir/IReservoir.sol"; +import { IReservoir } from "../../reservoir/IReservoir.sol"; /** * @title Interface for the L2 Rewards Reservoir diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol index 9a341b6b5..e5cce6fee 100644 --- a/contracts/l2/reservoir/L2Reservoir.sol +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -3,14 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; -import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; - -import "../../arbitrum/AddressAliasHelper.sol"; -import "../../reservoir/IReservoir.sol"; -import "../../reservoir/Reservoir.sol"; -import "./IL2Reservoir.sol"; -import "./L2ReservoirStorage.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; +import { ArbRetryableTx } from "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; + +import { Managed } from "../../governance/Managed.sol"; +import { IGraphToken } from "../../token/IGraphToken.sol"; +import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; +import { IReservoir } from "../../reservoir/IReservoir.sol"; +import { Reservoir } from "../../reservoir/Reservoir.sol"; +import { IL2Reservoir } from "./IL2Reservoir.sol"; +import { L2ReservoirV2Storage } from "./L2ReservoirStorage.sol"; /** * @dev ArbRetryableTx with additional interface to query the current redeemer. diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol index b57d38fa1..1fc1c30ce 100644 --- a/contracts/reservoir/L1Reservoir.sol +++ b/contracts/reservoir/L1Reservoir.sol @@ -3,13 +3,16 @@ pragma solidity ^0.7.6; pragma abicoder v2; -import "@openzeppelin/contracts/math/SafeMath.sol"; +import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; -import "../arbitrum/ITokenGateway.sol"; +import { ITokenGateway } from "../arbitrum/ITokenGateway.sol"; -import "../l2/reservoir/IL2Reservoir.sol"; -import "./Reservoir.sol"; -import "./L1ReservoirStorage.sol"; +import { Managed } from "../governance/Managed.sol"; +import { IGraphToken } from "../token/IGraphToken.sol"; +import { IStaking } from "../staking/IStaking.sol"; +import { IL2Reservoir } from "../l2/reservoir/IL2Reservoir.sol"; +import { Reservoir } from "./Reservoir.sol"; +import { L1ReservoirV2Storage } from "./L1ReservoirStorage.sol"; /** * @title L1 Rewards Reservoir From 53e0a80e5c432311fe30e489610b0f8efa10099d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Thu, 1 Sep 2022 17:28:00 -0300 Subject: [PATCH 46/47] test: fix call in l2Reservoir test --- test/l2/l2Reservoir.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/l2/l2Reservoir.test.ts b/test/l2/l2Reservoir.test.ts index 26a9ccc75..39bba0209 100644 --- a/test/l2/l2Reservoir.test.ts +++ b/test/l2/l2Reservoir.test.ts @@ -138,7 +138,6 @@ describe('L2Reservoir', () => { mockRouter.address, mockL1GRT.address, mockL1Gateway.address, - mockL1Reservoir.address, ) arbTxMock = await smock.fake('IArbTxWithRedeemer', { From 559ea00c29c0138f1eb98e69b84fd7ddf0d45014 Mon Sep 17 00:00:00 2001 From: Pablo Carranza Velez Date: Tue, 27 Sep 2022 09:56:30 -0300 Subject: [PATCH 47/47] fix: adjust gre, e2e and configs to account for reservoirs --- cli/contracts.ts | 4 ++ config/graph.arbitrum-goerli.yml | 10 +++++ config/graph.arbitrum-localhost.yml | 10 +++++ config/graph.goerli.yml | 17 ++++++-- config/graph.localhost.yml | 17 ++++++-- config/graph.mainnet.yml | 1 + e2e/deployment/config/l1/graphToken.test.ts | 11 ++++-- e2e/deployment/config/l1/l1Reservoir.test.ts | 39 +++++++++++++++++++ .../config/l1/rewardsManager.test.ts | 5 --- e2e/deployment/config/l2/l2GraphToken.test.ts | 9 ++++- e2e/deployment/config/l2/l2Reservoir.test.ts | 33 ++++++++++++++++ .../config/l2/rewardsManager.test.ts | 5 --- 12 files changed, 140 insertions(+), 21 deletions(-) create mode 100644 e2e/deployment/config/l1/l1Reservoir.test.ts create mode 100644 e2e/deployment/config/l2/l2Reservoir.test.ts diff --git a/cli/contracts.ts b/cli/contracts.ts index b01727c20..6e9e1d28f 100644 --- a/cli/contracts.ts +++ b/cli/contracts.ts @@ -26,6 +26,8 @@ import { L1GraphTokenGateway } from '../build/types/L1GraphTokenGateway' import { L2GraphToken } from '../build/types/L2GraphToken' import { L2GraphTokenGateway } from '../build/types/L2GraphTokenGateway' import { BridgeEscrow } from '../build/types/BridgeEscrow' +import { L1Reservoir } from '../build/types/L1Reservoir' +import { L2Reservoir } from '../build/types/L2Reservoir' export interface NetworkContracts { EpochManager: EpochManager @@ -49,6 +51,8 @@ export interface NetworkContracts { BridgeEscrow: BridgeEscrow L2GraphToken: L2GraphToken L2GraphTokenGateway: L2GraphTokenGateway + L1Reservoir: L1Reservoir + L2Reservoir: L2Reservoir } export const loadAddressBookContract = ( diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml index fa1a66ad3..20e16940e 100644 --- a/config/graph.arbitrum-goerli.yml +++ b/config/graph.arbitrum-goerli.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L2Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -149,3 +152,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + L2Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "approveRewardsManager" + - fn: "syncAllContracts" diff --git a/config/graph.arbitrum-localhost.yml b/config/graph.arbitrum-localhost.yml index 1061044be..519b9bfba 100644 --- a/config/graph.arbitrum-localhost.yml +++ b/config/graph.arbitrum-localhost.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L2GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L2Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -149,3 +152,10 @@ contracts: - fn: "syncAllContracts" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian + L2Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + calls: + - fn: "approveRewardsManager" + - fn: "syncAllContracts" diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index b4b735b4e..e71b1e98d 100644 --- a/config/graph.goerli.yml +++ b/config/graph.goerli.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L1Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -57,7 +60,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{RewardsManager.address}}" + minter: "${{L1Reservoir.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +134,6 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" @@ -158,3 +159,13 @@ contracts: controller: "${{Controller.address}}" calls: - fn: "syncAllContracts" + L1Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + dripInterval: 50400 + calls: + - fn: "approveRewardsManager" + - fn: "initialSnapshot" + pendingRewards: "0" + - fn: "syncAllContracts" diff --git a/config/graph.localhost.yml b/config/graph.localhost.yml index 4a90dfd20..3eb3a9161 100644 --- a/config/graph.localhost.yml +++ b/config/graph.localhost.yml @@ -33,6 +33,9 @@ contracts: - fn: "setContractProxy" id: "0xd362cac9cb75c10d67bcc0b7eeb0b1ef48bb5420b556c092d4fd7f758816fcf0" # keccak256('GraphTokenGateway') contractAddress: "${{L1GraphTokenGateway.address}}" + - fn: "setContractProxy" + id: "0x96ba401694892957e25e29c7a1e4171ae9945b5ee36339de79b199a530436e9e" # keccak256('Reservoir') + contractAddress: "${{L1Reservoir.address}}" - fn: "setPauseGuardian" pauseGuardian: *pauseGuardian - fn: "transferOwnership" @@ -57,7 +60,7 @@ contracts: initialSupply: "10000000000000000000000000000" # in wei calls: - fn: "addMinter" - minter: "${{RewardsManager.address}}" + minter: "${{L1Reservoir.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +134,6 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000012184945188" # per block increase of total supply, blocks in a year = 365*60*60*24/13 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" @@ -158,3 +159,13 @@ contracts: controller: "${{Controller.address}}" calls: - fn: "syncAllContracts" + L1Reservoir: + proxy: true + init: + controller: "${{Controller.address}}" + dripInterval: 50400 + calls: + - fn: "approveRewardsManager" + - fn: "initialSnapshot" + pendingRewards: "0" + - fn: "syncAllContracts" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index ea11603fd..529e40ffc 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -167,4 +167,5 @@ contracts: calls: - fn: "approveRewardsManager" - fn: "initialSnapshot" + pendingRewards: "0" - fn: "syncAllContracts" diff --git a/e2e/deployment/config/l1/graphToken.test.ts b/e2e/deployment/config/l1/graphToken.test.ts index 41e6322d0..d8d14ff64 100644 --- a/e2e/deployment/config/l1/graphToken.test.ts +++ b/e2e/deployment/config/l1/graphToken.test.ts @@ -5,7 +5,7 @@ import GraphChain from '../../../../gre/helpers/network' describe('[L1] GraphToken', () => { const graph = hre.graph() - const { GraphToken, RewardsManager } = graph.contracts + const { GraphToken, L1Reservoir, RewardsManager } = graph.contracts let unauthorized: SignerWithAddress @@ -23,9 +23,14 @@ describe('[L1] GraphToken', () => { await expect(tx).revertedWith('Only minter can call') }) - it('RewardsManager should be minter', async function () { + it('L1Reservoir should be minter', async function () { + const reservoirIsMinter = await GraphToken.isMinter(L1Reservoir.address) + expect(reservoirIsMinter).eq(true) + }) + + it('RewardsManager should not be minter', async function () { const rewardsMgrIsMinter = await GraphToken.isMinter(RewardsManager.address) - expect(rewardsMgrIsMinter).eq(true) + expect(rewardsMgrIsMinter).eq(false) }) }) }) diff --git a/e2e/deployment/config/l1/l1Reservoir.test.ts b/e2e/deployment/config/l1/l1Reservoir.test.ts new file mode 100644 index 000000000..16531d6a6 --- /dev/null +++ b/e2e/deployment/config/l1/l1Reservoir.test.ts @@ -0,0 +1,39 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' +import { getItemValue } from '../../../../cli/config' + +describe('[L1] L1Reservoir configuration', () => { + const graph = hre.graph() + const { graphConfig } = graph + const { L1Reservoir, Controller, GraphToken, RewardsManager } = graph.contracts + + before(async function () { + if (GraphChain.isL2(graph.chainId)) this.skip() + }) + + it('should be controlled by Controller', async function () { + const controller = await L1Reservoir.controller() + expect(controller).eq(Controller.address) + }) + + it('should have a snapshot of the total supply', async function () { + expect(await L1Reservoir.issuanceBase()).eq(await GraphToken.totalSupply()) + }) + + it('should have issuanceRate set to zero', async function () { + expect(await L1Reservoir.issuanceRate()).eq(0) + }) + + it('should have dripInterval set from config', async function () { + const value = await L1Reservoir.dripInterval() + const expected = getItemValue(graphConfig, 'contracts/L1Reservoir/init/dripInterval') + expect(value).eq(expected) + }) + + it('should have RewardsManager approved for the max GRT amount', async function () { + expect(await GraphToken.allowance(L1Reservoir.address, RewardsManager.address)).eq( + hre.ethers.constants.MaxUint256, + ) + }) +}) diff --git a/e2e/deployment/config/l1/rewardsManager.test.ts b/e2e/deployment/config/l1/rewardsManager.test.ts index 4cc990161..d8bd1f553 100644 --- a/e2e/deployment/config/l1/rewardsManager.test.ts +++ b/e2e/deployment/config/l1/rewardsManager.test.ts @@ -9,9 +9,4 @@ describe('[L1] RewardsManager configuration', () => { before(async function () { if (GraphChain.isL2(graph.chainId)) this.skip() }) - - it('issuanceRate should match "issuanceRate" in the config file', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('1000000012184945188') // hardcoded as it's set with a function call rather than init parameter - }) }) diff --git a/e2e/deployment/config/l2/l2GraphToken.test.ts b/e2e/deployment/config/l2/l2GraphToken.test.ts index 7b87253e0..4b5391a01 100644 --- a/e2e/deployment/config/l2/l2GraphToken.test.ts +++ b/e2e/deployment/config/l2/l2GraphToken.test.ts @@ -5,7 +5,7 @@ import GraphChain from '../../../../gre/helpers/network' describe('[L2] L2GraphToken', () => { const graph = hre.graph() - const { L2GraphToken, RewardsManager } = graph.contracts + const { L2GraphToken, RewardsManager, L2Reservoir } = graph.contracts let unauthorized: SignerWithAddress @@ -36,9 +36,14 @@ describe('[L2] L2GraphToken', () => { await expect(tx).revertedWith('Only Governor can call') }) - it('RewardsManager should not be minter (for now)', async function () { + it('RewardsManager should not be minter', async function () { const rewardsMgrIsMinter = await L2GraphToken.isMinter(RewardsManager.address) expect(rewardsMgrIsMinter).eq(false) }) + + it('L2Reservoir should not be minter', async function () { + const reservoirIsMinter = await L2GraphToken.isMinter(L2Reservoir.address) + expect(reservoirIsMinter).eq(false) + }) }) }) diff --git a/e2e/deployment/config/l2/l2Reservoir.test.ts b/e2e/deployment/config/l2/l2Reservoir.test.ts new file mode 100644 index 000000000..12afc4552 --- /dev/null +++ b/e2e/deployment/config/l2/l2Reservoir.test.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import GraphChain from '../../../../gre/helpers/network' +import { getItemValue } from '../../../../cli/config' + +describe('[L2] L2Reservoir configuration', () => { + const graph = hre.graph() + const { graphConfig } = graph + const { L2Reservoir, Controller, GraphToken, RewardsManager } = graph.contracts + + before(async function () { + if (GraphChain.isL1(graph.chainId)) this.skip() + }) + + it('should be controlled by Controller', async function () { + const controller = await L2Reservoir.controller() + expect(controller).eq(Controller.address) + }) + + it('should have issuanceBase set to zero', async function () { + expect(await L2Reservoir.issuanceBase()).eq(0) + }) + + it('should have issuanceRate set to zero', async function () { + expect(await L2Reservoir.issuanceRate()).eq(0) + }) + + it('should have RewardsManager approved for the max GRT amount', async function () { + expect(await GraphToken.allowance(L2Reservoir.address, RewardsManager.address)).eq( + hre.ethers.constants.MaxUint256, + ) + }) +}) diff --git a/e2e/deployment/config/l2/rewardsManager.test.ts b/e2e/deployment/config/l2/rewardsManager.test.ts index 29ad83c5f..7754758de 100644 --- a/e2e/deployment/config/l2/rewardsManager.test.ts +++ b/e2e/deployment/config/l2/rewardsManager.test.ts @@ -9,9 +9,4 @@ describe('[L2] RewardsManager configuration', () => { before(async function () { if (GraphChain.isL1(graph.chainId)) this.skip() }) - - it('issuanceRate should be zero', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('0') - }) })