From c7bb5d330d025c1bc67c5374a0279743ab406a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Tue, 24 Jan 2023 12:28:02 -0300 Subject: [PATCH] feat: implement BridgeDeposits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .github/workflows/template-deploy.yaml | 2 +- .gitignore | 2 +- package.json | 4 +- schema.graphql | 21 +-- src/mappings/bridgeHelpers.ts | 105 ------------ src/mappings/controller.ts | 2 +- src/mappings/curation.ts | 2 +- src/mappings/disputeManager.ts | 2 +- src/mappings/epochManager.ts | 2 +- src/mappings/ethereumDIDRegistry.ts | 4 +- src/mappings/gns.ts | 4 +- src/mappings/graphToken.ts | 2 +- src/mappings/helpers/bridge.ts | 159 ++++++++++++++++++ src/mappings/helpers/byte.ts | 32 ++++ src/mappings/helpers/event-log.ts | 43 +++++ .../helpers/events/InboxMessageDelivered.ts | 54 ++++++ .../helpers/events/MessageDelivered.ts | 39 +++++ .../events/OutBoxTransactionExecuted.ts | 38 +++++ src/mappings/helpers/events/TxToL2.ts | 32 ++++ src/mappings/{ => helpers}/helpers.ts | 12 +- .../metadata.template.ts} | 2 +- src/mappings/helpers/rlp.ts | 38 +++++ src/mappings/l1Gateway.ts | 47 ++++-- src/mappings/l2Gateway.ts | 16 +- src/mappings/rewardsManager.ts | 2 +- src/mappings/serviceRegistry.ts | 2 +- src/mappings/staking.ts | 2 +- src/mappings/tokenLockWallets/manager.ts | 2 +- 28 files changed, 512 insertions(+), 160 deletions(-) delete mode 100644 src/mappings/bridgeHelpers.ts create mode 100644 src/mappings/helpers/bridge.ts create mode 100644 src/mappings/helpers/byte.ts create mode 100644 src/mappings/helpers/event-log.ts create mode 100644 src/mappings/helpers/events/InboxMessageDelivered.ts create mode 100644 src/mappings/helpers/events/MessageDelivered.ts create mode 100644 src/mappings/helpers/events/OutBoxTransactionExecuted.ts create mode 100644 src/mappings/helpers/events/TxToL2.ts rename src/mappings/{ => helpers}/helpers.ts (99%) rename src/mappings/{metadataHelpers.template.ts => helpers/metadata.template.ts} (99%) create mode 100644 src/mappings/helpers/rlp.ts diff --git a/.github/workflows/template-deploy.yaml b/.github/workflows/template-deploy.yaml index 68d0b5cd..ba7e7d7d 100644 --- a/.github/workflows/template-deploy.yaml +++ b/.github/workflows/template-deploy.yaml @@ -70,7 +70,7 @@ jobs: # Run scripts - name: Prepare IPFS enabled files - run: ./node_modules/.bin/mustache ./config/ipfs.json ./src/mappings/metadataHelpers.template.ts > ./src/mappings/metadataHelpers.ts + run: ./node_modules/.bin/mustache ./config/ipfs.json ./src/mappings/helpers/metadata.template.ts > ./src/mappings/helpers/metadata.ts - name: Prepare addresses ${{ inputs.ENVIRONMENT }} run: ./node_modules/.bin/ts-node config/${{ inputs.CONFIG }} && ./node_modules/.bin/mustache ./config/generatedAddresses.json ./config/addresses.template.ts > ./config/addresses.ts diff --git a/.gitignore b/.gitignore index f584ad27..9f1ae223 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ build/ generated/ node_modules/ src/types/ -src/mappings/metadataHelpers.ts +src/mappings/helpers/metadata.ts .DS_STORE yarn-error.log subgraph.yaml diff --git a/package.json b/package.json index ae9b762d..dbd770d9 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "prep:addresses:arbitrum": "ts-node config/mainnetArbitrumAddressScript.ts && mustache ./config/generatedAddresses.json ./config/addresses.template.ts > ./config/addresses.ts", "prep:addresses:arbitrum-goerli": "ts-node config/arbitrumGoerliAddressScript.ts && mustache ./config/generatedAddresses.json ./config/addresses.template.ts > ./config/addresses.ts", "prepare:arbitrum-goerli": "yarn prep:addresses:arbitrum-goerli && mustache ./config/generatedAddresses.json subgraph.template.yaml > subgraph.yaml && graph codegen --output-dir src/types/", - "prep:ipfs": "mustache ./config/ipfs.json ./src/mappings/metadataHelpers.template.ts > ./src/mappings/metadataHelpers.ts", - "prep:no-ipfs": "mustache ./config/no-ipfs.json ./src/mappings/metadataHelpers.template.ts > ./src/mappings/metadataHelpers.ts", + "prep:ipfs": "mustache ./config/ipfs.json ./src/mappings/helpers/metadata.template.ts > ./src/mappings/helpers/metadata.ts", + "prep:no-ipfs": "mustache ./config/no-ipfs.json ./src/mappings/helpers/metadata.template.ts > ./src/mappings/helpers/metadata.ts", "lint": "yarn eslint .", "lint-fix": "eslint . --fix", "prettier": "prettier '**/*.ts'", diff --git a/schema.graphql b/schema.graphql index 5ca13c3d..43d984f7 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1157,17 +1157,13 @@ type BridgeWithdrawalTransaction implements Transaction @entity { signer: GraphAccount! type: TransactionType! "txHash refers to the tx on the chain corresponding to this subgraph deployment" - txHash: Bytes! + txHash: Bytes from: Bytes to: Bytes amount: BigInt l1Token: Bytes - """ - transactionIndex is the unique value that allows matching an L2 transaction with its L1 counterpart - - On L1, Arbitrum's Outbox contract emits OutBoxTransactionExecuted event with a transactionIndex property - - On L2, the Graph gateway emits WithdrawalInitiated with the transaction index as l2ToL1Id - """ - transactionIndex: Int + "transactionIndex is the unique value that allows matching an L2 transaction with its L1 counterpart" + transactionIndex: BigInt } """ @@ -1185,12 +1181,10 @@ type BridgeDepositTransaction implements Transaction @entity { to: Bytes amount: BigInt l1Token: Bytes - """ - retryableTicketId is the unique value that allows matching an L2 transaction with its L1 counterpart - - On L1, Arbitrum's Outbox contract emits OutBoxTransactionExecuted event with a transactionIndex property - - On L2, the Graph gateway emits WithdrawalInitiated with the transaction index as l2ToL1Id - """ - retryableTicketId: Bytes! + "retryableTicketId is the unique value that allows matching an L2 transaction with its L1 counterpart" + retryableTicketId: String + "Wether the deposit was initiated through Arbitrum's gateway router (Only available on L1 networks)" + routed: Boolean } enum TransactionType { @@ -1201,6 +1195,7 @@ enum TransactionType { MintNSignal BurnNSignal BridgeWithdrawal + BridgeDeposit } """ diff --git a/src/mappings/bridgeHelpers.ts b/src/mappings/bridgeHelpers.ts deleted file mode 100644 index f337a45a..00000000 --- a/src/mappings/bridgeHelpers.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Bytes, BigInt, ethereum, log, crypto } from "@graphprotocol/graph-ts"; - -function strip0xPrefix(input: string): string { - return input.startsWith("0x") ? input.slice(2) : input; -} - -// Gets transactionIndex -// Returns null if the withdrawal call is made under the following conditions: -// - Call is made from another contract (i.e: a multicall call) and -// - Call contains multiple 'OutBoxTransactionExecuted' events (i.e: multiple withdrawals on same multicall call) -export function getTransactionIndex(event: ethereum.Event): BigInt | null { - return getTransactionIndexFromCalldata(event) || getTransactionIndexFromLogs(event); -} - -// Gets transactionIndex from the calldata -// Returns null if the call is made from another contract (i.e: a multicall call) -export function getTransactionIndexFromCalldata(event: ethereum.Event): BigInt | null { - let stringCallData = event.transaction.input.toHexString(); - let strippedCallData = strip0xPrefix(stringCallData); - - // Validate selector - // Method signature: executeTransaction(bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes) - // MethodID: 0x08635a95 - let selector = strippedCallData.slice(0, 8); - if (selector != "08635a95") { - log.error("Invalid function selector", [selector]); - return null; - } - - // Decode ABI using a modified types mapping since the decode method fails with dynamic types - // Original types mapping: (bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes) - // Modified types mapping: (uint256,uint256,address,address,uint256,uint256,uint256,uint256) - let types = - "(uint256,uint256,address,address,uint256,uint256,uint256,uint256)"; - - let decoded = ethereum.decode( - types, - Bytes.fromHexString(strippedCallData.substring(8)) - ); - if (decoded !== null) { - return decoded.toTuple()[1].toBigInt(); // transactionIndex - } else { - log.error("Could not decode call data!", []); - return null; - } -} - -// Get transactionIndex from the 'OutBoxTransactionExecuted' event, emitted by Arbitrum's Outbox contract on the same tx -// Returns null if the event is not found or if there are multiple events (i.e: a multicall call) -export function getTransactionIndexFromLogs(event: ethereum.Event): BigInt | null { - let receipt = event.receipt; - if (receipt === null) { - log.error("Could not get tx receipt!", []); - return null; - } - - let eventCount = 0; - let transactionIndex: BigInt | null = null; - - let logs = receipt.logs; - for (let i = 0; i < logs.length; i++) { - let topics = logs[i].topics; - - // OutBoxTransactionExecuted event - if (isOutBoxTransactionExecutedEvent(topics[0])) { - eventCount = eventCount + 1; - - // Parse event data to get the transactionIndex - let data = logs[i].data; - if (data.length === 32) { - let amountBytes = Bytes.fromHexString( - strip0xPrefix(data.toHexString()) - ); - transactionIndex = BigInt.fromUnsignedBytes( - amountBytes.reverse() as Bytes - ); - } else { - log.error("Invalid data length", [ - data.length.toString(), - data.toHexString(), - ]); - } - } - } - - if (eventCount != 1) { - log.error("Event count for 'OutBoxTransactionExecuted' is not 1!", [ - eventCount.toString(), - ]); - transactionIndex = null; - } - - return transactionIndex; -} - -function isOutBoxTransactionExecutedEvent(topic: Bytes): boolean { - return ( - topic == - crypto.keccak256( - Bytes.fromUTF8( - "OutBoxTransactionExecuted(address,address,uint256,uint256)" - ) - ) - ); -} \ No newline at end of file diff --git a/src/mappings/controller.ts b/src/mappings/controller.ts index be2b7e6f..a01c1bd8 100644 --- a/src/mappings/controller.ts +++ b/src/mappings/controller.ts @@ -6,7 +6,7 @@ import { NewPauseGuardian, } from '../types/Controller/Controller' -import { createOrLoadGraphNetwork } from './helpers' +import { createOrLoadGraphNetwork } from './helpers/helpers' /** * @dev handleSetContractProxy diff --git a/src/mappings/curation.ts b/src/mappings/curation.ts index 476109be..466e6670 100644 --- a/src/mappings/curation.ts +++ b/src/mappings/curation.ts @@ -16,7 +16,7 @@ import { joinID, calculatePricePerShare, batchUpdateSubgraphSignalledTokens, -} from './helpers' +} from './helpers/helpers' import { zeroBD } from './utils' import { addresses } from '../../config/addresses' diff --git a/src/mappings/disputeManager.ts b/src/mappings/disputeManager.ts index 05864ee4..6b0dc118 100644 --- a/src/mappings/disputeManager.ts +++ b/src/mappings/disputeManager.ts @@ -11,7 +11,7 @@ import { DisputeManager, } from '../types/DisputeManager/DisputeManager' import { DisputeManagerStitched } from '../types/DisputeManager/DisputeManagerStitched' -import { createOrLoadGraphNetwork } from './helpers' +import { createOrLoadGraphNetwork } from './helpers/helpers' // This handles Single query and Conflicting disputes export function handleQueryDisputeCreated(event: QueryDisputeCreated): void { diff --git a/src/mappings/epochManager.ts b/src/mappings/epochManager.ts index 958fc158..f6490005 100644 --- a/src/mappings/epochManager.ts +++ b/src/mappings/epochManager.ts @@ -1,6 +1,6 @@ import { GraphNetwork } from '../types/schema' import { EpochRun, EpochLengthUpdate } from '../types/EpochManager/EpochManager' -import { createOrLoadEpoch, createEpoch, createOrLoadGraphNetwork } from './helpers' +import { createOrLoadEpoch, createEpoch, createOrLoadGraphNetwork } from './helpers/helpers' import { addresses } from '../../config/addresses' /** diff --git a/src/mappings/ethereumDIDRegistry.ts b/src/mappings/ethereumDIDRegistry.ts index 5a969815..0d015017 100644 --- a/src/mappings/ethereumDIDRegistry.ts +++ b/src/mappings/ethereumDIDRegistry.ts @@ -1,8 +1,8 @@ import { Bytes } from '@graphprotocol/graph-ts' import { DIDAttributeChanged } from '../types/EthereumDIDRegistry/EthereumDIDRegistry' -import { addQm, createOrLoadGraphAccount } from './helpers' -import { fetchGraphAccountMetadata } from './metadataHelpers' +import { addQm, createOrLoadGraphAccount } from './helpers/helpers' +import { fetchGraphAccountMetadata } from './helpers/metadata' export function handleDIDAttributeChanged(event: DIDAttributeChanged): void { let graphAccount = createOrLoadGraphAccount(event.params.identity, event.block.timestamp) diff --git a/src/mappings/gns.ts b/src/mappings/gns.ts index 47edd667..ce0b66a2 100644 --- a/src/mappings/gns.ts +++ b/src/mappings/gns.ts @@ -56,8 +56,8 @@ import { duplicateOrUpdateSubgraphVersionWithNewID, duplicateOrUpdateNameSignalWithNewID, createOrLoadGraphNetwork -} from './helpers' -import { fetchSubgraphMetadata, fetchSubgraphVersionMetadata } from './metadataHelpers' +} from './helpers/helpers' +import { fetchSubgraphMetadata, fetchSubgraphVersionMetadata } from './helpers/metadata' import { addresses } from '../../config/addresses' export function handleSetDefaultName(event: SetDefaultName): void { diff --git a/src/mappings/graphToken.ts b/src/mappings/graphToken.ts index bd0a6a70..7412b082 100644 --- a/src/mappings/graphToken.ts +++ b/src/mappings/graphToken.ts @@ -1,5 +1,5 @@ import { Approval, Transfer, GraphToken } from '../types/GraphToken/GraphToken' -import { createOrLoadGraphAccount, createOrLoadGraphNetwork } from './helpers' +import { createOrLoadGraphAccount, createOrLoadGraphNetwork } from './helpers/helpers' import { GraphNetwork } from '../types/schema' /** diff --git a/src/mappings/helpers/bridge.ts b/src/mappings/helpers/bridge.ts new file mode 100644 index 00000000..24a26469 --- /dev/null +++ b/src/mappings/helpers/bridge.ts @@ -0,0 +1,159 @@ +import { + Bytes, + BigInt, + ethereum, + log, + crypto, + ByteArray, +} from "@graphprotocol/graph-ts"; +import { DepositInitiated } from "../../generated/L1GraphTokenGateway/L1GraphTokenGateway"; +import { bigIntToBytes, ensureEvenLength, padZeros, strip0xPrefix } from "./byte"; +import { RLPEncodeArray } from "./rlp"; +import { getOutBoxTransactionExecutedData } from "./events/OutBoxTransactionExecuted"; +import { getMessageDeliveredData } from "./events/MessageDelivered"; +import { getInboxMessageDeliveredData } from "./events/InboxMessageDelivered"; +import { getTxToL2Data } from "./events/TxToL2"; +import { BridgeDepositTransaction } from "../../generated/schema"; + +// Gets transactionIndex +// Returns null if the withdrawal call is made under the following conditions: +// - Call is made from another contract (i.e: a multicall call) and +// - Call contains multiple 'OutBoxTransactionExecuted' events (i.e: multiple withdrawals on same multicall call) +export function getTransactionIndex(event: ethereum.Event): BigInt | null { + return ( + getTransactionIndexFromCalldata(event) || getTransactionIndexFromLogs(event) + ); +} + +// Gets transactionIndex from the calldata +// Returns null if the call is made from another contract (i.e: a multicall call) +export function getTransactionIndexFromCalldata( + event: ethereum.Event +): BigInt | null { + let stringCallData = event.transaction.input.toHexString(); + let strippedCallData = strip0xPrefix(stringCallData); + + // Validate selector + // Method signature: executeTransaction(bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes) + // MethodID: 0x08635a95 + let selector = strippedCallData.slice(0, 8); + if (selector != "08635a95") { + log.error("Invalid function selector", [selector]); + return null; + } + + // Decode ABI using a modified types mapping since the decode method fails with dynamic types + // - Original types mapping: (bytes32[],uint256,address,address,uint256,uint256,uint256,uint256,bytes) + // - Modified types mapping: (uint256,uint256,address,address,uint256,uint256,uint256,uint256) + // + // The bytes32[] is replaced with uint256, this is ok since it's a dynamic type so the actual value stored + // is the length of the bytes array which is a uint256 and the array contents are appended at the end of the calldata + // + // The bytes at the end can easily be dropped since we don't use it + let types = + "(uint256,uint256,address,address,uint256,uint256,uint256,uint256)"; + + let decoded = ethereum.decode( + types, + Bytes.fromHexString(strippedCallData.substring(8)) + ); + if (decoded !== null) { + return decoded.toTuple()[1].toBigInt(); // transactionIndex + } else { + log.error("Could not decode call data!", []); + return null; + } +} + +// Get transactionIndex from the 'OutBoxTransactionExecuted' event, emitted by Arbitrum's Outbox contract on the same tx +// Returns null if the event is not found or if there are multiple events (i.e: a multicall call) +export function getTransactionIndexFromLogs( + event: ethereum.Event +): BigInt | null { + let data = getOutBoxTransactionExecutedData(event); + return data !== null ? data.transactionIndex : null; +} + +// Calculates Arbitrum's retryable ticket ID using tx event data +// Returns null if the required events are not found or if there are multiple duplicate events (i.e: a multicall call) +export function getRetryableTicketId( + event: DepositInitiated, + entity: BridgeDepositTransaction +): string | null { + // Get the data from the events + let messageDeliveredData = getMessageDeliveredData(event); + let inboxMessageDeliveredData = getInboxMessageDeliveredData(event); + let txToL2Data = getTxToL2Data(event); + + if ( + messageDeliveredData === null || + inboxMessageDeliveredData === null || + txToL2Data === null + ) + return null; + + // Build the input array + let fields: ByteArray[] = []; + + // TODO: get chainId dynamically + // 0x066EED = 421613 (Arbitrum Goerli) + // 0xA4B1 = 42161 (Arbitrum One) + let l2ChainId = Bytes.fromHexString("0xA4B1"); + fields.push(l2ChainId); + + let messageNumber = Bytes.fromHexString( + padZeros( + Bytes.fromUint8Array( + Bytes.fromBigInt(event.params.sequenceNumber).reverse() + ).toHexString() + ) + ); + fields.push(messageNumber); + + let fromAddress = messageDeliveredData.sender; + fields.push(fromAddress); + + let l1BaseFee = bigIntToBytes(messageDeliveredData.baseFeeL1); + fields.push(l1BaseFee); + + let l1CallValue = bigIntToBytes(inboxMessageDeliveredData.l1CallValue); + fields.push(l1CallValue); + + let maxFeePerGas = bigIntToBytes(inboxMessageDeliveredData.maxFeePerGas); + fields.push(maxFeePerGas); + + let gasLimit = bigIntToBytes(inboxMessageDeliveredData.gasLimit); + fields.push(gasLimit); + + let destinationAddress = inboxMessageDeliveredData.to; + fields.push(destinationAddress); + + let l2CallValue = bigIntToBytes(inboxMessageDeliveredData.l2CallValue); + fields.push(l2CallValue); + + let callValueRefundAddress = inboxMessageDeliveredData.callValueRefundAddress; + fields.push(callValueRefundAddress); + + let maxSubmissionFee = bigIntToBytes( + inboxMessageDeliveredData.maxSubmissionCost + ); + fields.push(maxSubmissionFee); + + let excessFeeRefundAddress = inboxMessageDeliveredData.excessFeeRefundAddress; + fields.push(excessFeeRefundAddress); + + let data = txToL2Data; + fields.push(data); + + return calculateSubmitRetryableId(fields); +} + +// Calculate the retryable ticket id +// See https://github.com/OffchainLabs/arbitrum-sdk/blob/1d69e5f03f537ef9af7aaa039a28a00cf61b2fc0/src/lib/message/L1ToL2Message.ts#L115-L161 +export function calculateSubmitRetryableId(input: ByteArray[]): string { + let txType = Bytes.fromHexString("0x69"); // arbitrum submit retry transactions have type 0x69 + let encodedFields = Bytes.fromHexString(RLPEncodeArray(input)); + let rlpEnc = txType.concat(encodedFields); + + return crypto.keccak256(rlpEnc).toHexString(); +} diff --git a/src/mappings/helpers/byte.ts b/src/mappings/helpers/byte.ts new file mode 100644 index 00000000..6353f4a8 --- /dev/null +++ b/src/mappings/helpers/byte.ts @@ -0,0 +1,32 @@ +import { BigInt, ByteArray, Bytes } from "@graphprotocol/graph-ts"; + +export function numberToBytes(num: u64): ByteArray { + return stripZeros(Bytes.fromU64(num).reverse()); +} + +export function bigIntToBytes(num: BigInt): Bytes { + return Bytes.fromUint8Array(stripZeros(Bytes.fromBigInt(num).reverse())); +} + +export function stripZeros(bytes: Uint8Array): ByteArray { + let i = 0; + while (i < bytes.length && bytes[i] == 0) { + i++; + } + return Bytes.fromUint8Array(bytes.slice(i)); +} + +export function strip0xPrefix(input: string): string { + return input.startsWith("0x") ? input.slice(2) : input; +} + +// Pads a hex string with zeros to 64 characters +export function padZeros(input: string): string { + let data = strip0xPrefix(input); + return "0x".concat(data.padStart(64, "0")); +} + +export function ensureEvenLength(input: string): string { + if (input.length % 2 == 0) return input + return "0x0".concat(strip0xPrefix(input.toString())); +} \ No newline at end of file diff --git a/src/mappings/helpers/event-log.ts b/src/mappings/helpers/event-log.ts new file mode 100644 index 00000000..7f3c69ab --- /dev/null +++ b/src/mappings/helpers/event-log.ts @@ -0,0 +1,43 @@ +import { Bytes, ethereum, log, crypto } from "@graphprotocol/graph-ts"; + +// Loops through all logs in an event tx receipt +// Returns the data blob for the log with a matching event signature +// If the event log is not found or there are multiple with the same signature, returns null +export function getDataFromEventLog( + event: ethereum.Event, + eventSignature: string +): Bytes | null { + let receipt = event.receipt; + if (receipt === null) { + log.error("Could not get tx receipt!", []); + return null; + } + + let eventCount = 0; + let eventData: Bytes | null = null; + + let logs = receipt.logs; + for (let i = 0; i < logs.length; i++) { + let topics = logs[i].topics; + + if (isEventLog(topics[0], eventSignature)) { + eventCount = eventCount + 1; + eventData = logs[i].data; + } + } + + if (eventCount != 1) { + let eventName = eventSignature.split("(")[0]; + log.error(`Event count for '${eventName}' is not 1!`, [ + eventCount.toString(), + ]); + eventData = null; // reset to null if there are multiple events + } + + return eventData; +} + +// Returns true if the topic corresponds to an event signature +function isEventLog(topic: Bytes, targetEventSignature: string): boolean { + return topic == crypto.keccak256(Bytes.fromUTF8(targetEventSignature)); +} \ No newline at end of file diff --git a/src/mappings/helpers/events/InboxMessageDelivered.ts b/src/mappings/helpers/events/InboxMessageDelivered.ts new file mode 100644 index 00000000..3794968e --- /dev/null +++ b/src/mappings/helpers/events/InboxMessageDelivered.ts @@ -0,0 +1,54 @@ +import { log, BigInt, Bytes, ethereum, Address } from "@graphprotocol/graph-ts"; +import { ensureEvenLength } from "../byte"; +import { getDataFromEventLog } from "../event-log"; + +// AS compiler doesn't like interfaces +class InboxMessageDelivered { + to: Bytes; + l2CallValue: BigInt; + l1CallValue: BigInt; + maxSubmissionCost: BigInt; + excessFeeRefundAddress: Bytes; + callValueRefundAddress: Bytes; + gasLimit: BigInt; + maxFeePerGas: BigInt; + dataLength: BigInt; +} + +let EVENT_NAME = "InboxMessageDelivered"; +let EVENT_SIGNATURE = + "InboxMessageDelivered(uint256,bytes)"; +let EVENT_DATA_TYPES = "(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256)"; + +export function getInboxMessageDeliveredData( + event: ethereum.Event +): InboxMessageDelivered | null { + let data = getDataFromEventLog(event, EVENT_SIGNATURE); + if (data === null) return null; + + return parseInboxMessageDeliveredData(data); +} + +export function parseInboxMessageDeliveredData(data: Bytes): InboxMessageDelivered | null { + let decoded = ethereum.decode(EVENT_DATA_TYPES, data); + + if (decoded !== null) { + let decodedTuple = decoded.toTuple(); + return { + to: Bytes.fromHexString(ensureEvenLength(decodedTuple[2].toBigInt().toHexString())), + l2CallValue: decodedTuple[3].toBigInt(), + l1CallValue: decodedTuple[4].toBigInt(), + maxSubmissionCost: decodedTuple[5].toBigInt(), + excessFeeRefundAddress: Bytes.fromHexString(ensureEvenLength(decodedTuple[6].toBigInt().toHexString())), + callValueRefundAddress: Bytes.fromHexString(ensureEvenLength(decodedTuple[7].toBigInt().toHexString())), + gasLimit: decodedTuple[8].toBigInt(), + maxFeePerGas: decodedTuple[9].toBigInt(), + dataLength: decodedTuple[10].toBigInt(), + }; + } else { + log.error(`Could not decode call data for ${EVENT_NAME}!`, [ + data.toHexString(), + ]); + return null; + } +} diff --git a/src/mappings/helpers/events/MessageDelivered.ts b/src/mappings/helpers/events/MessageDelivered.ts new file mode 100644 index 00000000..25deafab --- /dev/null +++ b/src/mappings/helpers/events/MessageDelivered.ts @@ -0,0 +1,39 @@ +import { log, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"; +import { getDataFromEventLog } from "../event-log"; + +// AS compiler doesn't like interfaces +class MessageDeliveredData { + sender: Bytes; + baseFeeL1: BigInt; +} + +let EVENT_NAME = "MessageDelivered"; +let EVENT_SIGNATURE = + "MessageDelivered(uint256,bytes32,address,uint8,address,bytes32,uint256,uint64)"; +let EVENT_DATA_TYPES = "(address,uint8,address,bytes32,uint256,uint64)"; + +export function getMessageDeliveredData( + event: ethereum.Event +): MessageDeliveredData | null { + let data = getDataFromEventLog(event, EVENT_SIGNATURE); + if (data === null) return null; + + return parseMessageDeliveredData(data); +} + +export function parseMessageDeliveredData(data: Bytes): MessageDeliveredData | null { + let decoded = ethereum.decode(EVENT_DATA_TYPES, data); + + if (decoded !== null) { + let decodedTuple = decoded.toTuple(); + return { + sender: decodedTuple[2].toAddress(), + baseFeeL1: decodedTuple[4].toBigInt(), + }; + } else { + log.error(`Could not decode call data for ${EVENT_NAME}!`, [ + data.toHexString(), + ]); + return null; + } +} diff --git a/src/mappings/helpers/events/OutBoxTransactionExecuted.ts b/src/mappings/helpers/events/OutBoxTransactionExecuted.ts new file mode 100644 index 00000000..70c9878f --- /dev/null +++ b/src/mappings/helpers/events/OutBoxTransactionExecuted.ts @@ -0,0 +1,38 @@ +import { log, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"; +import { getDataFromEventLog } from "../event-log"; + +// AS compiler doesn't like interfaces +class OutBoxTransactionExecutedData { + transactionIndex: BigInt; +} + +let EVENT_NAME = "OutBoxTransactionExecuted"; +let EVENT_SIGNATURE = + "OutBoxTransactionExecuted(address,address,uint256,uint256)"; +let EVENT_DATA_TYPES = "(uint256)"; + +export function getOutBoxTransactionExecutedData( + event: ethereum.Event +): OutBoxTransactionExecutedData | null { + let data = getDataFromEventLog(event, EVENT_SIGNATURE); + if (data === null) return null; + + return parseOutBoxTransactionExecutedData(data); +} + +function parseOutBoxTransactionExecutedData( + data: Bytes +): OutBoxTransactionExecutedData | null { + let decoded = ethereum.decode(EVENT_DATA_TYPES, data); + + if (decoded !== null) { + return { + transactionIndex: decoded.toTuple()[0].toBigInt(), + }; + } else { + log.error(`Could not decode call data for ${EVENT_NAME}!`, [ + data.toHexString(), + ]); + return null; + } +} diff --git a/src/mappings/helpers/events/TxToL2.ts b/src/mappings/helpers/events/TxToL2.ts new file mode 100644 index 00000000..86c2d31a --- /dev/null +++ b/src/mappings/helpers/events/TxToL2.ts @@ -0,0 +1,32 @@ +import { log, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"; +import { getDataFromEventLog } from "../event-log"; + +let EVENT_NAME = "TxToL2"; +let EVENT_SIGNATURE = + "TxToL2(address,address,uint256,bytes)"; +let EVENT_DATA_TYPES = "(uint256,uint256)"; + +export function getTxToL2Data( + event: ethereum.Event +): Bytes | null { + let data = getDataFromEventLog(event, EVENT_SIGNATURE); + if (data === null) return null; + + return parseTxToL2Data(data) +} + +export function parseTxToL2Data( + data: Bytes +): Bytes | null { + let decoded = ethereum.decode(EVENT_DATA_TYPES, data); + + if (decoded !== null) { + let dataLength = decoded.toTuple()[1].toBigInt(); + return Bytes.fromUint8Array(data.subarray(64, 64 + dataLength.toI32())); + } else { + log.error(`Could not decode call data for ${EVENT_NAME}!`, [ + data.toHexString(), + ]); + return null; + } +} \ No newline at end of file diff --git a/src/mappings/helpers.ts b/src/mappings/helpers/helpers.ts similarity index 99% rename from src/mappings/helpers.ts rename to src/mappings/helpers/helpers.ts index a1376777..8c99ffd2 100644 --- a/src/mappings/helpers.ts +++ b/src/mappings/helpers/helpers.ts @@ -28,12 +28,12 @@ import { SubgraphCategoryRelation, NameSignalSubgraphRelation, CurrentSubgraphDeploymentRelation, -} from '../types/schema' -import { ENS } from '../types/GNS/ENS' -import { Controller } from '../types/Controller/Controller' -import { EpochManager } from '../types/EpochManager/EpochManager' -import { fetchSubgraphDeploymentManifest } from './metadataHelpers' -import { addresses } from '../../config/addresses' +} from '../../types/schema' +import { ENS } from '../../types/GNS/ENS' +import { Controller } from '../../types/Controller/Controller' +import { EpochManager } from '../../types/EpochManager/EpochManager' +import { fetchSubgraphDeploymentManifest } from './metadata' +import { addresses } from '../../../config/addresses' export function createOrLoadSubgraph( bigIntID: BigInt, diff --git a/src/mappings/metadataHelpers.template.ts b/src/mappings/helpers/metadata.template.ts similarity index 99% rename from src/mappings/metadataHelpers.template.ts rename to src/mappings/helpers/metadata.template.ts index 6169729d..e3b36cce 100644 --- a/src/mappings/metadataHelpers.template.ts +++ b/src/mappings/helpers/metadata.template.ts @@ -1,7 +1,7 @@ import { json, ipfs, Bytes, JSONValueKind, log } from '@graphprotocol/graph-ts' import { GraphAccount, Subgraph, SubgraphVersion, SubgraphDeployment } from '../types/schema' import { jsonToString } from './utils' -import { createOrLoadSubgraphCategory, createOrLoadSubgraphCategoryRelation, createOrLoadNetwork } from './helpers' +import { createOrLoadSubgraphCategory, createOrLoadSubgraphCategoryRelation, createOrLoadNetwork } from './helpers/helpers' export function fetchGraphAccountMetadata(graphAccount: GraphAccount, ipfsHash: string): void { {{#ipfs}} diff --git a/src/mappings/helpers/rlp.ts b/src/mappings/helpers/rlp.ts new file mode 100644 index 00000000..d03c72f4 --- /dev/null +++ b/src/mappings/helpers/rlp.ts @@ -0,0 +1,38 @@ +import { ByteArray, Bytes } from "@graphprotocol/graph-ts"; +import { numberToBytes, stripZeros } from "./byte"; + +// Implementation based on https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ +export function RLPEncodeArray(input: ByteArray[]): string { + const output: string[] = []; // in hexstring + let outputLength = 0; // in bytes + + for (let i = 0; i < input.length; i++) { + const encoded = RLPEncode(input[i]); + output.push(encoded); + outputLength += encoded.length / 2 - 1; // don't count 0x prefix + } + + const encodedLength = encodeLength(outputLength, 192); + const concatenatedOutput = output.reduce( + (acc, val) => acc.concat(Bytes.fromHexString(val)), + stripZeros(Bytes.empty()) + ); + return encodedLength.concat(concatenatedOutput).toHexString(); +} + +export function RLPEncode(input: ByteArray): string { + if (input.length == 1 && input[0] < 128) { + return input.toHexString(); + } + const encodedLength = encodeLength(input.length, 128); + return encodedLength.concat(input).toHexString(); +} + +function encodeLength(len: u64, offset: u64): ByteArray { + if (len < 56) { + return numberToBytes(len + offset); + } + + const prefix = offset + 55 + numberToBytes(len).length; + return numberToBytes(prefix).concat(numberToBytes(len)); +} \ No newline at end of file diff --git a/src/mappings/l1Gateway.ts b/src/mappings/l1Gateway.ts index 2071e378..3d7b437a 100644 --- a/src/mappings/l1Gateway.ts +++ b/src/mappings/l1Gateway.ts @@ -1,16 +1,11 @@ import { - DepositInitiated, WithdrawalFinalized, + DepositInitiated, } from '../types/L1GraphTokenGateway/L1GraphTokenGateway' -import { BridgeWithdrawalTransaction } from '../types/schema' -import { getTransactionIndex } from './bridgeHelpers' -import { createOrLoadGraphNetwork } from './helpers' - -export function handleDepositInitiated(event: DepositInitiated): void { - let graphNetwork = createOrLoadGraphNetwork(event.block.number, event.address) - graphNetwork.totalGRTDeposited = graphNetwork.totalGRTDeposited.plus(event.params.amount) - graphNetwork.save() -} +import { BridgeWithdrawalTransaction, BridgeDepositTransaction } from '../types/schema' +import { getRetryableTicketId, getTransactionIndex } from './helpers/bridge' +import { getDataFromEventLog } from './helpers/event-log' +import { createOrLoadGraphNetwork } from './helpers/helpers' export function handleWithdrawalFinalized(event: WithdrawalFinalized): void { // Update total GRT withdrawn confirmed @@ -20,7 +15,7 @@ export function handleWithdrawalFinalized(event: WithdrawalFinalized): void { ) graphNetwork.save() - // Save the withdrawal transaction + // Save withdrawal data let entity = new BridgeWithdrawalTransaction( event.transaction.hash.concatI32(event.logIndex.toI32()).toHexString(), ) @@ -32,11 +27,39 @@ export function handleWithdrawalFinalized(event: WithdrawalFinalized): void { entity.txHash = event.transaction.hash entity.from = event.params.from entity.to = event.params.to + entity.amount = event.params.amount + entity.l1Token = event.params.l1Token + entity.transactionIndex = getTransactionIndex(event) + entity.save() +} + +export function handleDepositInitiated(event: DepositInitiated): void { + // Update total GRT deposited + let graphNetwork = createOrLoadGraphNetwork(event.block.number, event.address) + graphNetwork.totalGRTDeposited = graphNetwork.totalGRTDeposited.plus(event.params.amount) + graphNetwork.save() + + // Save deposit data + let entity = new BridgeDepositTransaction( + event.transaction.hash.concatI32(event.logIndex.toI32()).toHexString(), + ) + + entity.blockNumber = event.block.number.toI32() + entity.timestamp = event.block.timestamp.toI32() + entity.signer = event.params.from.toHexString() + entity.type = 'BridgeDeposit' + entity.txHash = event.transaction.hash + entity.from = event.params.from + entity.to = event.params.to entity.amount = event.params.amount entity.l1Token = event.params.l1Token + entity.retryableTicketId = getRetryableTicketId(event, entity) - entity.transactionIndex = getTransactionIndex(event) + // Deposits initiated through Arbitrum's gateway router will emit a TransferRouted event + let EVENT_SIGNATURE = 'TransferRouted(address,address,address,address)' + let data = getDataFromEventLog(event, EVENT_SIGNATURE) + entity.routed = data !== null entity.save() } diff --git a/src/mappings/l2Gateway.ts b/src/mappings/l2Gateway.ts index 93fce78e..be4efb1e 100644 --- a/src/mappings/l2Gateway.ts +++ b/src/mappings/l2Gateway.ts @@ -1,11 +1,14 @@ import { WithdrawalInitiated } from '../types/L2GraphTokenGateway/L2GraphTokenGateway' -import { createOrLoadGraphNetwork } from './helpers' +import { createOrLoadGraphNetwork } from './helpers/helpers' import { BridgeWithdrawalTransaction } from '../types/schema' export function handleWithdrawalInitiated(event: WithdrawalInitiated): void { + // Update total GRT withdrawn let graphNetwork = createOrLoadGraphNetwork(event.block.number, event.address) + graphNetwork.totalGRTWithdrawn = graphNetwork.totalGRTWithdrawn.plus(event.params.amount) + graphNetwork.save() - // Save the withdrawal transaction + // Save withdrawal data let withdrawalTransaction = new BridgeWithdrawalTransaction( event.transaction.hash.toHexString().concat('-').concat(event.logIndex.toString()), ) @@ -21,9 +24,10 @@ export function handleWithdrawalInitiated(event: WithdrawalInitiated): void { withdrawalTransaction.transactionIndex = event.params.l2ToL1Id withdrawalTransaction.amount = event.params.amount withdrawalTransaction.l1Token = event.params.l1Token - withdrawalTransaction.save() - // Update total GRT withdrawn - graphNetwork.totalGRTWithdrawn = graphNetwork.totalGRTWithdrawn.plus(event.params.amount) - graphNetwork.save() + withdrawalTransaction.save() } + +export function handleDepositFinalized(event: DepositFinalized): void { + +} \ No newline at end of file diff --git a/src/mappings/rewardsManager.ts b/src/mappings/rewardsManager.ts index 95014158..cc480e8b 100644 --- a/src/mappings/rewardsManager.ts +++ b/src/mappings/rewardsManager.ts @@ -12,7 +12,7 @@ import { updateAdvancedIndexerMetrics, updateDelegationExchangeRate, createOrLoadGraphNetwork -} from './helpers' +} from './helpers/helpers' import { addresses } from '../../config/addresses' export function handleRewardsAssigned(event: RewardsAssigned): void { diff --git a/src/mappings/serviceRegistry.ts b/src/mappings/serviceRegistry.ts index fd40f2c8..83268a10 100644 --- a/src/mappings/serviceRegistry.ts +++ b/src/mappings/serviceRegistry.ts @@ -1,7 +1,7 @@ import { ServiceRegistered, ServiceUnregistered } from '../types/ServiceRegistry/ServiceRegistry' import { Indexer } from '../types/schema' -import { createOrLoadIndexer, createOrLoadGraphAccount } from './helpers' +import { createOrLoadIndexer, createOrLoadGraphAccount } from './helpers/helpers' /** * @dev handleServiceRegistered diff --git a/src/mappings/staking.ts b/src/mappings/staking.ts index c1185ad9..fb9682d1 100644 --- a/src/mappings/staking.ts +++ b/src/mappings/staking.ts @@ -43,7 +43,7 @@ import { calculatePricePerShare, batchUpdateSubgraphSignalledTokens, createOrLoadGraphNetwork, -} from './helpers' +} from './helpers/helpers' import { addresses } from '../../config/addresses' export function handleDelegationParametersUpdated(event: DelegationParametersUpdated): void { diff --git a/src/mappings/tokenLockWallets/manager.ts b/src/mappings/tokenLockWallets/manager.ts index 81d92c74..7f84bacb 100644 --- a/src/mappings/tokenLockWallets/manager.ts +++ b/src/mappings/tokenLockWallets/manager.ts @@ -9,7 +9,7 @@ import { } from '../../types/GraphTokenLockManager/GraphTokenLockManager' import { GraphTokenLockWallet } from '../../types/templates' import { TokenManager, TokenLockWallet, AuthorizedFunction } from '../../types/schema' -import { createOrLoadGraphAccount, createOrLoadGraphNetwork } from '../helpers' +import { createOrLoadGraphAccount, createOrLoadGraphNetwork } from '../helpers/helpers' import { addresses } from '../../../config/addresses' export function handleMasterCopyUpdated(event: MasterCopyUpdated): void {