From 1480c647eca8471062325b867af72d8b9ac87f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Carranza=20V=C3=A9lez?= Date: Sun, 5 Jun 2022 11:42:22 -0700 Subject: [PATCH] feat: use Gelato rewards for drip (WIP, experimental, doesn't work yet) --- contracts/gelato/OpsReady.sol | 38 +++++++++++++ .../l2/reservoir/L2ReservoirDripKeeper.sol | 51 ++++++++++++++++++ contracts/reservoir/IReservoir.sol | 26 +++++++++ contracts/reservoir/L1ReservoirDripKeeper.sol | 54 +++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 contracts/gelato/OpsReady.sol create mode 100644 contracts/l2/reservoir/L2ReservoirDripKeeper.sol create mode 100644 contracts/reservoir/L1ReservoirDripKeeper.sol diff --git a/contracts/gelato/OpsReady.sol b/contracts/gelato/OpsReady.sol new file mode 100644 index 000000000..584bbd9ad --- /dev/null +++ b/contracts/gelato/OpsReady.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: UNLICENSED + +// Copied from https://docs.gelato.network/developer-products/gelato-ops-smart-contract-automation-hub/paying-for-your-transactions +// Modified to use our solidity version. + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import { SafeERC20, IERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +interface IOps { + function gelato() external view returns (address payable); +} + +abstract contract OpsReady { + address public immutable ops; + address payable public immutable gelato; + address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + constructor(address _ops) { + ops = _ops; + gelato = IOps(_ops).gelato(); + } + + modifier onlyOps() { + require(msg.sender == ops, "OpsReady: onlyOps"); + _; + } + + function _transfer(uint256 _amount, address _paymentToken) internal { + if (_paymentToken == ETH) { + (bool success, ) = gelato.call{ value: _amount }(""); + require(success, "_transfer: ETH transfer failed"); + } else { + SafeERC20.safeTransfer(IERC20(_paymentToken), gelato, _amount); + } + } +} diff --git a/contracts/l2/reservoir/L2ReservoirDripKeeper.sol b/contracts/l2/reservoir/L2ReservoirDripKeeper.sol new file mode 100644 index 000000000..bd39abcc5 --- /dev/null +++ b/contracts/l2/reservoir/L2ReservoirDripKeeper.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "../../reservoir/IReservoir.sol"; +import "../../gelato/OpsReady.sol"; +import "../../governance/Managed.sol"; +import "../../upgrades/GraphUpgradeable.sol"; + +import "arbos-precompiles/arbos/builtin/ArbRetryableTx.sol"; + +interface INextDripNonce { + function nextDripNonce() external returns (uint256); +} + +contract L2ReservoirDripKeeper is OpsReady, GraphUpgradeable, Managed { + address internal constant ARB_TX_ADDRESS = address(0x000000000000000000000000000000000000006E); + + /** + * @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 Redeem a retryable ticket for the L2Reservoir receiveDrip + * This currently won't work :( because ArbRetryableTx.redeem reverts + * when called by a contract. + * @param txId + */ + function redeemDripWithReward(bytes32 txId) external onlyOps { + uint256 fee; + address feeToken; + + (fee, feeToken) = IOps(ops).getFeeDetails(); + + // Transfer the reward to Gelato + _transfer(fee, feeToken); + + INextDripNonce reservoir = INextDripNonce(_resolveContract(keccak256("Reservoir"))); + uint256 beforeNonce = reservoir.nextDripNonce(); + ArbRetryableTx(ARB_TX_ADDRESS).redeem(txId); + uint256 afterNonce = reservoir.nextDripNonce(); + // Only pay reward if the ticket caused the nonce to increase, i.e. it called receiveDrip successfully + require(beforeNonce != afterNonce, "TX_DID_NOT_DRIP"); + } +} diff --git a/contracts/reservoir/IReservoir.sol b/contracts/reservoir/IReservoir.sol index dfc64f14f..6eabcb697 100644 --- a/contracts/reservoir/IReservoir.sol +++ b/contracts/reservoir/IReservoir.sol @@ -56,3 +56,29 @@ interface IL2Reservoir is IReservoir { uint256 _nonce ) external; } + +/** + * @title Interface for the L1 Rewards Reservoir + * @dev This exposes a specific function for the L1Reservoir that is called + * by the ReservoirDripKeeper. + */ +interface IL1Reservoir is IReservoir { + /** + * @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; +} diff --git a/contracts/reservoir/L1ReservoirDripKeeper.sol b/contracts/reservoir/L1ReservoirDripKeeper.sol new file mode 100644 index 000000000..462893f15 --- /dev/null +++ b/contracts/reservoir/L1ReservoirDripKeeper.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +pragma solidity ^0.7.6; +pragma abicoder v2; + +import "./IReservoir.sol"; +import "../gelato/OpsReady.sol"; +import "../governance/Managed.sol"; +import "../upgrades/GraphUpgradeable.sol"; + +contract L1ReservoirDripKeeper is OpsReady, GraphUpgradeable, Managed { + /** + * @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 Drip indexer rewards for layers 1 and 2, sending a Gelato reward + * 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 dripWithReward( + uint256 l2MaxGas, + uint256 l2GasPriceBid, + uint256 l2MaxSubmissionCost + ) external payable onlyOps { + uint256 fee; + address feeToken; + + (fee, feeToken) = IOps(ops).getFeeDetails(); + + // Transfer the reward to Gelato + _transfer(fee, feeToken); + + // (After Nitro) this will revert if we need to send to L2 and maxSubmissionCost is insufficient. + IReservoir(_resolveContract(keccak256("Reservoir"))).drip( + l2MaxGas, + l2GasPriceBid, + l2MaxSubmissionCost + ); + } +}