diff --git a/.solhintignore b/.solhintignore index 24a21bb44..b36dffeb7 100644 --- a/.solhintignore +++ b/.solhintignore @@ -4,6 +4,5 @@ node_modules ./contracts/discovery/erc1056 ./contracts/rewards/RewardsManager.sol ./contracts/staking/libs/LibFixedMath.sol -./contracts/tests/RewardsManagerMock.sol ./contracts/tests/ens ./contracts/tests/testnet/GSRManager.sol diff --git a/cli/commands/protocol/get.ts b/cli/commands/protocol/get.ts index 439b539b4..801e468a7 100644 --- a/cli/commands/protocol/get.ts +++ b/cli/commands/protocol/get.ts @@ -34,7 +34,7 @@ export const gettersList = { 'epochs-length': { contract: 'EpochManager', name: 'epochLength' }, 'epochs-current': { contract: 'EpochManager', name: 'currentEpoch' }, // Rewards - 'rewards-issuance-rate': { contract: 'RewardsManager', name: 'issuanceRate' }, + 'rewards-issuance-per-block': { contract: 'RewardsManager', name: 'issuancePerBlock' }, 'subgraph-availability-oracle': { contract: 'RewardsManager', name: 'subgraphAvailabilityOracle', diff --git a/cli/commands/protocol/set.ts b/cli/commands/protocol/set.ts index 966d07897..a7850601a 100644 --- a/cli/commands/protocol/set.ts +++ b/cli/commands/protocol/set.ts @@ -39,7 +39,7 @@ export const settersList = { // Epochs 'epochs-length': { contract: 'EpochManager', name: 'setEpochLength' }, // Rewards - 'rewards-issuance-rate': { contract: 'RewardsManager', name: 'setIssuanceRate' }, + 'rewards-issuance-per-block': { contract: 'RewardsManager', name: 'setIssuancePerBlock' }, 'subgraph-availability-oracle': { contract: 'RewardsManager', name: 'setSubgraphAvailabilityOracle', diff --git a/config/graph.arbitrum-goerli.yml b/config/graph.arbitrum-goerli.yml index 5b41e4382..f7ab04744 100644 --- a/config/graph.arbitrum-goerli.yml +++ b/config/graph.arbitrum-goerli.yml @@ -57,6 +57,8 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor diff --git a/config/graph.arbitrum-localhost.yml b/config/graph.arbitrum-localhost.yml index a17755fbc..52e2b411a 100644 --- a/config/graph.arbitrum-localhost.yml +++ b/config/graph.arbitrum-localhost.yml @@ -57,6 +57,8 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor diff --git a/config/graph.arbitrum-one.yml b/config/graph.arbitrum-one.yml index 49c7b0b13..aa15603a0 100644 --- a/config/graph.arbitrum-one.yml +++ b/config/graph.arbitrum-one.yml @@ -57,6 +57,8 @@ contracts: init: owner: "${{Env.deployer}}" calls: + - fn: "addMinter" + minter: "${{RewardsManager.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor diff --git a/config/graph.goerli.yml b/config/graph.goerli.yml index af19d28ed..8668c4938 100644 --- a/config/graph.goerli.yml +++ b/config/graph.goerli.yml @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +133,8 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" diff --git a/config/graph.localhost.yml b/config/graph.localhost.yml index b93f88dad..ce99ecabd 100644 --- a/config/graph.localhost.yml +++ b/config/graph.localhost.yml @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +133,8 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" diff --git a/config/graph.mainnet.yml b/config/graph.mainnet.yml index a1aaebbbb..03fe08496 100644 --- a/config/graph.mainnet.yml +++ b/config/graph.mainnet.yml @@ -58,6 +58,8 @@ contracts: calls: - fn: "addMinter" minter: "${{RewardsManager.address}}" + - fn: "addMinter" + minter: "${{L1GraphTokenGateway.address}}" - fn: "renounceMinter" - fn: "transferOwnership" owner: *governor @@ -131,8 +133,8 @@ contracts: init: controller: "${{Controller.address}}" calls: - - fn: "setIssuanceRate" - issuanceRate: "1000000011247641700" # per block increase of total supply, blocks in a year = 365*60*60*24/13 + - fn: "setIssuancePerBlock" + issuancePerBlock: "114155251141552511415" # per block increase of total supply, blocks in a year = 365*60*60*24/12 - fn: "setSubgraphAvailabilityOracle" subgraphAvailabilityOracle: *availabilityOracle - fn: "syncAllContracts" diff --git a/contracts/gateway/L1GraphTokenGateway.sol b/contracts/gateway/L1GraphTokenGateway.sol index 094d99611..d946b78a7 100644 --- a/contracts/gateway/L1GraphTokenGateway.sol +++ b/contracts/gateway/L1GraphTokenGateway.sol @@ -40,6 +40,14 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess address public escrow; /// Addresses for which this mapping is true are allowed to send callhooks in outbound transfers mapping(address => bool) public callhookAllowlist; + /// Total amount minted from L2 + uint256 public totalMintedFromL2; + /// Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock + uint256 public accumulatedL2MintAllowanceSnapshot; + /// Block at which new L2 allowance starts accumulating + uint256 public lastL2MintAllowanceUpdateBlock; + /// New L2 mint allowance per block + uint256 public l2MintAllowancePerBlock; /// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2 event DepositInitiated( @@ -71,6 +79,14 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess event AddedToCallhookAllowlist(address newAllowlisted); /// Emitted when an address is removed from the callhook allowlist event RemovedFromCallhookAllowlist(address notAllowlisted); + /// Emitted when the L2 mint allowance per block is updated + event L2MintAllowanceUpdated( + uint256 accumulatedL2MintAllowanceSnapshot, + uint256 l2MintAllowancePerBlock, + uint256 lastL2MintAllowanceUpdateBlock + ); + /// Emitted when tokens are minted due to an incoming transfer from L2 + event TokensMintedFromL2(uint256 amount); /** * @dev Allows a function to be called only by the gateway's L2 counterpart. @@ -182,6 +198,56 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess emit RemovedFromCallhookAllowlist(_notAllowlisted); } + /** + * @dev Updates the L2 mint allowance per block + * It is meant to be called _after_ the issuancePerBlock is updated in L2. + * The caller should provide the new issuance per block and the block at which it was updated, + * the function will automatically compute the values so that the bridge's mint allowance + * correctly tracks the maximum rewards minted in L2. + * @param _l2IssuancePerBlock New issuancePerBlock that has been set in L2 + * @param _updateBlockNum L1 Block number at which issuancePerBlock was updated in L2 + */ + function updateL2MintAllowance(uint256 _l2IssuancePerBlock, uint256 _updateBlockNum) + external + onlyGovernor + { + require(_updateBlockNum < block.number, "BLOCK_MUST_BE_PAST"); + require(_updateBlockNum > lastL2MintAllowanceUpdateBlock, "BLOCK_MUST_BE_INCREMENTING"); + accumulatedL2MintAllowanceSnapshot = accumulatedL2MintAllowanceAtBlock(_updateBlockNum); + lastL2MintAllowanceUpdateBlock = _updateBlockNum; + l2MintAllowancePerBlock = _l2IssuancePerBlock; + emit L2MintAllowanceUpdated( + accumulatedL2MintAllowanceSnapshot, + l2MintAllowancePerBlock, + lastL2MintAllowanceUpdateBlock + ); + } + + /** + * @dev Manually sets the parameters used to compute the L2 mint allowance + * The use of this function is not recommended, use updateL2MintAllowance instead; + * this one is only meant to be used as a backup recovery if a previous call to + * updateL2MintAllowance was done with incorrect values. + * @param _accumulatedL2MintAllowanceSnapshot Accumulated L2 mint allowance at L1 block _lastL2MintAllowanceUpdateBlock + * @param _l2MintAllowancePerBlock L2 issuance per block since block number _lastL2MintAllowanceUpdateBlock + * @param _lastL2MintAllowanceUpdateBlock L1 Block number at which issuancePerBlock was last updated in L2 + */ + function setL2MintAllowanceParametersManual( + uint256 _accumulatedL2MintAllowanceSnapshot, + uint256 _l2MintAllowancePerBlock, + uint256 _lastL2MintAllowanceUpdateBlock + ) external onlyGovernor { + require(_lastL2MintAllowanceUpdateBlock < block.number, "BLOCK_MUST_BE_PAST"); + accumulatedL2MintAllowanceSnapshot = _accumulatedL2MintAllowanceSnapshot; + l2MintAllowancePerBlock = _l2MintAllowancePerBlock; + lastL2MintAllowanceUpdateBlock = _lastL2MintAllowanceUpdateBlock; + emit L2MintAllowanceUpdated( + accumulatedL2MintAllowanceSnapshot, + l2MintAllowancePerBlock, + lastL2MintAllowanceUpdateBlock + ); + } + /** * @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox. * The tokens are escrowed by the gateway until they are withdrawn back to L1. @@ -277,8 +343,10 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess require(_l1Token == address(token), "TOKEN_NOT_GRT"); uint256 escrowBalance = token.balanceOf(escrow); - // If the bridge doesn't have enough tokens, something's very wrong! - require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS"); + if (_amount > escrowBalance) { + // This will revert if trying to mint more than allowed + _mintFromL2(_amount.sub(escrowBalance)); + } token.transferFrom(escrow, _to, _amount); emit WithdrawalFinalized(_l1Token, _from, _to, 0, _amount); @@ -381,4 +449,42 @@ contract L1GraphTokenGateway is Initializable, GraphTokenGateway, L1ArbitrumMess (maxSubmissionCost, extraData) = abi.decode(extraData, (uint256, bytes)); return (from, maxSubmissionCost, extraData); } + + /** + * @dev Get the accumulated L2 mint allowance at a particular block number + * @param _blockNum Block at which allowance will be computed + * @return The accumulated GRT amount that can be minted from L2 at the specified block + */ + function accumulatedL2MintAllowanceAtBlock(uint256 _blockNum) public view returns (uint256) { + require(_blockNum >= lastL2MintAllowanceUpdateBlock, "INVALID_BLOCK_FOR_MINT_ALLOWANCE"); + return + accumulatedL2MintAllowanceSnapshot.add( + l2MintAllowancePerBlock.mul(_blockNum.sub(lastL2MintAllowanceUpdateBlock)) + ); + } + + /** + * @dev Mint new L1 tokens coming from L2 + * This will check if the amount to mint is within the L2's mint allowance, and revert otherwise. + * The tokens will be sent to the bridge escrow (from where they will then be sent to the destinatary + * of the current inbound transfer). + * @param _amount Number of tokens to mint + */ + function _mintFromL2(uint256 _amount) internal { + // If we're trying to mint more than allowed, something's gone terribly wrong + // (either the L2 issuance is wrong, or the Arbitrum bridge has been compromised) + require(_l2MintAmountAllowed(_amount), "INVALID_L2_MINT_AMOUNT"); + totalMintedFromL2 = totalMintedFromL2.add(_amount); + graphToken().mint(escrow, _amount); + emit TokensMintedFromL2(_amount); + } + + /** + * @dev Check if minting a certain amount of tokens from L2 is within allowance + * @param _amount Number of tokens that would be minted + * @return true if minting those tokens is allowed, or false if it would be over allowance + */ + function _l2MintAmountAllowed(uint256 _amount) internal view returns (bool) { + return (totalMintedFromL2.add(_amount) <= accumulatedL2MintAllowanceAtBlock(block.number)); + } } diff --git a/contracts/rewards/IRewardsManager.sol b/contracts/rewards/IRewardsManager.sol index dc17c8ba8..87d3c2455 100644 --- a/contracts/rewards/IRewardsManager.sol +++ b/contracts/rewards/IRewardsManager.sol @@ -15,7 +15,7 @@ interface IRewardsManager { // -- Config -- - function setIssuanceRate(uint256 _issuanceRate) external; + function setIssuancePerBlock(uint256 _issuancePerBlock) external; function setMinimumSubgraphSignal(uint256 _minimumSubgraphSignal) external; diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index 6d2e78965..e259d50b8 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -28,11 +28,10 @@ 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; - uint256 private constant MIN_ISSUANCE_RATE = 1e18; + uint256 private constant FIXED_POINT_SCALING_FACTOR = 1e18; // -- Events -- @@ -76,29 +75,28 @@ 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 + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * Whenever this function is called in layer 2, the updateL2MintAllowance function + * _must_ be called on the L1GraphTokenGateway in L1, to ensure the bridge can mint the + * right amount of tokens. + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ - function setIssuanceRate(uint256 _issuanceRate) external override onlyGovernor { - _setIssuanceRate(_issuanceRate); + function setIssuancePerBlock(uint256 _issuancePerBlock) external override onlyGovernor { + _setIssuancePerBlock(_issuancePerBlock); } /** - * @dev Sets the issuance rate. - * @param _issuanceRate Issuance rate + * @dev Sets the GRT issuance per block. + * The issuance is defined as a fixed amount of rewards per block in GRT. + * @param _issuancePerBlock Issuance expressed in GRT per block (scaled by 1e18) */ - function _setIssuanceRate(uint256 _issuanceRate) private { - require(_issuanceRate >= MIN_ISSUANCE_RATE, "Issuance rate under minimum allowed"); - - // Called since `issuance rate` will change + function _setIssuancePerBlock(uint256 _issuancePerBlock) private { + // Called since `issuance per block` will change updateAccRewardsPerSignal(); - issuanceRate = _issuanceRate; - emit ParameterUpdated("issuanceRate"); + issuancePerBlock = _issuancePerBlock; + emit ParameterUpdated("issuancePerBlock"); } /** @@ -188,18 +186,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` + * Linear formula: `x = 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` + * x: newly accrued rewards tokens for the period `t` * - * @return newly accrued rewards per signal since last update + * @return newly accrued rewards per signal since last update, scaled by FIXED_POINT_SCALING_FACTOR */ function getNewRewardsPerSignal() public view override returns (uint256) { // Calculate time steps @@ -208,9 +201,8 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa if (t == 0) { return 0; } - - // Zero issuance under a rate of 1.0 - if (issuanceRate <= MIN_ISSUANCE_RATE) { + // ...or if issuance is zero + if (issuancePerBlock == 0) { return 0; } @@ -221,16 +213,11 @@ 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 x = issuancePerBlock.mul(t); // 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 x.mul(FIXED_POINT_SCALING_FACTOR).div(signalledTokens); } /** @@ -262,7 +249,7 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa ? getAccRewardsPerSignal() .sub(subgraph.accRewardsPerSignalSnapshot) .mul(subgraphSignalledTokens) - .div(TOKEN_DECIMALS) + .div(FIXED_POINT_SCALING_FACTOR) : 0; return subgraph.accRewardsForSubgraph.add(newRewards); } @@ -294,9 +281,9 @@ contract RewardsManager is RewardsManagerV3Storage, 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 @@ -307,14 +294,13 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa /** * @dev Updates the accumulated rewards per signal and save checkpoint block number. - * Must be called before `issuanceRate` or `total signalled GRT` changes + * Must be called before `issuancePerBlock` or `total signalled GRT` changes * Called from the Curation contract on mint() and burn() * @return Accumulated rewards per signal */ function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); return accRewardsPerSignal; } @@ -395,7 +381,7 @@ contract RewardsManager is RewardsManagerV3Storage, 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); } /** @@ -438,67 +424,4 @@ contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsMa 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 - */ - 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) - } - } - } - } - } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index 7626992da..821d93edb 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 private 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 private tokenSupplySnapshotDeprecated; +} + +contract RewardsManagerV4Storage is RewardsManagerV3Storage { + // GRT issued for indexer rewards per block + uint256 public issuancePerBlock; } 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/e2e/deployment/config/l1/rewardsManager.test.ts b/e2e/deployment/config/l1/rewardsManager.test.ts index b3ee0ffd7..d46f15397 100644 --- a/e2e/deployment/config/l1/rewardsManager.test.ts +++ b/e2e/deployment/config/l1/rewardsManager.test.ts @@ -10,8 +10,8 @@ describe('[L1] RewardsManager configuration', () => { 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('1000000011247641700') // hardcoded as it's set with a function call rather than init parameter + it('issuancePerBlock should match "issuancePerBlock" in the config file', async function () { + const value = await RewardsManager.issuancePerBlock() + expect(value).eq('114155251141552511415') // 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 a4ccdaec8..6b5f03c92 100644 --- a/e2e/deployment/config/l2/l2GraphToken.test.ts +++ b/e2e/deployment/config/l2/l2GraphToken.test.ts @@ -46,9 +46,9 @@ describe('[L2] L2GraphToken', () => { await expect(tx).revertedWith('Only Governor can call') }) - it('RewardsManager should not be minter (for now)', async function () { + it('RewardsManager should be minter', async function () { const rewardsMgrIsMinter = await L2GraphToken.isMinter(RewardsManager.address) - expect(rewardsMgrIsMinter).eq(false) + expect(rewardsMgrIsMinter).eq(true) }) }) }) diff --git a/e2e/deployment/config/l2/rewardsManager.test.ts b/e2e/deployment/config/l2/rewardsManager.test.ts index a5e2e7cbf..5329abec8 100644 --- a/e2e/deployment/config/l2/rewardsManager.test.ts +++ b/e2e/deployment/config/l2/rewardsManager.test.ts @@ -10,8 +10,8 @@ describe('[L2] RewardsManager configuration', () => { if (GraphChain.isL1(graph.chainId)) this.skip() }) - it('issuanceRate should be zero', async function () { - const value = await RewardsManager.issuanceRate() - expect(value).eq('0') + it('issuancePerBlock should be zero', async function () { + const value = await RewardsManager.issuancePerBlock() + expect(value).eq('0') // hardcoded as it's set with a function call rather than init parameter }) }) diff --git a/tasks/deployment/config.ts b/tasks/deployment/config.ts index 123afed53..f3992e652 100644 --- a/tasks/deployment/config.ts +++ b/tasks/deployment/config.ts @@ -67,7 +67,7 @@ const staking: Contract = { const rewardsManager: Contract = { name: 'RewardsManager', - initParams: [{ name: 'issuanceRate', type: 'BigNumber' }], + initParams: [{ name: 'issuancePerBlock', type: 'BigNumber' }], } const contractList: Contract[] = [epochManager, curation, disputeManager, staking, rewardsManager] diff --git a/test/gateway/l1GraphTokenGateway.test.ts b/test/gateway/l1GraphTokenGateway.test.ts index 76b00ee6b..1959cba2f 100644 --- a/test/gateway/l1GraphTokenGateway.test.ts +++ b/test/gateway/l1GraphTokenGateway.test.ts @@ -16,6 +16,7 @@ import { toGRT, Account, applyL1ToL2Alias, + advanceBlocks, provider, } from '../lib/testHelpers' import { BridgeEscrow } from '../../build/types/BridgeEscrow' @@ -416,8 +417,8 @@ describe('L1GraphTokenGateway', () => { ) const escrowBalance = await grt.balanceOf(bridgeEscrow.address) const senderBalance = await grt.balanceOf(tokenSender.address) - await expect(escrowBalance).eq(toGRT('10')) - await expect(senderBalance).eq(toGRT('990')) + expect(escrowBalance).eq(toGRT('10')) + expect(senderBalance).eq(toGRT('990')) } before(async function () { await fixture.configureL1Bridge( @@ -430,6 +431,183 @@ describe('L1GraphTokenGateway', () => { ) }) + describe('updateL2MintAllowance', function () { + it('rejects calls that are not from the governor', async function () { + const tx = l1GraphTokenGateway + .connect(pauseGuardian.address) + .updateL2MintAllowance(toGRT('1'), await latestBlock()) + await expect(tx).revertedWith('Only Controller governor') + }) + it('does not allow using a future or current block number', async function () { + const issuancePerBlock = toGRT('120') + let issuanceUpdatedAtBlock = (await latestBlock()).add(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = await latestBlock() // This will be block.number-1 in our next tx + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx3) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + }) + it('does not allow using a block number lower than or equal to the previous one', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = await latestBlock() + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_INCREMENTING') + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.sub(1)) + await expect(tx3).revertedWith('BLOCK_MUST_BE_INCREMENTING') + const tx4 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.add(1)) + await expect(tx4) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(issuancePerBlock, issuancePerBlock, issuanceUpdatedAtBlock.add(1)) + }) + it('updates the snapshot and issuance to follow a new linear function, accumulating up to the specified block', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = (await latestBlock()).sub(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + // Now the mint allowance should be issuancePerBlock * 3 + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(issuancePerBlock.mul(3)) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(0) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + issuanceUpdatedAtBlock, + ) + + await advanceBlocks(10) + + const newIssuancePerBlock = toGRT('200') + const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1) + + const expectedAccumulatedSnapshot = issuancePerBlock.mul( + newIssuanceUpdatedAtBlock.sub(issuanceUpdatedAtBlock), + ) + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(newIssuancePerBlock, newIssuanceUpdatedAtBlock) + await expect(tx2) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(expectedAccumulatedSnapshot, newIssuancePerBlock, newIssuanceUpdatedAtBlock) + + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(expectedAccumulatedSnapshot.add(newIssuancePerBlock.mul(2))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq( + expectedAccumulatedSnapshot, + ) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + newIssuanceUpdatedAtBlock, + ) + }) + }) + describe('setL2MintAllowanceParametersManual', function () { + it('rejects calls that are not from the governor', async function () { + const tx = l1GraphTokenGateway + .connect(pauseGuardian.address) + .setL2MintAllowanceParametersManual(toGRT('0'), toGRT('1'), await latestBlock()) + await expect(tx).revertedWith('Only Controller governor') + }) + it('does not allow using a future or current block number', async function () { + const issuancePerBlock = toGRT('120') + let issuanceUpdatedAtBlock = (await latestBlock()).add(2) + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx1).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx2).revertedWith('BLOCK_MUST_BE_PAST') + issuanceUpdatedAtBlock = await latestBlock() // This will be block.number-1 in our next tx + const tx3 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + await expect(tx3) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock) + }) + it('updates the snapshot and issuance to follow a new linear function, manually setting the snapshot value', async function () { + const issuancePerBlock = toGRT('120') + const issuanceUpdatedAtBlock = (await latestBlock()).sub(2) + const snapshotValue = toGRT('10') + const tx1 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual( + snapshotValue, + issuancePerBlock, + issuanceUpdatedAtBlock, + ) + await expect(tx1) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock) + // Now the mint allowance should be 10 + issuancePerBlock * 3 + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(snapshotValue.add(issuancePerBlock.mul(3))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(snapshotValue) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + issuanceUpdatedAtBlock, + ) + + await advanceBlocks(10) + + const newIssuancePerBlock = toGRT('200') + const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1) + const newSnapshotValue = toGRT('10') + + const tx2 = l1GraphTokenGateway + .connect(governor.signer) + .setL2MintAllowanceParametersManual( + newSnapshotValue, + newIssuancePerBlock, + newIssuanceUpdatedAtBlock, + ) + await expect(tx2) + .emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated') + .withArgs(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock) + + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(newSnapshotValue.add(newIssuancePerBlock.mul(2))) + expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq( + newSnapshotValue, + ) + expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock) + expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq( + newIssuanceUpdatedAtBlock, + ) + }) + }) describe('calculateL2TokenAddress', function () { it('returns the L2 token address', async function () { expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(mockL2GRT.address) @@ -577,7 +755,7 @@ describe('L1GraphTokenGateway', () => { ) await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY') }) - it('reverts if the gateway does not have tokens', async function () { + it('reverts if the gateway does not have tokens or allowance', async function () { // This scenario should never really happen, but we still // test that the gateway reverts in this case const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( @@ -607,7 +785,7 @@ describe('L1GraphTokenGateway', () => { toBN('0'), encodedCalldata, ) - await expect(tx).revertedWith('BRIDGE_OUT_OF_FUNDS') + await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') }) it('reverts if the gateway is revoked from escrow', async function () { await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) @@ -680,8 +858,105 @@ describe('L1GraphTokenGateway', () => { .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('8')) const escrowBalance = await grt.balanceOf(bridgeEscrow.address) const senderBalance = await grt.balanceOf(tokenSender.address) - await expect(escrowBalance).eq(toGRT('2')) - await expect(senderBalance).eq(toGRT('998')) + expect(escrowBalance).eq(toGRT('2')) + expect(senderBalance).eq(toGRT('998')) + }) + it('mints tokens up to the L2 mint allowance', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + + // Start accruing L2 mint allowance at 2 GRT per block + await l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(toGRT('2'), await latestBlock()) + await advanceBlocks(2) + // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so + // there should be 8 GRT allowed to be minted from L2 in the next block. + + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('18'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx) + .emit(l1GraphTokenGateway, 'WithdrawalFinalized') + .withArgs(grt.address, l2Receiver.address, tokenSender.address, toBN('0'), toGRT('18')) + .emit(l1GraphTokenGateway, 'TokensMintedFromL2') + .withArgs(toGRT('8')) + expect(await l1GraphTokenGateway.totalMintedFromL2()).to.eq(toGRT('8')) + expect( + await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()), + ).to.eq(toGRT('8')) + + const escrowBalance = await grt.balanceOf(bridgeEscrow.address) + const senderBalance = await grt.balanceOf(tokenSender.address) + expect(escrowBalance).eq(toGRT('0')) + expect(senderBalance).eq(toGRT('1008')) + }) + it('reverts if the amount to mint is over the allowance', async function () { + await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10')) + await testValidOutboundTransfer(tokenSender.signer, defaultData, emptyCallHookData) + + // Start accruing L2 mint allowance at 2 GRT per block + await l1GraphTokenGateway + .connect(governor.signer) + .updateL2MintAllowance(toGRT('2'), await latestBlock()) + await advanceBlocks(2) + // Now it's been three blocks since the lastL2MintAllowanceUpdateBlock, so + // there should be 8 GRT allowed to be minted from L2 in the next block. + + // At this point, the gateway holds 10 GRT in escrow + const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData( + 'finalizeInboundTransfer', + [ + grt.address, + l2Receiver.address, + tokenSender.address, + toGRT('18.001'), + utils.defaultAbiCoder.encode(['uint256', 'bytes'], [0, []]), + ], + ) + // The real outbox would require a proof, which would + // validate that the tx was initiated by the L2 gateway but our mock + // just executes unconditionally + const tx = outboxMock + .connect(tokenSender.signer) + .executeTransaction( + toBN('0'), + [], + toBN('0'), + mockL2Gateway.address, + l1GraphTokenGateway.address, + toBN('1337'), + await latestBlock(), + toBN('133701337'), + toBN('0'), + encodedCalldata, + ) + await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT') }) }) }) diff --git a/test/lib/deployment.ts b/test/lib/deployment.ts index a6afe9dbb..e07b1e490 100644 --- a/test/lib/deployment.ts +++ b/test/lib/deployment.ts @@ -56,7 +56,7 @@ export const defaults = { initialSupply: toGRT('10000000000'), // 10 billion }, rewards: { - issuanceRate: toGRT('1.000000023206889619'), // 5% annual rate + issuancePerBlock: toGRT('114.155251141552511415'), // 300M GRT/year dripInterval: toBN('50400'), // 1 week in blocks (post-Merge) }, } diff --git a/test/lib/fixtures.ts b/test/lib/fixtures.ts index 8375a86a4..ed6002a2d 100644 --- a/test/lib/fixtures.ts +++ b/test/lib/fixtures.ts @@ -155,6 +155,7 @@ export class NetworkFixture { } else { await l1GraphTokenGateway.connect(deployer).syncAllContracts() await bridgeEscrow.connect(deployer).syncAllContracts() + await grt.connect(deployer).addMinter(l1GraphTokenGateway.address) } await staking.connect(deployer).setSlasher(slasherAddress, true) diff --git a/test/rewards/rewards.test.ts b/test/rewards/rewards.test.ts index beed73a73..3c72214a9 100644 --- a/test/rewards/rewards.test.ts +++ b/test/rewards/rewards.test.ts @@ -9,7 +9,6 @@ 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 { @@ -30,7 +29,7 @@ const MAX_PPM = 1000000 const { HashZero, WeiPerEther } = constants -const toRound = (n: BigNumber) => formatGRT(n).split('.')[0] +const toRound = (n: BigNumber) => formatGRT(n.add(toGRT('0.5'))).split('.')[0] describe('Rewards', () => { let delegator: Account @@ -48,7 +47,6 @@ describe('Rewards', () => { let epochManager: EpochManager let staking: Staking let rewardsManager: RewardsManager - let rewardsManagerMock: RewardsManagerMock // Derive some channel keys for each indexer used to sign attestations const channelKey1 = deriveChannelKey() @@ -62,20 +60,19 @@ 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 + const ISSUANCE_RATE_PERIODS = 4 // blocks required to issue 800 GRT rewards + const ISSUANCE_PER_BLOCK = toBN('200000000000000000000') // 200 GRT 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 => { + const getRewardsPerSignal = (k: 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() + return k.times(t).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) @@ -88,7 +85,6 @@ describe('Rewards', () => { 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 @@ -106,8 +102,7 @@ describe('Rewards', () => { async accruedByElapsed(nBlocks: BigNumber | number) { const n = getRewardsPerSignal( - new BN(this.totalSupply.toString()), - new BN(ISSUANCE_RATE_PER_BLOCK.toString()).div(1e18), + new BN(ISSUANCE_PER_BLOCK.toString()), new BN(nBlocks.toString()), new BN(this.totalSignalled.toString()), ) @@ -143,13 +138,8 @@ describe('Rewards', () => { 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) + // 200 GRT per block + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) // Distribute test funds for (const wallet of [indexer1, indexer2, curator1, curator2]) { @@ -168,28 +158,22 @@ 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')) + describe('issuance per block update', function () { + it('reject set issuance per block if unauthorized', async function () { + const tx = rewardsManager.connect(indexer1.signer).setIssuancePerBlock(toGRT('1.025')) await expect(tx).revertedWith('Only 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 to minimum allowed (0)', async function () { + const newIssuancePerBlock = toGRT('0') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) }) 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) + const newIssuancePerBlock = toGRT('100.025') + await rewardsManager.connect(governor.signer).setIssuancePerBlock(newIssuancePerBlock) + expect(await rewardsManager.issuancePerBlock()).eq(newIssuancePerBlock) expect(await rewardsManager.accRewardsPerSignalLastBlockUpdated()).eq(await latestBlock()) }) }) @@ -245,7 +229,7 @@ describe('Rewards', () => { context('issuing rewards', async function () { beforeEach(async function () { // 5% minute rate (4 blocks) - await rewardsManager.connect(governor.signer).setIssuanceRate(ISSUANCE_RATE_PER_BLOCK) + await rewardsManager.connect(governor.signer).setIssuancePerBlock(ISSUANCE_PER_BLOCK) }) describe('getNewRewardsPerSignal', function () { @@ -450,10 +434,8 @@ describe('Rewards', () => { 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') + const expectedSubgraphRewards = toGRT('1400') // 7 blocks since signaling to when we do getAccRewardsForSubgraph + const expectedRewardsAT = toGRT('0.08') // allocated during 5 blocks: 1000 GRT, divided by 12500 allocated tokens // Update await rewardsManager.onSubgraphAllocationUpdate(subgraphDeploymentID1) @@ -466,7 +448,7 @@ describe('Rewards', () => { const contractRewardsAT = subgraph.accRewardsPerAllocatedToken expect(toRound(expectedSubgraphRewards)).eq(toRound(contractSubgraphRewards)) - expect(toRound(expectedRewardsAT)).eq(toRound(contractRewardsAT)) + expect(toRound(expectedRewardsAT.mul(1000))).eq(toRound(contractRewardsAT.mul(1000))) }) }) @@ -619,13 +601,11 @@ describe('Rewards', () => { 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 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 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -731,13 +711,11 @@ describe('Rewards', () => { 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 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 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 12500. - const expectedIndexingRewards = toGRT('913715958') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 2 blocks after the signal is minted. + // The final snapshot is when we close the allocation, that happens 9 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 7) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('1400') // Close allocation. At this point rewards should be collected for that indexer const tx = await staking @@ -805,13 +783,11 @@ describe('Rewards', () => { // 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 2 blocks after the signal is minted: - // startRewardsPerToken = (10004000000 * 1.01227 ^ 2 - 10004000000) / 14500 = 8466.995 - // The final snapshot is when we close the allocation, that happens 4 blocks later: - // endRewardsPerToken = (10004000000 * 1.01227 ^ 4 - 10004000000) / 14500 = 34496.55 - // Then our expected rewards are (endRewardsPerToken - startRewardsPerToken) * 14500. - const expectedIndexingRewards = toGRT('377428566.77') + // Rewards per token will be (issuancePerBlock * nBlocks) / allocatedTokens + // The first snapshot is after allocating, that is 1 block after the signal is minted. + // The final snapshot is when we close the allocation, that happens 4 blocks after signal is minted. + // So the rewards will be ((issuancePerBlock * 3) / allocatedTokens) * allocatedTokens + const expectedIndexingRewards = toGRT('600') // Calculate delegators cut const indexerRewards = delegationParams.indexingRewardCut .mul(expectedIndexingRewards) @@ -848,18 +824,6 @@ describe('Rewards', () => { }) }) - 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 diff --git a/test/staking/allocation.test.ts b/test/staking/allocation.test.ts index 4d6a9150c..473ecb451 100644 --- a/test/staking/allocation.test.ts +++ b/test/staking/allocation.test.ts @@ -582,6 +582,9 @@ describe('Staking:Allocation', () => { for (const tokensToAllocate of [toBN(100), toBN(0)]) { context(`> with ${tokensToAllocate} allocated tokens`, async function () { beforeEach(async function () { + // Advance to next epoch to avoid creating the allocation + // right at the epoch boundary, which would mess up the tests. + await advanceToNextEpoch(epochManager) await allocate(tokensToAllocate) })