diff --git a/contracts/l2/reservoir/L2Reservoir.sol b/contracts/l2/reservoir/L2Reservoir.sol new file mode 100644 index 000000000..c63a1bdb0 --- /dev/null +++ b/contracts/l2/reservoir/L2Reservoir.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "@openzeppelin/contracts/math/SafeMath.sol"; + +import "../../arbitrum/L2ArbitrumMessenger.sol"; + +import "../../reservoir/IReservoir.sol"; +import "./L2ReservoirStorage.sol"; + +/** + * @title L2 Rewards Reservoir + * @dev TODO + */ +contract L2Reservoir is L2ReservoirV1Storage, Reservoir { + 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); + } + + function getAccumulatedRewards(uint256 blocknum) external override returns (uint256) { + // R(t) = R(t0) + (DeltaR2(t, t0)) + // (deltaRewards implicitly uses lambda because it computes using normalizedTokenSupply) + return accumulatedLayerRewards + deltaRewards(blocknum, lastRewardsUpdateBlock); + } + + function deltaRewards(uint256 t1, uint256 t0) public returns (uint256) { + if (issuanceRate <= MIN_ISSUANCE_RATE) { + return 0; + } + return + normalizedTokenSupplyCache.mul(_pow(issuanceRate, t1.sub(t0), TOKEN_DECIMALS)).div( + TOKEN_DECIMALS + ); + } + + function receiveDrip(uint256 _normalizedTokenSupply) external onlyL2Gateway { + if (_normalizedTokenSupply > normalizedTokenSupplyCache) { + normalizedTokenSupplyCache = _normalizedTokenSupply; + lastRewardsUpdateBlock = block.number; + } + } +} diff --git a/contracts/l2/reservoir/L2ReservoirStorage.sol b/contracts/l2/reservoir/L2ReservoirStorage.sol new file mode 100644 index 000000000..44f5cd018 --- /dev/null +++ b/contracts/l2/reservoir/L2ReservoirStorage.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +import "../../reservoir/IReservoir.sol"; +import "../../governance/Managed.sol"; + +contract L2ReservoirV1Storage is Managed { + uint256 public normalizedTokenSupplyCache; + uint256 public lastRewardsUpdateBlock; +} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol new file mode 100644 index 000000000..82eabf517 --- /dev/null +++ b/contracts/reservoir/IReservoir.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; + +interface IReservoir { + function getAccumulatedRewards(uint256 blocknum) external; +} diff --git a/contracts/reservoir/L1Reservoir.sol b/contracts/reservoir/L1Reservoir.sol new file mode 100644 index 000000000..ea3c8e91a --- /dev/null +++ b/contracts/reservoir/L1Reservoir.sol @@ -0,0 +1,109 @@ +// 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 + * @dev TODO + */ +contract L1Reservoir is ReservoirV1Storage, Reservoir { + using SafeMath for uint256; + + function getAccumulatedRewards(uint256 blocknum) external override returns (uint256) { + // R(t) = R(t0) + (1-lambda) * (DeltaR(t, t0)) + return + accumulatedLayerRewards + + deltaRewards(blocknum, lastRewardsUpdateBlock) + .mul(TOKEN_DECIMALS.sub(l2RewardsFraction)) + .div(TOKEN_DECIMALS); + } + + function drip( + uint256 l2MaxGas, + uint256 l2GasPriceBid, + uint256 l2MaxSubmissionCost + ) external payable { + uint256 mintedRewardsTotal = deltaRewards(rewardsMintedUntilBlock, lastRewardsUpdateBlock); + uint256 mintedRewardsActual = deltaRewards(block.number, lastRewardsUpdateBlock); + // eps = (signed int) mintedRewardsTotal - mintedRewardsActual + + lastRewardsUpdateBlock = block.number; + rewardsMintedUntilBlock = block.number.add(MINT_SUPPLY_PERIOD); + // n: + uint256 newRewardsToDistribute = deltaRewards( + rewardsMintedUntilBlock, + lastRewardsUpdateBlock + ); + // N = n - eps + uint256 tokensToMint = newRewardsToDistribute.add(mintedRewardsActual).sub( + mintedRewardsTotal + ); + + if (tokensToMint > 0) { + graphToken().mint(address(this), tokensToMint); + } + + accumulatedLayerRewards = getAccumulatedRewards(block.number); + + tokenSupplyCache = graphToken().totalSupply(); + + uint256 tokensToSendToL2 = l2RewardsFraction.mul(newRewardsToDistribute).div( + TOKEN_DECIMALS + ); + if (l2RewardsFraction != lastL2RewardsFraction) { + if (mintedRewardsTotal > mintedRewardsActual) { + // eps > 0, i.e. t < t1_old + tokensToSendToL2 = tokensToSendToL2.sub( + lastL2RewardsFraction.mul(mintedRewardsTotal.sub(mintedRewardsActual)) + ); + } else { + tokensToSendToL2 = tokensToSendToL2.add( + lastL2RewardsFraction.mul(mintedRewardsActual.sub(mintedRewardsTotal)) + ); + } + lastL2RewardsFraction = l2RewardsFraction; + } + _sendNewTokensAndStateToL2(tokensToSendToL2, l2MaxSubmissionCost, l2GasPriceBid, l2MaxGas); + } + + function _sendNewTokensAndStateToL2( + uint256 nTokens, + uint256 maxGas, + uint256 gasPriceBid, + uint256 maxSubmissionCost + ) internal { + uint256 normalizedSupply = l2RewardsFraction * tokenSupplyCache; + bytes memory extraData = abi.encodeWithSelector( + L2Reservoir.receiveDrip.selector, + normalizedSupply + ); + bytes memory data = abi.encode(maxSubmissionCost, extraData); + ITokenGateway gateway = ITokenGateway(_resolveContract(keccak256("GraphTokenGateway"))); + gateway.outboundTransfer{ value: msg.value }( + address(graphToken()), + l2ReservoirAddress, + nTokens, + maxGas, + gasPriceBid, + data + ); + } + + function deltaRewards(uint256 t1, uint256 t0) public returns (uint256) { + if (issuanceRate <= MIN_ISSUANCE_RATE) { + return 0; + } + return + tokenSupplyCache.mul(_pow(issuanceRate, t1.sub(t0), TOKEN_DECIMALS)).div( + TOKEN_DECIMALS + ); + } +} diff --git a/contracts/reservoir/Reservoir.sol b/contracts/reservoir/Reservoir.sol new file mode 100644 index 000000000..805867063 --- /dev/null +++ b/contracts/reservoir/Reservoir.sol @@ -0,0 +1,79 @@ +// 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 + * @dev TODO + */ +abstract contract Reservoir is GraphUpgradeable, Managed, IReservoir { + using SafeMath for uint256; + + uint256 internal constant TOKEN_DECIMALS = 1e18; + uint256 internal constant MIN_ISSUANCE_RATE = 1e18; + uint256 internal constant MINT_SUPPLY_PERIOD = 45815; // ~1 week in blocks + + function _pow( + uint256 x, + uint256 n, + uint256 base + ) private 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..e289171b9 --- /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"; + +contract ReservoirV1Storage is Managed { + uint256 public l2RewardsFraction; // Expressed in base 1e18 + uint256 public lastL2RewardsFraction; + address public l2ReservoirAddress; + uint256 public lastRewardsUpdateBlock; + uint256 public rewardsMintedUntilBlock; + uint256 public accumulatedGlobalRewards; + uint256 public accumulatedLayerRewards; + uint256 public tokenSupplyCache; + uint256 public issuanceRate; +} diff --git a/contracts/rewards/RewardsManager.sol b/contracts/rewards/RewardsManager.sol index e16a56f3f..4acebfa16 100644 --- a/contracts/rewards/RewardsManager.sol +++ b/contracts/rewards/RewardsManager.sol @@ -10,6 +10,8 @@ import "../upgrades/GraphUpgradeable.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 @@ -18,7 +20,7 @@ import "./IRewardsManager.sol"; * total rewards for the Subgraph are split up for each Indexer based on much they have Staked on * that Subgraph. */ -contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsManager { +contract RewardsManager is RewardsManagerV3Storage, GraphUpgradeable, IRewardsManager { using SafeMath for uint256; uint256 private constant TOKEN_DECIMALS = 1e18; @@ -155,27 +157,13 @@ contract RewardsManager is RewardsManagerV2Storage, 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; @@ -188,16 +176,14 @@ contract RewardsManager is RewardsManagerV2Storage, 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 + ); } /** @@ -272,7 +258,7 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa function updateAccRewardsPerSignal() public override returns (uint256) { accRewardsPerSignal = getAccRewardsPerSignal(); accRewardsPerSignalLastBlockUpdated = block.number; - tokenSupplySnapshot = graphToken().totalSupply(); + accRewardsOnLastSignalUpdate = reservoir().getAccumulatedRewards(block.number); return accRewardsPerSignal; } @@ -459,4 +445,8 @@ contract RewardsManager is RewardsManagerV2Storage, GraphUpgradeable, IRewardsMa } } } + + function reservoir() private returns (IReservoir) { + return IReservoir(_resolveContract(keccak256("Reservoir"))); + } } diff --git a/contracts/rewards/RewardsManagerStorage.sol b/contracts/rewards/RewardsManagerStorage.sol index c1d9cd7e1..fd16caa1b 100644 --- a/contracts/rewards/RewardsManagerStorage.sol +++ b/contracts/rewards/RewardsManagerStorage.sol @@ -26,3 +26,8 @@ contract RewardsManagerV2Storage is RewardsManagerV1Storage { // Snapshot of the total supply of GRT when accRewardsPerSignal was last updated uint256 public tokenSupplySnapshot; } + +contract RewardsManagerV3Storage is RewardsManagerV2Storage { + // Accumulated rewards at accRewardsPerSignalLastBlockUpdated + uint256 public accRewardsOnLastSignalUpdate; +}