Skip to content

feat: subgraph migration to L2 sending only the owner's tokens (OZ M-01) #764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 3, 2023
187 changes: 96 additions & 91 deletions contracts/discovery/L1GNS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/Sa
import { GNS } from "./GNS.sol";

import { ITokenGateway } from "../arbitrum/ITokenGateway.sol";
import { L1ArbitrumMessenger } from "../arbitrum/L1ArbitrumMessenger.sol";
import { IL2GNS } from "../l2/discovery/IL2GNS.sol";
import { IGraphToken } from "../token/IGraphToken.sol";
import { L1GNSV1Storage } from "./L1GNSStorage.sol";
Expand All @@ -23,22 +22,24 @@ import { L1GNSV1Storage } from "./L1GNSStorage.sol";
* transaction.
* This L1GNS variant includes some functions to allow migrating subgraphs to L2.
*/
contract L1GNS is GNS, L1GNSV1Storage, L1ArbitrumMessenger {
contract L1GNS is GNS, L1GNSV1Storage {
using SafeMathUpgradeable for uint256;

/// @dev Emitted when a subgraph was sent to L2 through the bridge
event SubgraphSentToL2(uint256 indexed _subgraphID, address indexed _l2Owner);
/// @dev Emitted when the address of the Arbitrum Inbox was updated
event ArbitrumInboxAddressUpdated(address _inbox);

/**
* @dev sets the addresses for L1 inbox provided by Arbitrum
* @param _inbox Address of the Inbox that is part of the Arbitrum Bridge
*/
function setArbitrumInboxAddress(address _inbox) external onlyGovernor {
arbitrumInboxAddress = _inbox;
emit ArbitrumInboxAddressUpdated(_inbox);
}
event SubgraphSentToL2(
uint256 indexed _subgraphID,
address indexed _l1Owner,
address indexed _l2Owner,
uint256 _tokens
);

/// @dev Emitted when a curator's balance for a subgraph was sent to L2
event CuratorBalanceSentToL2(
uint256 indexed _subgraphID,
address indexed _l1Curator,
address indexed _l2Beneficiary,
uint256 _tokens
);

/**
* @notice Send a subgraph's data and tokens to L2.
Expand Down Expand Up @@ -72,120 +73,124 @@ contract L1GNS is GNS, L1GNSV1Storage, L1ArbitrumMessenger {
subgraphData.disabled = true;
subgraphData.vSignal = 0;

bytes memory extraData = _encodeSubgraphDataForL2(_subgraphID, _l2Owner, subgraphData);
// We send only the subgraph owner's tokens and nsignal to L2,
// and for everyone else we set the withdrawableGRT so that they can choose
// to withdraw or migrate their signal.
uint256 ownerNSignal = subgraphData.curatorNSignal[msg.sender];
uint256 totalSignal = subgraphData.nSignal;

bytes memory data = abi.encode(_maxSubmissionCost, extraData);
IGraphToken grt = graphToken();
ITokenGateway gateway = graphTokenGateway();
grt.approve(address(gateway), curationTokens);
gateway.outboundTransfer{ value: msg.value }({
_token: address(grt),
_to: counterpartGNSAddress,
_amount: curationTokens,
_maxGas: _maxGas,
_gasPriceBid: _gasPriceBid,
_data: data
});
// Get owner share of tokens to be sent to L2
uint256 tokensForL2 = ownerNSignal.mul(curationTokens).div(totalSignal);
// This leaves the subgraph as if it was deprecated,
// so other curators can withdraw:
subgraphData.curatorNSignal[msg.sender] = 0;
subgraphData.nSignal = totalSignal.sub(ownerNSignal);
subgraphData.withdrawableGRT = curationTokens.sub(tokensForL2);

bytes memory extraData = abi.encode(
uint8(IL2GNS.L1MessageCodes.RECEIVE_SUBGRAPH_CODE),
_subgraphID,
_l2Owner
);

_sendTokensAndMessageToL2GNS(
tokensForL2,
_maxGas,
_gasPriceBid,
_maxSubmissionCost,
extraData
);

subgraphData.reserveRatioDeprecated = 0;
_burnNFT(_subgraphID);
emit SubgraphSentToL2(_subgraphID, _l2Owner);
emit SubgraphSentToL2(_subgraphID, msg.sender, _l2Owner, tokensForL2);
}

/**
* @notice Claim the balance for a curator's signal in a subgraph that was
* migrated to L2, by sending a retryable ticket to the L2GNS.
* @notice Send the balance for a curator's signal in a subgraph that was
* migrated to L2, using the L1GraphTokenGateway.
* The balance will be claimed for a beneficiary address, as this method can be
* used by curators that use a contract address in L1 that may not exist in L2.
* This will set the curator's signal on L1 to zero, so the caller must ensure
* that the retryable ticket is redeemed before expiration, or the signal will be lost.
* It is up to the caller to verify that the subgraph migration was finished in L2,
* but if it wasn't, the tokens will be sent to the beneficiary in L2.
* @dev Use the Arbitrum SDK to estimate the L2 retryable ticket parameters.
* @param _subgraphID Subgraph ID
* @param _beneficiary Address that will receive the tokens in L2
* @param _maxGas Max gas to use for the L2 retryable ticket
* @param _gasPriceBid Gas price bid for the L2 retryable ticket
* @param _maxSubmissionCost Max submission cost for the L2 retryable ticket
* @return The sequence ID for the retryable ticket, as returned by the Arbitrum inbox.
*/
function claimCuratorBalanceToBeneficiaryOnL2(
function sendCuratorBalanceToBeneficiaryOnL2(
uint256 _subgraphID,
address _beneficiary,
uint256 _maxGas,
uint256 _gasPriceBid,
uint256 _maxSubmissionCost
) external payable notPartialPaused returns (bytes memory) {
) external payable notPartialPaused {
require(subgraphMigratedToL2[_subgraphID], "!MIGRATED");

// The Arbitrum bridge will check this too, we just check here for an early exit
require(_maxSubmissionCost != 0, "NO_SUBMISSION_COST");

L2GasParams memory gasParams = L2GasParams(_maxSubmissionCost, _maxGas, _gasPriceBid);

uint256 curatorNSignal = getCuratorSignal(_subgraphID, msg.sender);
SubgraphData storage subgraphData = _getSubgraphData(_subgraphID);
uint256 curatorNSignal = subgraphData.curatorNSignal[msg.sender];
require(curatorNSignal != 0, "NO_SIGNAL");
bytes memory outboundCalldata = getClaimCuratorBalanceOutboundCalldata(
uint256 subgraphNSignal = subgraphData.nSignal;
require(subgraphNSignal != 0, "NO_SUBGRAPH_SIGNAL");

uint256 withdrawableGRT = subgraphData.withdrawableGRT;
uint256 tokensForL2 = curatorNSignal.mul(withdrawableGRT).div(subgraphNSignal);
bytes memory extraData = abi.encode(
uint8(IL2GNS.L1MessageCodes.RECEIVE_CURATOR_BALANCE_CODE),
_subgraphID,
curatorNSignal,
msg.sender,
_beneficiary
);

// Similarly to withdrawing from a deprecated subgraph,
// we remove the curator's signal from the subgraph.
SubgraphData storage subgraphData = _getSubgraphData(_subgraphID);
// Set the subgraph as if the curator had withdrawn their tokens
subgraphData.curatorNSignal[msg.sender] = 0;
subgraphData.nSignal = subgraphData.nSignal.sub(curatorNSignal);

uint256 seqNum = sendTxToL2({
_inbox: arbitrumInboxAddress,
_to: counterpartGNSAddress,
_user: msg.sender,
_l1CallValue: msg.value,
_l2CallValue: 0,
_l2GasParams: gasParams,
_data: outboundCalldata
});

return abi.encode(seqNum);
}

/**
* @notice Get the outbound calldata that will be sent to L2
* when calling claimCuratorBalanceToBeneficiaryOnL2.
* This can be useful to estimate the L2 retryable ticket parameters.
* @param _subgraphID Subgraph ID
* @param _curatorNSignal Curator's signal in the subgraph
* @param _curator Curator address
* @param _beneficiary Address that will own the signal in L2
*/
function getClaimCuratorBalanceOutboundCalldata(
uint256 _subgraphID,
uint256 _curatorNSignal,
address _curator,
address _beneficiary
) public pure returns (bytes memory) {
return
abi.encodeWithSelector(
IL2GNS.claimL1CuratorBalanceToBeneficiary.selector,
_subgraphID,
_curator,
_curatorNSignal,
_beneficiary
);
subgraphData.nSignal = subgraphNSignal.sub(curatorNSignal);
subgraphData.withdrawableGRT = withdrawableGRT.sub(tokensForL2);

// Send the tokens and data to L2 using the L1GraphTokenGateway
_sendTokensAndMessageToL2GNS(
tokensForL2,
_maxGas,
_gasPriceBid,
_maxSubmissionCost,
extraData
);
emit CuratorBalanceSentToL2(_subgraphID, msg.sender, _beneficiary, tokensForL2);
}

/**
* @dev Encodes the subgraph data as callhook parameters
* for the L2 migration.
* @param _subgraphID Subgraph ID
* @param _l2Owner Owner of the subgraph on L2
* @param _subgraphData Subgraph data
* @notice Sends a message to the L2GNS with some extra data,
* also sending some tokens, using the L1GraphTokenGateway.
* @param _tokens Amount of tokens to send to L2
* @param _maxGas Max gas to use for the L2 retryable ticket
* @param _gasPriceBid Gas price bid for the L2 retryable ticket
* @param _maxSubmissionCost Max submission cost for the L2 retryable ticket
* @param _extraData Extra data for the callhook on L2GNS
*/
function _encodeSubgraphDataForL2(
uint256 _subgraphID,
address _l2Owner,
SubgraphData storage _subgraphData
) internal view returns (bytes memory) {
return abi.encode(_subgraphID, _l2Owner, _subgraphData.nSignal);
function _sendTokensAndMessageToL2GNS(
uint256 _tokens,
uint256 _maxGas,
uint256 _gasPriceBid,
uint256 _maxSubmissionCost,
bytes memory _extraData
) internal {
bytes memory data = abi.encode(_maxSubmissionCost, _extraData);
IGraphToken grt = graphToken();
ITokenGateway gateway = graphTokenGateway();
grt.approve(address(gateway), _tokens);
gateway.outboundTransfer{ value: msg.value }(
address(grt),
counterpartGNSAddress,
_tokens,
_maxGas,
_gasPriceBid,
data
);
}
}
2 changes: 0 additions & 2 deletions contracts/discovery/L1GNSStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ pragma abicoder v2;
* reduce the size of the gap accordingly.
*/
abstract contract L1GNSV1Storage {
/// Address of the Arbitrum DelayedInbox
address public arbitrumInboxAddress;
/// True for subgraph IDs that have been migrated to L2
mapping(uint256 => bool) public subgraphMigratedToL2;
/// @dev Storage gap to keep storage slots fixed in future versions
Expand Down
9 changes: 3 additions & 6 deletions contracts/l2/curation/IL2Curation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ interface IL2Curation {
* only during an L1-L2 migration).
* @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal
* @param _tokensIn Amount of Graph Tokens to deposit
* @param _signalOutMin Expected minimum amount of signal to receive
* @return Signal minted
*/
function mintTaxFree(
bytes32 _subgraphDeploymentID,
uint256 _tokensIn,
uint256 _signalOutMin
) external returns (uint256);
function mintTaxFree(bytes32 _subgraphDeploymentID, uint256 _tokensIn)
external
returns (uint256);

/**
* @notice Calculate amount of signal that can be bought with tokens in a curation pool,
Expand Down
17 changes: 7 additions & 10 deletions contracts/l2/curation/L2Curation.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { IRewardsManager } from "../../rewards/IRewardsManager.sol";
import { Managed } from "../../governance/Managed.sol";
import { IGraphToken } from "../../token/IGraphToken.sol";
import { CurationV2Storage } from "../../curation/CurationStorage.sol";
import { ICuration } from "../../curation/ICuration.sol";
import { IGraphCurationToken } from "../../curation/IGraphCurationToken.sol";
import { IL2Curation } from "./IL2Curation.sol";

Expand Down Expand Up @@ -231,23 +230,21 @@ contract L2Curation is CurationV2Storage, GraphUpgradeable, IL2Curation {
* only during an L1-L2 migration).
* @param _subgraphDeploymentID Subgraph deployment pool from where to mint signal
* @param _tokensIn Amount of Graph Tokens to deposit
* @param _signalOutMin Expected minimum amount of signal to receive
* @return Signal minted
*/
function mintTaxFree(
bytes32 _subgraphDeploymentID,
uint256 _tokensIn,
uint256 _signalOutMin
) external override notPartialPaused onlyGNS returns (uint256) {
function mintTaxFree(bytes32 _subgraphDeploymentID, uint256 _tokensIn)
external
override
notPartialPaused
onlyGNS
returns (uint256)
{
// Need to deposit some funds
require(_tokensIn != 0, "Cannot deposit zero tokens");

// Exchange GRT tokens for GCS of the subgraph pool (no tax)
uint256 signalOut = _tokensToSignal(_subgraphDeploymentID, _tokensIn);

// Slippage protection
require(signalOut >= _signalOutMin, "Slippage protection");

address curator = msg.sender;
CurationPool storage curationPool = pools[_subgraphDeploymentID];

Expand Down
30 changes: 5 additions & 25 deletions contracts/l2/discovery/IL2GNS.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { ICallhookReceiver } from "../../gateway/ICallhookReceiver.sol";
* @title Interface for the L2GNS contract.
*/
interface IL2GNS is ICallhookReceiver {
enum L1MessageCodes {
RECEIVE_SUBGRAPH_CODE,
RECEIVE_CURATOR_BALANCE_CODE
}

/**
* @dev The SubgraphL2MigrationData struct holds information
* about a subgraph related to its migration from L1 to L2.
Expand All @@ -34,29 +39,4 @@ interface IL2GNS is ICallhookReceiver {
bytes32 _subgraphMetadata,
bytes32 _versionMetadata
) external;

/**
* @notice Deprecate a subgraph that was migrated from L1, but for which
* the migration was never finished. Anyone can call this function after a certain amount of
* blocks have passed since the subgraph was migrated, if the subgraph owner didn't
* call finishSubgraphMigrationFromL1. In L2GNS this timeout is the FINISH_MIGRATION_TIMEOUT constant.
* @param _subgraphID Subgraph ID
*/
function deprecateSubgraphMigratedFromL1(uint256 _subgraphID) external;

/**
* @notice Claim curator balance belonging to a curator from L1.
* This will be credited to the a beneficiary on L2, and can only be called
* from the GNS on L1 through a retryable ticket.
* @param _subgraphID Subgraph on which to claim the balance
* @param _curator Curator who owns the balance on L1
* @param _balance Balance of the curator from L1
* @param _beneficiary Address of an L2 beneficiary for the balance
*/
function claimL1CuratorBalanceToBeneficiary(
uint256 _subgraphID,
address _curator,
uint256 _balance,
address _beneficiary
) external;
}
Loading