diff --git a/contracts/discovery/L1GNS.sol b/contracts/discovery/L1GNS.sol index 82b34bc2c..3e8d94360 100644 --- a/contracts/discovery/L1GNS.sol +++ b/contracts/discovery/L1GNS.sol @@ -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"; @@ -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. @@ -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 + ); } } diff --git a/contracts/discovery/L1GNSStorage.sol b/contracts/discovery/L1GNSStorage.sol index ecd0de319..64163636e 100644 --- a/contracts/discovery/L1GNSStorage.sol +++ b/contracts/discovery/L1GNSStorage.sol @@ -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 diff --git a/contracts/l2/curation/IL2Curation.sol b/contracts/l2/curation/IL2Curation.sol index 4d54b4804..bd8806538 100644 --- a/contracts/l2/curation/IL2Curation.sol +++ b/contracts/l2/curation/IL2Curation.sol @@ -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, diff --git a/contracts/l2/curation/L2Curation.sol b/contracts/l2/curation/L2Curation.sol index 638870c0e..7c493a612 100644 --- a/contracts/l2/curation/L2Curation.sol +++ b/contracts/l2/curation/L2Curation.sol @@ -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"; @@ -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]; diff --git a/contracts/l2/discovery/IL2GNS.sol b/contracts/l2/discovery/IL2GNS.sol index 1f9f77c6e..227625928 100644 --- a/contracts/l2/discovery/IL2GNS.sol +++ b/contracts/l2/discovery/IL2GNS.sol @@ -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. @@ -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; } diff --git a/contracts/l2/discovery/L2GNS.sol b/contracts/l2/discovery/L2GNS.sol index 76b12eb1d..dd31c4726 100644 --- a/contracts/l2/discovery/L2GNS.sol +++ b/contracts/l2/discovery/L2GNS.sol @@ -5,9 +5,7 @@ pragma abicoder v2; import { SafeMathUpgradeable } from "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import { AddressAliasHelper } from "../../arbitrum/AddressAliasHelper.sol"; import { GNS } from "../../discovery/GNS.sol"; -import { IGNS } from "../../discovery/IGNS.sol"; import { ICuration } from "../../curation/ICuration.sol"; import { IL2GNS } from "./IL2GNS.sol"; import { L2GNSV1Storage } from "./L2GNSStorage.sol"; @@ -28,20 +26,22 @@ import { IL2Curation } from "../curation/IL2Curation.sol"; contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { using SafeMathUpgradeable for uint256; - /// The amount of time (in blocks) that a subgraph owner has to finish the migration - /// from L1 before the subgraph can be deprecated: 1 week - uint256 public constant FINISH_MIGRATION_TIMEOUT = 50400; - /// @dev Emitted when a subgraph is received from L1 through the bridge - event SubgraphReceivedFromL1(uint256 indexed _subgraphID); + event SubgraphReceivedFromL1( + uint256 indexed _subgraphID, + address indexed _owner, + uint256 _tokens + ); /// @dev Emitted when a subgraph migration from L1 is finalized, so the subgraph is published event SubgraphMigrationFinalized(uint256 indexed _subgraphID); /// @dev Emitted when the L1 balance for a curator has been claimed - event CuratorBalanceClaimed( - uint256 indexed _subgraphID, - address indexed _l1Curator, - address indexed _l2Curator, - uint256 _nSignalClaimed + event CuratorBalanceReceived(uint256 _subgraphID, address _l2Curator, uint256 _tokens); + /// @dev Emitted when the L1 balance for a curator has been returned to the beneficiary. + /// This can happen if the subgraph migration was not finished when the curator's tokens arrived. + event CuratorBalanceReturnedToBeneficiary( + uint256 _subgraphID, + address _l2Curator, + uint256 _tokens ); /** @@ -52,24 +52,18 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { _; } - /** - * @dev Checks that the sender is the L2 alias of the counterpart - * GNS on L1. - */ - modifier onlyL1Counterpart() { - require( - msg.sender == AddressAliasHelper.applyL1ToL2Alias(counterpartGNSAddress), - "ONLY_COUNTERPART_GNS" - ); - _; - } - /** * @notice Receive tokens with a callhook from the bridge. - * The callhook will receive a subgraph from L1. The _data parameter + * The callhook will receive a subgraph or a curator's balance from L1. The _data parameter * must contain the ABI encoding of: - * (uint256 subgraphID, address subgraphOwner, uint256 nSignal) - * This is encoded by _encodeSubgraphDataForL2 in L1GNS. + * (uint8 code, uint256 subgraphId, address beneficiary) + * Where `code` is one of the codes defined in IL2GNS.L1MessageCodes. + * If the code is RECEIVE_SUBGRAPH_CODE, the beneficiary is the address of the + * owner of the subgraph on L2. + * If the code is RECEIVE_CURATOR_BALANCE_CODE, then the beneficiary is the + * address of the curator in L2. In this case, If the subgraph migration was never finished + * (or the subgraph doesn't exist), the tokens will be sent to the curator. + * @dev This function is called by the L2GraphTokenGateway contract. * @param _from Token sender in L1 (must be the L1GNS) * @param _amount Amount of tokens that were transferred * @param _data ABI-encoded callhook data @@ -80,12 +74,18 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { bytes calldata _data ) external override notPartialPaused onlyL2Gateway { require(_from == counterpartGNSAddress, "ONLY_L1_GNS_THROUGH_BRIDGE"); - (uint256 subgraphID, address subgraphOwner, uint256 nSignal) = abi.decode( + (uint8 code, uint256 subgraphID, address beneficiary) = abi.decode( _data, - (uint256, address, uint256) + (uint8, uint256, address) ); - _receiveSubgraphFromL1(subgraphID, subgraphOwner, _amount, nSignal); + if (code == uint8(L1MessageCodes.RECEIVE_SUBGRAPH_CODE)) { + _receiveSubgraphFromL1(subgraphID, beneficiary, _amount); + } else if (code == uint8(L1MessageCodes.RECEIVE_CURATOR_BALANCE_CODE)) { + _mintSignalFromL1(subgraphID, beneficiary, _amount); + } else { + revert("INVALID_CODE"); + } } /** @@ -105,7 +105,6 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { ) external override notPartialPaused onlySubgraphAuth(_subgraphID) { IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); - // A subgraph require(migratedData.subgraphReceivedOnL2BlockNumber != 0, "INVALID_SUBGRAPH"); require(!migratedData.l2Done, "ALREADY_DONE"); migratedData.l2Done = true; @@ -116,9 +115,14 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { IL2Curation curation = IL2Curation(address(curation())); // Update pool: constant nSignal, vSignal can change (w/no slippage protection) // Buy all signal from the new deployment - subgraphData.vSignal = curation.mintTaxFree(_subgraphDeploymentID, migratedData.tokens, 0); - subgraphData.disabled = false; + uint256 vSignal = curation.mintTaxFree(_subgraphDeploymentID, migratedData.tokens); + uint256 nSignal = vSignalToNSignal(_subgraphID, vSignal); + subgraphData.disabled = false; + subgraphData.vSignal = vSignal; + subgraphData.nSignal = nSignal; + subgraphData.curatorNSignal[msg.sender] = nSignal; + subgraphData.subgraphDeploymentID = _subgraphDeploymentID; // Set the token metadata _setSubgraphMetadata(_subgraphID, _subgraphMetadata); @@ -129,73 +133,14 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { migratedData.tokens, _subgraphDeploymentID ); - // Update target deployment - subgraphData.subgraphDeploymentID = _subgraphDeploymentID; emit SubgraphVersionUpdated(_subgraphID, _subgraphDeploymentID, _versionMetadata); emit SubgraphMigrationFinalized(_subgraphID); } - /** - * @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 - override - notPartialPaused - { - IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; - require(migratedData.subgraphReceivedOnL2BlockNumber != 0, "INVALID_SUBGRAPH"); - require(!migratedData.l2Done, "ALREADY_FINISHED"); - require( - block.number > migratedData.subgraphReceivedOnL2BlockNumber + FINISH_MIGRATION_TIMEOUT, - "TOO_EARLY" - ); - SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); - - migratedData.l2Done = true; - uint256 withdrawableGRT = migratedData.tokens; - subgraphData.withdrawableGRT = withdrawableGRT; - subgraphData.reserveRatioDeprecated = 0; - _burnNFT(_subgraphID); - emit SubgraphDeprecated(_subgraphID, withdrawableGRT); - } - - /** - * @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 override notPartialPaused onlyL1Counterpart { - IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; - - require(migratedData.l2Done, "!MIGRATED"); - require(!migratedData.curatorBalanceClaimed[_curator], "ALREADY_CLAIMED"); - - SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); - subgraphData.curatorNSignal[_beneficiary] = subgraphData.curatorNSignal[_beneficiary].add( - _balance - ); - migratedData.curatorBalanceClaimed[_curator] = true; - emit CuratorBalanceClaimed(_subgraphID, _curator, _beneficiary, _balance); - } - /** * @notice Publish a new version of an existing subgraph. * @dev This is the same as the one in the base GNS, but skips the check for - * a subgraph to not be pre-curated, as the reserve ration in L2 is set to 1, + * a subgraph to not be pre-curated, as the reserve ratio in L2 is set to 1, * which prevents the risk of rug-pulling. * @param _subgraphID Subgraph ID * @param _subgraphDeploymentID Subgraph deployment ID of the new version @@ -270,13 +215,11 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { * @param _subgraphID Subgraph ID * @param _subgraphOwner Owner of the subgraph * @param _tokens Tokens to be deposited in the subgraph - * @param _nSignal Name signal for the subgraph in L1 */ function _receiveSubgraphFromL1( uint256 _subgraphID, address _subgraphOwner, - uint256 _tokens, - uint256 _nSignal + uint256 _tokens ) internal { IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); @@ -284,16 +227,56 @@ contract L2GNS is GNS, L2GNSV1Storage, IL2GNS { subgraphData.reserveRatioDeprecated = fixedReserveRatio; // The subgraph will be disabled until finishSubgraphMigrationFromL1 is called subgraphData.disabled = true; - subgraphData.nSignal = _nSignal; migratedData.tokens = _tokens; migratedData.subgraphReceivedOnL2BlockNumber = block.number; // Mint the NFT. Use the subgraphID as tokenID. // This function will check the if tokenID already exists. + // Note we do this here so that we can later do the onlySubgraphAuth + // check in finishSubgraphMigrationFromL1. _mintNFT(_subgraphOwner, _subgraphID); - emit SubgraphReceivedFromL1(_subgraphID); + emit SubgraphReceivedFromL1(_subgraphID, _subgraphOwner, _tokens); + } + + /** + * @notice Deposit GRT into a subgraph and mint signal, using tokens received from L1. + * If the subgraph migration was never finished (or the subgraph doesn't exist), the tokens will be sent to the curator. + * @dev This looks a lot like GNS.mintSignal, but doesn't pull the tokens from the + * curator and has no slippage protection. + * @param _subgraphID Subgraph ID + * @param _curator Curator address + * @param _tokensIn The amount of tokens the nameCurator wants to deposit + */ + function _mintSignalFromL1( + uint256 _subgraphID, + address _curator, + uint256 _tokensIn + ) internal { + IL2GNS.SubgraphL2MigrationData storage migratedData = subgraphL2MigrationData[_subgraphID]; + SubgraphData storage subgraphData = _getSubgraphData(_subgraphID); + + // If subgraph migration wasn't finished, we should send the tokens to the curator + if (!migratedData.l2Done || subgraphData.disabled) { + graphToken().transfer(_curator, _tokensIn); + emit CuratorBalanceReturnedToBeneficiary(_subgraphID, _curator, _tokensIn); + } else { + // Get name signal to mint for tokens deposited + IL2Curation curation = IL2Curation(address(curation())); + uint256 vSignal = curation.mintTaxFree(subgraphData.subgraphDeploymentID, _tokensIn); + uint256 nSignal = vSignalToNSignal(_subgraphID, vSignal); + + // Update pools + subgraphData.vSignal = subgraphData.vSignal.add(vSignal); + subgraphData.nSignal = subgraphData.nSignal.add(nSignal); + subgraphData.curatorNSignal[_curator] = subgraphData.curatorNSignal[_curator].add( + nSignal + ); + + emit SignalMinted(_subgraphID, _curator, nSignal, vSignal, _tokensIn); + emit CuratorBalanceReceived(_subgraphID, _curator, _tokensIn); + } } /** diff --git a/test/gns.test.ts b/test/gns.test.ts index b24c359d3..93ea11d57 100644 --- a/test/gns.test.ts +++ b/test/gns.test.ts @@ -235,10 +235,13 @@ describe('L1GNS', () => { // Give some funds to the signers and approve gns contract to use funds on signers behalf await grt.connect(governor.signer).mint(me.address, tokens100000) await grt.connect(governor.signer).mint(other.address, tokens100000) + await grt.connect(governor.signer).mint(another.address, tokens100000) await grt.connect(me.signer).approve(gns.address, tokens100000) await grt.connect(me.signer).approve(curation.address, tokens100000) await grt.connect(other.signer).approve(gns.address, tokens100000) await grt.connect(other.signer).approve(curation.address, tokens100000) + await grt.connect(another.signer).approve(gns.address, tokens100000) + await grt.connect(another.signer).approve(curation.address, tokens100000) // Update curation tax to test the functionality of it in disableNameSignal() await curation.connect(governor.signer).setCurationTaxPercentage(curationTaxPercentage) @@ -303,22 +306,6 @@ describe('L1GNS', () => { }) }) - describe('setArbitrumInboxAddress', function () { - it('should set `arbitrumInboxAddress`', async function () { - // Can set if allowed - const newValue = other.address - const tx = gns.connect(governor.signer).setArbitrumInboxAddress(newValue) - await expect(tx).emit(gns, 'ArbitrumInboxAddressUpdated').withArgs(newValue) - expect(await gns.arbitrumInboxAddress()).eq(newValue) - }) - - it('reject set `arbitrumInboxAddress` if not allowed', async function () { - const newValue = other.address - const tx = gns.connect(me.signer).setArbitrumInboxAddress(newValue) - await expect(tx).revertedWith('Only Controller governor') - }) - }) - describe('setSubgraphNFT', function () { it('should set `setSubgraphNFT`', async function () { const newValue = gns.address // I just use any contract address @@ -1032,7 +1019,8 @@ describe('L1GNS', () => { const subgraph0 = await publishNewSubgraph(me, newSubgraph0, gns) // Curate on the subgraph await gns.connect(me.signer).mintSignal(subgraph0.id, toGRT('90000'), 0) - + // Add an additional curator that is not the owner + await gns.connect(other.signer).mintSignal(subgraph0.id, toGRT('10000'), 0) return subgraph0 } @@ -1048,12 +1036,21 @@ describe('L1GNS', () => { const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') + + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const tx = gns .connect(me.signer) .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address) + + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) return subgraph0 } const publishAndCurateOnLegacySubgraph = async function (seqID: BigNumber): Promise { @@ -1082,6 +1079,8 @@ describe('L1GNS', () => { const curatedTokens = await grt.balanceOf(curation.address) const subgraphBefore = await gns.subgraphs(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') @@ -1090,27 +1089,30 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraph0.id, other.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, other.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, other.address, expectedSentToL2) + const expectedRemainingTokens = curatedTokens.sub(expectedSentToL2) const subgraphAfter = await gns.subgraphs(subgraph0.id) expect(subgraphAfter.vSignal).eq(0) - expect(await grt.balanceOf(gns.address)).eq(0) + expect(await grt.balanceOf(gns.address)).eq(expectedRemainingTokens) expect(subgraphAfter.disabled).eq(true) - expect(subgraphAfter.withdrawableGRT).eq(0) + expect(subgraphAfter.withdrawableGRT).eq(expectedRemainingTokens) const migrated = await gns.subgraphMigratedToL2(subgraph0.id) expect(migrated).eq(true) const expectedCallhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [subgraph0.id, other.address, subgraphBefore.nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), subgraph0.id, other.address], // code = 0 means RECEIVE_SUBGRAPH_CODE ) - const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, gns.address, mockL2GNS.address, - curatedTokens, + expectedSentToL2, expectedCallhookData, ) await expect(tx) @@ -1120,8 +1122,11 @@ describe('L1GNS', () => { it('sends tokens and calldata for a legacy subgraph to L2 through the GRT bridge', async function () { const seqID = toBN('2') const subgraphID = await publishAndCurateOnLegacySubgraph(seqID) - const curatedTokens = await grt.balanceOf(curation.address) + const subgraphBefore = await legacyGNSMock.legacySubgraphData(me.address, seqID) + const curatedTokens = await legacyGNSMock.subgraphTokens(subgraphID) + const beforeOwnerSignal = await legacyGNSMock.getCuratorSignal(subgraphID, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) const maxSubmissionCost = toBN('100') const maxGas = toBN('10') @@ -1131,27 +1136,30 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraphID, other.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(legacyGNSMock, 'SubgraphSentToL2').withArgs(subgraphID, other.address) + await expect(tx) + .emit(legacyGNSMock, 'SubgraphSentToL2') + .withArgs(subgraphID, me.address, other.address, expectedSentToL2) + const expectedRemainingTokens = curatedTokens.sub(expectedSentToL2) const subgraphAfter = await legacyGNSMock.legacySubgraphData(me.address, seqID) expect(subgraphAfter.vSignal).eq(0) - expect(await grt.balanceOf(legacyGNSMock.address)).eq(0) + expect(await grt.balanceOf(legacyGNSMock.address)).eq(expectedRemainingTokens) expect(subgraphAfter.disabled).eq(true) - expect(subgraphAfter.withdrawableGRT).eq(0) + expect(subgraphAfter.withdrawableGRT).eq(expectedRemainingTokens) const migrated = await legacyGNSMock.subgraphMigratedToL2(subgraphID) expect(migrated).eq(true) const expectedCallhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [subgraphID, other.address, subgraphBefore.nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), subgraphID, other.address], // code = 0 means RECEIVE_SUBGRAPH_CODE ) const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( grt.address, legacyGNSMock.address, mockL2GNS.address, - curatedTokens, + expectedSentToL2, expectedCallhookData, ) await expect(tx) @@ -1174,6 +1182,11 @@ describe('L1GNS', () => { it('rejects calls for a subgraph that was already sent', async function () { const subgraph0 = await publishAndCurateOnSubgraph() + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') @@ -1182,7 +1195,9 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) const tx2 = gns .connect(me.signer) @@ -1224,6 +1239,11 @@ describe('L1GNS', () => { it('does not allow curators to burn signal after sending', async function () { const subgraph0 = await publishAndCurateOnSubgraph() + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') @@ -1232,14 +1252,23 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) const tx2 = gns.connect(me.signer).burnSignal(subgraph0.id, toBN(1), toGRT('0')) await expect(tx2).revertedWith('GNS: Must be active') + const tx3 = gns.connect(other.signer).burnSignal(subgraph0.id, toBN(1), toGRT('0')) + await expect(tx3).revertedWith('GNS: Must be active') }) it('does not allow curators to transfer signal after sending', async function () { const subgraph0 = await publishAndCurateOnSubgraph() + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') @@ -1248,14 +1277,23 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) const tx2 = gns.connect(me.signer).transferSignal(subgraph0.id, other.address, toBN(1)) await expect(tx2).revertedWith('GNS: Must be active') + const tx3 = gns.connect(other.signer).transferSignal(subgraph0.id, me.address, toBN(1)) + await expect(tx3).revertedWith('GNS: Must be active') }) - it('does not allow curators to withdraw GRT after sending', async function () { + it('does not allow the owner to withdraw GRT after sending', async function () { const subgraph0 = await publishAndCurateOnSubgraph() + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') @@ -1264,38 +1302,68 @@ describe('L1GNS', () => { .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), }) - await expect(tx).emit(gns, 'SubgraphSentToL2').withArgs(subgraph0.id, me.address) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) const tx2 = gns.connect(me.signer).withdraw(subgraph0.id) - await expect(tx2).revertedWith('GNS: No more GRT to withdraw') + await expect(tx2).revertedWith('GNS: No signal to withdraw GRT') }) - }) - describe('claimCuratorBalanceToBeneficiaryOnL2', function () { - beforeEach(async function () { - await gns.connect(governor.signer).setArbitrumInboxAddress(arbitrumMocks.inboxMock.address) - await legacyGNSMock - .connect(governor.signer) - .setArbitrumInboxAddress(arbitrumMocks.inboxMock.address) + it('allows a curator that is not the owner to withdraw GRT after sending', async function () { + const subgraph0 = await publishAndCurateOnSubgraph() + + const subgraphBefore = await gns.subgraphs(subgraph0.id) + const curatedTokens = await gns.subgraphTokens(subgraph0.id) + const beforeOwnerSignal = await gns.getCuratorSignal(subgraph0.id, me.address) + const expectedSentToL2 = beforeOwnerSignal.mul(curatedTokens).div(subgraphBefore.nSignal) + const beforeOtherSignal = await gns.getCuratorSignal(subgraph0.id, other.address) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + const tx = gns + .connect(me.signer) + .sendSubgraphToL2(subgraph0.id, me.address, maxGas, gasPriceBid, maxSubmissionCost, { + value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), + }) + await expect(tx) + .emit(gns, 'SubgraphSentToL2') + .withArgs(subgraph0.id, me.address, me.address, expectedSentToL2) + + const remainingTokens = (await gns.subgraphs(subgraph0.id)).withdrawableGRT + const tx2 = gns.connect(other.signer).withdraw(subgraph0.id) + await expect(tx2) + .emit(gns, 'GRTWithdrawn') + .withArgs(subgraph0.id, other.address, beforeOtherSignal, remainingTokens) }) - it('sends a transaction with a curator balance to the L2GNS using the Arbitrum inbox', async function () { - let beforeCuratorNSignal: BigNumber - const subgraph0 = await publishCurateAndSendSubgraph(async (subgraphID) => { - beforeCuratorNSignal = await gns.getCuratorSignal(subgraphID, me.address) - }) + }) + describe('sendCuratorBalanceToBeneficiaryOnL2', function () { + it('sends a transaction with a curator balance to the L2GNS using the L1 gateway', async function () { + const subgraph0 = await publishCurateAndSendSubgraph() + const afterSubgraph = await gns.subgraphs(subgraph0.id) + const curatorTokens = afterSubgraph.withdrawableGRT - const expectedCalldata = l2GNSIface.encodeFunctionData( - 'claimL1CuratorBalanceToBeneficiary', - [subgraph0.id, me.address, beforeCuratorNSignal, other.address], + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), subgraph0.id, another.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE + ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + gns.address, + mockL2GNS.address, + curatorTokens, + expectedCallhookData, ) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') const tx = gns - .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, - other.address, + another.address, maxGas, gasPriceBid, maxSubmissionCost, @@ -1306,26 +1374,36 @@ describe('L1GNS', () => { // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 await expect(tx) - .emit(gns, 'TxToL2') - .withArgs(me.address, mockL2GNS.address, toBN('2'), expectedCalldata) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(gns.address, mockL2Gateway.address, toBN('2'), expectedL2Data) + await expect(tx) + .emit(gns, 'CuratorBalanceSentToL2') + .withArgs(subgraph0.id, other.address, another.address, curatorTokens) }) it('sets the curator signal to zero so it cannot be called twice', async function () { - let beforeCuratorNSignal: BigNumber - const subgraph0 = await publishCurateAndSendSubgraph(async (subgraphID) => { - beforeCuratorNSignal = await gns.getCuratorSignal(subgraphID, me.address) - }) + const subgraph0 = await publishCurateAndSendSubgraph() + const afterSubgraph = await gns.subgraphs(subgraph0.id) + const curatorTokens = afterSubgraph.withdrawableGRT - const expectedCalldata = l2GNSIface.encodeFunctionData( - 'claimL1CuratorBalanceToBeneficiary', - [subgraph0.id, me.address, beforeCuratorNSignal, other.address], + const expectedCallhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), subgraph0.id, other.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE ) + const expectedL2Data = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + gns.address, + mockL2GNS.address, + curatorTokens, + expectedCallhookData, + ) + const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') const tx = gns - .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, other.address, maxGas, @@ -1338,13 +1416,12 @@ describe('L1GNS', () => { // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 await expect(tx) - .emit(gns, 'TxToL2') - .withArgs(me.address, mockL2GNS.address, toBN('2'), expectedCalldata) - expect(await gns.getCuratorSignal(subgraph0.id, me.address)).to.equal(toBN(0)) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(gns.address, mockL2Gateway.address, toBN('2'), expectedL2Data) const tx2 = gns - .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, other.address, maxGas, @@ -1356,30 +1433,76 @@ describe('L1GNS', () => { ) await expect(tx2).revertedWith('NO_SIGNAL') }) - it('sends a transaction with a curator balance from a legacy subgraph to the L2GNS', async function () { - const subgraphID = await publishAndCurateOnLegacySubgraph(toBN('2')) - - const beforeCuratorNSignal = await legacyGNSMock.getCuratorSignal(subgraphID, me.address) + it('sets the curator signal to zero so they cannot withdraw', async function () { + const subgraph0 = await publishCurateAndSendSubgraph(async (_subgraphId) => { + // We add another curator before migrating, so the the subgraph doesn't + // run out of withdrawable GRT and we can test that it denies the specific curator + // because they have sent their signal to L2, not because the subgraph is out of GRT. + await gns.connect(another.signer).mintSignal(_subgraphId, toGRT('1000'), toBN(0)) + }) const maxSubmissionCost = toBN('100') const maxGas = toBN('10') const gasPriceBid = toBN('20') - const tx = legacyGNSMock - .connect(me.signer) - .sendSubgraphToL2(subgraphID, me.address, maxGas, gasPriceBid, maxSubmissionCost, { - value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), - }) - await expect(tx).emit(legacyGNSMock, 'SubgraphSentToL2').withArgs(subgraphID, me.address) - const expectedCalldata = l2GNSIface.encodeFunctionData( - 'claimL1CuratorBalanceToBeneficiary', - [subgraphID, me.address, beforeCuratorNSignal, other.address], + await gns + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( + subgraph0.id, + other.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + { + value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), + }, + ) + + const tx = gns.connect(other.signer).withdraw(subgraph0.id) + await expect(tx).revertedWith('GNS: No signal to withdraw GRT') + }) + it('gives each curator an amount of tokens proportional to their nSignal', async function () { + let beforeOtherNSignal: BigNumber + let beforeAnotherNSignal: BigNumber + const subgraph0 = await publishCurateAndSendSubgraph(async (subgraphID) => { + beforeOtherNSignal = await gns.getCuratorSignal(subgraphID, other.address) + await gns.connect(another.signer).mintSignal(subgraphID, toGRT('10000'), 0) + beforeAnotherNSignal = await gns.getCuratorSignal(subgraphID, another.address) + }) + const afterSubgraph = await gns.subgraphs(subgraph0.id) + + // Compute how much is owed to each curator + const curator1Tokens = beforeOtherNSignal + .mul(afterSubgraph.withdrawableGRT) + .div(afterSubgraph.nSignal) + const curator2Tokens = beforeAnotherNSignal + .mul(afterSubgraph.withdrawableGRT) + .div(afterSubgraph.nSignal) + + const expectedCallhookData1 = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), subgraph0.id, other.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE + ) + const expectedCallhookData2 = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), subgraph0.id, another.address], // code = 1 means RECEIVE_CURATOR_BALANCE_CODE + ) + const expectedL2Data1 = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + gns.address, + mockL2GNS.address, + curator1Tokens, + expectedCallhookData1, ) - const tx2 = legacyGNSMock - .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( - subgraphID, + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( + subgraph0.id, other.address, maxGas, gasPriceBid, @@ -1390,9 +1513,36 @@ describe('L1GNS', () => { ) // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 + await expect(tx) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(gns.address, mockL2Gateway.address, toBN('2'), expectedL2Data1) + + // Accept slight numerical errors given how we compute the amount of tokens to send + const curator2TokensUpdated = (await gns.subgraphs(subgraph0.id)).withdrawableGRT + expect(toRound(toFloat(curator2TokensUpdated))).to.equal(toRound(toFloat(curator2Tokens))) + const expectedL2Data2 = await l1GraphTokenGateway.getOutboundCalldata( + grt.address, + gns.address, + mockL2GNS.address, + curator2TokensUpdated, + expectedCallhookData2, + ) + const tx2 = gns + .connect(another.signer) + .sendCuratorBalanceToBeneficiaryOnL2( + subgraph0.id, + another.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + { + value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), + }, + ) + // seqNum (third argument in the event) is 3 now await expect(tx2) - .emit(legacyGNSMock, 'TxToL2') - .withArgs(me.address, mockL2GNS.address, toBN('2'), expectedCalldata) + .emit(l1GraphTokenGateway, 'TxToL2') + .withArgs(gns.address, mockL2Gateway.address, toBN('3'), expectedL2Data2) }) it('rejects calls for a subgraph that was not sent to L2', async function () { const subgraph0 = await publishAndCurateOnSubgraph() @@ -1403,7 +1553,7 @@ describe('L1GNS', () => { const tx = gns .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, other.address, maxGas, @@ -1429,7 +1579,7 @@ describe('L1GNS', () => { const tx = gns .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, other.address, maxGas, @@ -1451,7 +1601,7 @@ describe('L1GNS', () => { const tx = gns .connect(me.signer) - .claimCuratorBalanceToBeneficiaryOnL2( + .sendCuratorBalanceToBeneficiaryOnL2( subgraph0.id, other.address, maxGas, @@ -1464,6 +1614,32 @@ describe('L1GNS', () => { await expect(tx).revertedWith('NO_SUBMISSION_COST') }) + it('rejects calls if the curator has withdrawn the GRT', async function () { + const subgraph0 = await publishCurateAndSendSubgraph() + const afterSubgraph = await gns.subgraphs(subgraph0.id) + + await gns.connect(other.signer).withdraw(subgraph0.id) + + const maxSubmissionCost = toBN('100') + const maxGas = toBN('10') + const gasPriceBid = toBN('20') + + const tx = gns + .connect(other.signer) + .sendCuratorBalanceToBeneficiaryOnL2( + subgraph0.id, + another.address, + maxGas, + gasPriceBid, + maxSubmissionCost, + { + value: maxSubmissionCost.add(maxGas.mul(gasPriceBid)), + }, + ) + + // seqNum (third argument in the event) is 2, because number 1 was when the subgraph was sent to L2 + await expect(tx).revertedWith('NO_SIGNAL') + }) }) }) }) diff --git a/test/l2/l2Curation.test.ts b/test/l2/l2Curation.test.ts index f082efa97..b98afe462 100644 --- a/test/l2/l2Curation.test.ts +++ b/test/l2/l2Curation.test.ts @@ -256,9 +256,7 @@ describe('L2Curation', () => { const beforeTotalTokens = await grt.balanceOf(curation.address) // Curate - const tx = curation - .connect(gnsImpersonator) - .mintTaxFree(subgraphDeploymentID, tokensToDeposit, 0) + const tx = curation.connect(gnsImpersonator).mintTaxFree(subgraphDeploymentID, tokensToDeposit) await expect(tx) .emit(curation, 'Signalled') .withArgs(gns.address, subgraphDeploymentID, tokensToDeposit, expectedSignal, 0) @@ -472,9 +470,7 @@ describe('L2Curation', () => { describe('curate tax free (from GNS)', async function () { it('can not be called by anyone other than GNS', async function () { const tokensToDeposit = await curation.minimumCurationDeposit() - const tx = curation - .connect(curator.signer) - .mintTaxFree(subgraphDeploymentID, tokensToDeposit, 0) + const tx = curation.connect(curator.signer).mintTaxFree(subgraphDeploymentID, tokensToDeposit) await expect(tx).revertedWith('Only the GNS can call this') }) @@ -482,7 +478,7 @@ describe('L2Curation', () => { const tokensToDeposit = (await curation.minimumCurationDeposit()).sub(toBN(1)) const tx = curation .connect(gnsImpersonator) - .mintTaxFree(subgraphDeploymentID, tokensToDeposit, 0) + .mintTaxFree(subgraphDeploymentID, tokensToDeposit) await expect(tx).revertedWith('Curation deposit is below minimum required') }) @@ -510,15 +506,6 @@ describe('L2Curation', () => { ) await shouldMintTaxFree(tokensToDeposit, expectedSignal) }) - - it('should revert curate if over slippage', async function () { - const tokensToDeposit = toGRT('1000') - const expectedSignal = signalAmountFor1000Tokens - const tx = curation - .connect(gnsImpersonator) - .mintTaxFree(subgraphDeploymentID, tokensToDeposit, expectedSignal.add(1)) - await expect(tx).revertedWith('Slippage protection') - }) }) describe('collect', async function () { diff --git a/test/l2/l2GNS.test.ts b/test/l2/l2GNS.test.ts index a0928ceaf..f90b96f1c 100644 --- a/test/l2/l2GNS.test.ts +++ b/test/l2/l2GNS.test.ts @@ -10,7 +10,6 @@ import { getL2SignerFromL1, setAccountBalance, latestBlock, - advanceBlocks, } from '../lib/testHelpers' import { L2FixtureContracts, NetworkFixture } from '../lib/fixtures' import { toBN } from '../lib/testHelpers' @@ -23,7 +22,6 @@ import { burnSignal, DEFAULT_RESERVE_RATIO, deprecateSubgraph, - getTokensAndVSignal, mintSignal, publishNewSubgraph, publishNewVersion, @@ -40,7 +38,6 @@ interface L1SubgraphParams { curatedTokens: BigNumber subgraphMetadata: string versionMetadata: string - nSignal: BigNumber } describe('L2GNS', () => { @@ -89,7 +86,6 @@ describe('L2GNS', () => { curatedTokens: toGRT('1337'), subgraphMetadata: randomHexBytes(), versionMetadata: randomHexBytes(), - nSignal: toGRT('45670'), } } const migrateMockSubgraphFromL1 = async function ( @@ -97,11 +93,10 @@ describe('L2GNS', () => { curatedTokens: BigNumber, subgraphMetadata: string, versionMetadata: string, - nSignal: BigNumber, ) { const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) @@ -243,10 +238,10 @@ describe('L2GNS', () => { describe('receiving a subgraph from L1 (onTokenTransfer)', function () { it('cannot be called by someone other than the L2GraphTokenGateway', async function () { - const { l1SubgraphId, curatedTokens, nSignal } = await defaultL1SubgraphParams() + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) const tx = gns .connect(me.signer) @@ -254,22 +249,20 @@ describe('L2GNS', () => { await expect(tx).revertedWith('ONLY_GATEWAY') }) it('rejects calls if the L1 sender is not the L1GNS', async function () { - const { l1SubgraphId, curatedTokens, nSignal } = await defaultL1SubgraphParams() + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) const tx = gatewayFinalizeTransfer(me.address, gns.address, curatedTokens, callhookData) await expect(tx).revertedWith('ONLY_L1_GNS_THROUGH_BRIDGE') }) it('creates a subgraph in a disabled state', async function () { - const l1SubgraphId = await buildSubgraphID(me.address, toBN('1'), 1) - const curatedTokens = toGRT('1337') - const nSignal = toBN('4567') + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) const tx = gatewayFinalizeTransfer( mockL1GNS.address, @@ -281,7 +274,9 @@ describe('L2GNS', () => { await expect(tx) .emit(l2GraphTokenGateway, 'DepositFinalized') .withArgs(mockL1GRT.address, mockL1GNS.address, gns.address, curatedTokens) - await expect(tx).emit(gns, 'SubgraphReceivedFromL1').withArgs(l1SubgraphId) + await expect(tx) + .emit(gns, 'SubgraphReceivedFromL1') + .withArgs(l1SubgraphId, me.address, curatedTokens) const migrationData = await gns.subgraphL2MigrationData(l1SubgraphId) const subgraphData = await gns.subgraphs(l1SubgraphId) @@ -291,7 +286,7 @@ describe('L2GNS', () => { expect(migrationData.subgraphReceivedOnL2BlockNumber).eq(await latestBlock()) expect(subgraphData.vSignal).eq(0) - expect(subgraphData.nSignal).eq(nSignal) + expect(subgraphData.nSignal).eq(0) expect(subgraphData.subgraphDeploymentID).eq(HashZero) expect(subgraphData.reserveRatioDeprecated).eq(DEFAULT_RESERVE_RATIO) expect(subgraphData.disabled).eq(true) @@ -302,12 +297,10 @@ describe('L2GNS', () => { it('does not conflict with a locally created subgraph', async function () { const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns) - const l1SubgraphId = await buildSubgraphID(me.address, toBN('0'), 1) - const curatedTokens = toGRT('1337') - const nSignal = toBN('4567') + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) const tx = gatewayFinalizeTransfer( mockL1GNS.address, @@ -319,7 +312,9 @@ describe('L2GNS', () => { await expect(tx) .emit(l2GraphTokenGateway, 'DepositFinalized') .withArgs(mockL1GRT.address, mockL1GNS.address, gns.address, curatedTokens) - await expect(tx).emit(gns, 'SubgraphReceivedFromL1').withArgs(l1SubgraphId) + await expect(tx) + .emit(gns, 'SubgraphReceivedFromL1') + .withArgs(l1SubgraphId, me.address, curatedTokens) const migrationData = await gns.subgraphL2MigrationData(l1SubgraphId) const subgraphData = await gns.subgraphs(l1SubgraphId) @@ -329,7 +324,7 @@ describe('L2GNS', () => { expect(migrationData.subgraphReceivedOnL2BlockNumber).eq(await latestBlock()) expect(subgraphData.vSignal).eq(0) - expect(subgraphData.nSignal).eq(nSignal) + expect(subgraphData.nSignal).eq(0) expect(subgraphData.subgraphDeploymentID).eq(HashZero) expect(subgraphData.reserveRatioDeprecated).eq(DEFAULT_RESERVE_RATIO) expect(subgraphData.disabled).eq(true) @@ -350,11 +345,11 @@ describe('L2GNS', () => { describe('finishing a subgraph migration from L1', function () { it('publishes the migrated subgraph and mints signal with no tax', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) // Calculate expected signal before minting @@ -389,13 +384,15 @@ describe('L2GNS', () => { expect(migrationDataAfter.l2Done).eq(true) expect(subgraphAfter.disabled).eq(false) expect(subgraphAfter.subgraphDeploymentID).eq(newSubgraph0.subgraphDeploymentID) + const expectedNSignal = await gns.vSignalToNSignal(l1SubgraphId, expectedSignal) + expect(await gns.getCuratorSignal(l1SubgraphId, me.address)).eq(expectedNSignal) }) it('cannot be called by someone other than the subgraph owner', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) @@ -438,11 +435,11 @@ describe('L2GNS', () => { await expect(tx).revertedWith('INVALID_SUBGRAPH') }) it('accepts calls to a pre-curated subgraph deployment', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) @@ -490,13 +487,11 @@ describe('L2GNS', () => { ) }) it('rejects calls if the subgraph deployment ID is zero', async function () { - const l1SubgraphId = await buildSubgraphID(me.address, toBN('1'), 1) - const curatedTokens = toGRT('1337') const metadata = randomHexBytes() - const nSignal = toBN('4567') + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) @@ -505,140 +500,89 @@ describe('L2GNS', () => { .finishSubgraphMigrationFromL1(l1SubgraphId, HashZero, metadata, metadata) await expect(tx).revertedWith('GNS: deploymentID != 0') }) - }) - describe('deprecating a subgraph with an unfinished migration from L1', function () { - it('deprecates the subgraph and sets the withdrawableGRT', async function () { - const { l1SubgraphId, curatedTokens, nSignal } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], - ) - await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) - - await advanceBlocks(50400) - - const tx = gns - .connect(other.signer) // Can be called by anyone - .deprecateSubgraphMigratedFromL1(l1SubgraphId) - await expect(tx).emit(gns, 'SubgraphDeprecated').withArgs(l1SubgraphId, curatedTokens) - - const subgraphAfter = await gns.subgraphs(l1SubgraphId) - const migrationDataAfter = await gns.subgraphL2MigrationData(l1SubgraphId) - expect(subgraphAfter.vSignal).eq(0) - expect(migrationDataAfter.l2Done).eq(true) - expect(subgraphAfter.disabled).eq(true) - expect(subgraphAfter.subgraphDeploymentID).eq(HashZero) - expect(subgraphAfter.withdrawableGRT).eq(curatedTokens) - - // Check that the curator can withdraw the GRT - const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) - await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) - // Note the signal is assigned to other.address as beneficiary - await gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - const curatorBalanceBefore = await grt.balanceOf(other.address) - const expectedTokensOut = curatedTokens.mul(toGRT('10')).div(nSignal) - const withdrawTx = await gns.connect(other.signer).withdraw(l1SubgraphId) - await expect(withdrawTx) - .emit(gns, 'GRTWithdrawn') - .withArgs(l1SubgraphId, other.address, toGRT('10'), expectedTokensOut) - const curatorBalanceAfter = await grt.balanceOf(other.address) - expect(curatorBalanceAfter.sub(curatorBalanceBefore)).eq(expectedTokensOut) - }) - it('rejects calls if not enough time has passed', async function () { - const { l1SubgraphId, curatedTokens, nSignal } = await defaultL1SubgraphParams() - const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], - ) - await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) - - await advanceBlocks(50399) - - const tx = gns - .connect(other.signer) // Can be called by anyone - .deprecateSubgraphMigratedFromL1(l1SubgraphId) - await expect(tx).revertedWith('TOO_EARLY') - }) - it('rejects calls if the subgraph migration was finished', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = - await defaultL1SubgraphParams() + it('rejects calls if the subgraph migration was already finished', async function () { + const metadata = randomHexBytes() + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() const callhookData = defaultAbiCoder.encode( - ['uint256', 'address', 'uint256'], - [l1SubgraphId, me.address, nSignal], + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], ) await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookData) - await advanceBlocks(50400) - await gns .connect(me.signer) .finishSubgraphMigrationFromL1( l1SubgraphId, newSubgraph0.subgraphDeploymentID, - subgraphMetadata, - versionMetadata, + metadata, + metadata, ) const tx = gns - .connect(other.signer) // Can be called by anyone - .deprecateSubgraphMigratedFromL1(l1SubgraphId) - await expect(tx).revertedWith('ALREADY_FINISHED') - }) - it('rejects calls for a subgraph that does not exist', async function () { - const l1SubgraphId = await buildSubgraphID(me.address, toBN('1'), 1) - - const tx = gns.connect(me.signer).deprecateSubgraphMigratedFromL1(l1SubgraphId) - await expect(tx).revertedWith('INVALID_SUBGRAPH') - }) - it('rejects calls for a subgraph that was not migrated', async function () { - const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns) - - const tx = gns.connect(me.signer).deprecateSubgraphMigratedFromL1(l2Subgraph.id) - await expect(tx).revertedWith('INVALID_SUBGRAPH') + .connect(me.signer) + .finishSubgraphMigrationFromL1( + l1SubgraphId, + newSubgraph0.subgraphDeploymentID, + metadata, + metadata, + ) + await expect(tx).revertedWith('ALREADY_DONE') }) }) - describe('claiming a curator balance with a message from L1', function () { + describe('claiming a curator balance with a message from L1 (onTokenTransfer)', function () { it('assigns a curator balance to a beneficiary', async function () { const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) // Eth for gas: await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() await migrateMockSubgraphFromL1( l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, - nSignal, ) - const tx = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) + const l2OwnerSignalBefore = await gns.getCuratorSignal(l1SubgraphId, me.address) + + const newCuratorTokens = toGRT('10') + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, other.address], + ) + const tx = await gatewayFinalizeTransfer( + mockL1GNS.address, + gns.address, + newCuratorTokens, + callhookData, + ) + await expect(tx) - .emit(gns, 'CuratorBalanceClaimed') - .withArgs(l1SubgraphId, me.address, other.address, toGRT('10')) - const l1CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, me.address) - const l2CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, other.address) - expect(l1CuratorBalance).eq(0) - expect(l2CuratorBalance).eq(toGRT('10')) + .emit(gns, 'CuratorBalanceReceived') + .withArgs(l1SubgraphId, other.address, newCuratorTokens) + + const l2NewCuratorSignal = await gns.getCuratorSignal(l1SubgraphId, other.address) + const expectedNewCuratorSignal = await gns.vSignalToNSignal( + l1SubgraphId, + await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), + ) + const l2OwnerSignalAfter = await gns.getCuratorSignal(l1SubgraphId, me.address) + expect(l2OwnerSignalAfter).eq(l2OwnerSignalBefore) + expect(l2NewCuratorSignal).eq(expectedNewCuratorSignal) }) - it('adds the balance to any existing balance for the beneficiary', async function () { + it('adds the signal to any existing signal for the beneficiary', async function () { const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) // Eth for gas: await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() await migrateMockSubgraphFromL1( l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, - nSignal, ) await grt.connect(governor.signer).mint(other.address, toGRT('10')) @@ -646,98 +590,191 @@ describe('L2GNS', () => { await gns.connect(other.signer).mintSignal(l1SubgraphId, toGRT('10'), toBN(0)) const prevSignal = await gns.getCuratorSignal(l1SubgraphId, other.address) - const tx = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) + const newCuratorTokens = toGRT('10') + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, other.address], + ) + const tx = await gatewayFinalizeTransfer( + mockL1GNS.address, + gns.address, + newCuratorTokens, + callhookData, + ) + await expect(tx) - .emit(gns, 'CuratorBalanceClaimed') - .withArgs(l1SubgraphId, me.address, other.address, toGRT('10')) - const l1CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, me.address) + .emit(gns, 'CuratorBalanceReceived') + .withArgs(l1SubgraphId, other.address, newCuratorTokens) + + const expectedNewCuratorSignal = await gns.vSignalToNSignal( + l1SubgraphId, + await curation.tokensToSignalNoTax(newSubgraph0.subgraphDeploymentID, newCuratorTokens), + ) const l2CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, other.address) - expect(l1CuratorBalance).eq(0) - expect(l2CuratorBalance).eq(prevSignal.add(toGRT('10'))) + expect(l2CuratorBalance).eq(prevSignal.add(expectedNewCuratorSignal)) }) - it('can only be called from the counterpart GNS L2 alias', async function () { - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + it('cannot be called by someone other than the L2GraphTokenGateway', async function () { + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() await migrateMockSubgraphFromL1( l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, - nSignal, ) + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, me.address], + ) + const tx = gns.connect(me.signer).onTokenTransfer(mockL1GNS.address, toGRT('1'), callhookData) + await expect(tx).revertedWith('ONLY_GATEWAY') + }) + it('rejects calls if the L1 sender is not the L1GNS', async function () { + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = + await defaultL1SubgraphParams() + await migrateMockSubgraphFromL1( + l1SubgraphId, + curatedTokens, + subgraphMetadata, + versionMetadata, + ) + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, me.address], + ) + const tx = gatewayFinalizeTransfer(me.address, gns.address, toGRT('1'), callhookData) - const tx = gns - .connect(governor.signer) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - await expect(tx).revertedWith('ONLY_COUNTERPART_GNS') - - const tx2 = gns - .connect(me.signer) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - await expect(tx2).revertedWith('ONLY_COUNTERPART_GNS') - - const tx3 = gns - .connect(mockL1GNS.signer) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - await expect(tx3).revertedWith('ONLY_COUNTERPART_GNS') + await expect(tx).revertedWith('ONLY_L1_GNS_THROUGH_BRIDGE') }) - it('rejects calls for a subgraph that does not exist', async function () { + it('if a subgraph does not exist, it returns the tokens to the beneficiary', async function () { const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) // Eth for gas: await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) const { l1SubgraphId } = await defaultL1SubgraphParams() - const tx = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - await expect(tx).revertedWith('!MIGRATED') + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, me.address], + ) + const curatorTokensBefore = await grt.balanceOf(me.address) + const gnsBalanceBefore = await grt.balanceOf(gns.address) + const tx = gatewayFinalizeTransfer(mockL1GNS.address, gns.address, toGRT('1'), callhookData) + await expect(tx) + .emit(gns, 'CuratorBalanceReturnedToBeneficiary') + .withArgs(l1SubgraphId, me.address, toGRT('1')) + const curatorTokensAfter = await grt.balanceOf(me.address) + expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) + const gnsBalanceAfter = await grt.balanceOf(gns.address) + // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, + // so the GNS balance should be the same + expect(gnsBalanceAfter).eq(gnsBalanceBefore) }) - it('rejects calls for an L2-native subgraph', async function () { + it('for an L2-native subgraph, it sends the tokens to the beneficiary', async function () { + // This should never really happen unless there's a clash in subgraph IDs (which should + // also never happen), but we test it anyway to ensure it's a well-defined behavior const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) // Eth for gas: await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) const l2Subgraph = await publishNewSubgraph(me, newSubgraph0, gns) - const tx = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l2Subgraph.id!, me.address, toGRT('10'), other.address) - await expect(tx).revertedWith('!MIGRATED') + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l2Subgraph.id!, me.address], + ) + const curatorTokensBefore = await grt.balanceOf(me.address) + const gnsBalanceBefore = await grt.balanceOf(gns.address) + const tx = gatewayFinalizeTransfer(mockL1GNS.address, gns.address, toGRT('1'), callhookData) + await expect(tx) + .emit(gns, 'CuratorBalanceReturnedToBeneficiary') + .withArgs(l2Subgraph.id!, me.address, toGRT('1')) + const curatorTokensAfter = await grt.balanceOf(me.address) + expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) + const gnsBalanceAfter = await grt.balanceOf(gns.address) + // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, + // so the GNS balance should be the same + expect(gnsBalanceAfter).eq(gnsBalanceBefore) }) - it('rejects calls if the balance was already claimed', async function () { + it('if a subgraph migration was not finished, it returns the tokens to the beneficiary', async function () { const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) // Eth for gas: await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) - const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, nSignal } = + const { l1SubgraphId, curatedTokens } = await defaultL1SubgraphParams() + const callhookDataSG = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(0), l1SubgraphId, me.address], + ) + await gatewayFinalizeTransfer(mockL1GNS.address, gns.address, curatedTokens, callhookDataSG) + + // At this point the SG exists, but migration is not finished + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, me.address], + ) + const curatorTokensBefore = await grt.balanceOf(me.address) + const gnsBalanceBefore = await grt.balanceOf(gns.address) + const tx = gatewayFinalizeTransfer(mockL1GNS.address, gns.address, toGRT('1'), callhookData) + await expect(tx) + .emit(gns, 'CuratorBalanceReturnedToBeneficiary') + .withArgs(l1SubgraphId, me.address, toGRT('1')) + const curatorTokensAfter = await grt.balanceOf(me.address) + expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) + const gnsBalanceAfter = await grt.balanceOf(gns.address) + // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, + // so the GNS balance should be the same + expect(gnsBalanceAfter).eq(gnsBalanceBefore) + }) + + it('if a subgraph was deprecated after migration, it returns the tokens to the beneficiary', async function () { + const mockL1GNSL2Alias = await getL2SignerFromL1(mockL1GNS.address) + // Eth for gas: + await setAccountBalance(await mockL1GNSL2Alias.getAddress(), parseEther('1')) + + const { l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata } = await defaultL1SubgraphParams() await migrateMockSubgraphFromL1( l1SubgraphId, curatedTokens, subgraphMetadata, versionMetadata, - nSignal, ) - const tx = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) + await gns.connect(me.signer).deprecateSubgraph(l1SubgraphId) + + // SG was migrated, but is deprecated now! + + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(1), l1SubgraphId, me.address], + ) + const curatorTokensBefore = await grt.balanceOf(me.address) + const gnsBalanceBefore = await grt.balanceOf(gns.address) + const tx = gatewayFinalizeTransfer(mockL1GNS.address, gns.address, toGRT('1'), callhookData) await expect(tx) - .emit(gns, 'CuratorBalanceClaimed') - .withArgs(l1SubgraphId, me.address, other.address, toGRT('10')) - const l1CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, me.address) - const l2CuratorBalance = await gns.getCuratorSignal(l1SubgraphId, other.address) - expect(l1CuratorBalance).eq(0) - expect(l2CuratorBalance).eq(toGRT('10')) - - // Now trying again should revert - const tx2 = gns - .connect(mockL1GNSL2Alias) - .claimL1CuratorBalanceToBeneficiary(l1SubgraphId, me.address, toGRT('10'), other.address) - await expect(tx2).revertedWith('ALREADY_CLAIMED') + .emit(gns, 'CuratorBalanceReturnedToBeneficiary') + .withArgs(l1SubgraphId, me.address, toGRT('1')) + const curatorTokensAfter = await grt.balanceOf(me.address) + expect(curatorTokensAfter).eq(curatorTokensBefore.add(toGRT('1'))) + const gnsBalanceAfter = await grt.balanceOf(gns.address) + // gatewayFinalizeTransfer will mint the tokens that are sent to the curator, + // so the GNS balance should be the same + expect(gnsBalanceAfter).eq(gnsBalanceBefore) + }) + }) + describe('onTokenTransfer with invalid codes', function () { + it('reverts', async function () { + // This should never really happen unless the Arbitrum bridge is compromised, + // so we test it anyway to ensure it's a well-defined behavior. + // code 2 does not exist: + const callhookData = defaultAbiCoder.encode( + ['uint8', 'uint256', 'address'], + [toBN(2), toBN(1337), me.address], + ) + const tx = gatewayFinalizeTransfer(mockL1GNS.address, gns.address, toGRT('1'), callhookData) + await expect(tx).revertedWith('INVALID_CODE') }) }) })