From 8e1f06e0c675856573a19b8940aa371b9c49eae3 Mon Sep 17 00:00:00 2001 From: Lukasz Zimnoch Date: Tue, 1 Aug 2023 19:46:43 +0200 Subject: [PATCH] Basic redemptions monitoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changeset adds redemptions to the monitoring system. Specifically, two new system events are supported now: ### Redemption requested An *informational system event* indicating that a new redemption was requested from the on-chain Bridge contract. This event is directly sent to Discord as a notification that does not require any action. ### Large redemption requested A *warning system event* indicating that a large redemption was requested from the on-chain Bridge contract. This event is sent to Sentry hub and should get team’s attention. The default action is making sure that the redemption is not a result of a malicious action, and if not, that the redemption is handled correctly by the system. --- monitoring/README.adoc | 2 + monitoring/docs/monitoring-and-telemetry.adoc | 16 ++++ monitoring/src/block-explorer.ts | 23 +++++ monitoring/src/context.ts | 2 + monitoring/src/deposit-monitor.ts | 35 ++------ monitoring/src/index.ts | 2 + monitoring/src/redemption-monitor.ts | 88 +++++++++++++++++++ 7 files changed, 140 insertions(+), 28 deletions(-) create mode 100644 monitoring/src/block-explorer.ts create mode 100644 monitoring/src/redemption-monitor.ts diff --git a/monitoring/README.adoc b/monitoring/README.adoc index db0300d48..826c8a809 100644 --- a/monitoring/README.adoc +++ b/monitoring/README.adoc @@ -158,6 +158,8 @@ The behavior can be configured using the following env variables: |*LARGE_DEPOSIT_THRESHOLD_SAT* |Satoshi threshold used to determine which deposits are large. Default: _1000000000_ |No +|*LARGE_REDEMPTION_THRESHOLD_SAT* |Satoshi threshold used to determine which redemptions are large. Default: _1000000000_ |No + |*DATA_DIR_PATH* |Directory used to persist processing data. Default: _./data_ |No |*SENTRY_DSN* |DSN of the Sentry receiver. If not set, events are not dispatched to Sentry |No diff --git a/monitoring/docs/monitoring-and-telemetry.adoc b/monitoring/docs/monitoring-and-telemetry.adoc index 2f369af2e..a1a8c52ad 100644 --- a/monitoring/docs/monitoring-and-telemetry.adoc +++ b/monitoring/docs/monitoring-and-telemetry.adoc @@ -111,11 +111,13 @@ are propagated to the Sentry hub that decides about next steps. Specific system events produced by the monitoring component are: * deposit revealed, +* redemption requested, * wallet registered, * DKG result submitted, * DKG result approved, * DKG result challenged, * large deposit revealed, +* large redemption requested, * optimistic minting canceled, * optimistic minting requested too early, * optimistic minting requested for undetermined Bitcoin transaction, @@ -131,6 +133,12 @@ An *informational system event* indicating that a new deposit was revealed to the on-chain Bridge contract. This event is directly sent to Discord as a notification that does not require any action. +==== Redemption requested + +An *informational system event* indicating that a new redemption was requested +from the on-chain Bridge contract. This event is directly sent to Discord as a +notification that does not require any action. + ==== Wallet registered An *informational system event* indicating that a new wallet was registered @@ -164,6 +172,14 @@ on-chain Bridge contract. This event is sent to Sentry hub and should get team’s attention. The default action is making sure that the deposit is handled correctly by the system. +==== Large redemption requested + +A *warning system event* indicating that a large redemption was requested from +the on-chain Bridge contract. This event is sent to Sentry hub and should get +team’s attention. The default action is making sure that the redemption is +not a result of a malicious action, and if not, that the redemption is +handled correctly by the system. + ==== Optimistic minting cancelled A *warning system event* indicating that an optimistic minting request was diff --git a/monitoring/src/block-explorer.ts b/monitoring/src/block-explorer.ts new file mode 100644 index 000000000..21847a761 --- /dev/null +++ b/monitoring/src/block-explorer.ts @@ -0,0 +1,23 @@ +import { context, Environment } from "./context" + +import type { BitcoinTransactionHash, Hex } from "@keep-network/tbtc-v2.ts" + +const ethTxUrlPrefixMapping = { + [Environment.Mainnet]: "https://etherscan.io/tx", + [Environment.Testnet]: "https://goerli.etherscan.io/tx", +} + +export function createEthTxUrl(txHash: Hex) { + return `${ + ethTxUrlPrefixMapping[context.environment] + }/${txHash.toPrefixedString()}` +} + +const btcTxUrlPrefixMapping = { + [Environment.Mainnet]: "https://mempool.space/tx", + [Environment.Testnet]: "https://mempool.space/testnet/tx", +} + +export function createBtcTxUrl(txHash: BitcoinTransactionHash) { + return `${btcTxUrlPrefixMapping[context.environment]}/${txHash.toString()}` +} diff --git a/monitoring/src/context.ts b/monitoring/src/context.ts index 57e6af324..a153d5abf 100644 --- a/monitoring/src/context.ts +++ b/monitoring/src/context.ts @@ -4,6 +4,7 @@ const { ETHEREUM_URL, ELECTRUM_URL, LARGE_DEPOSIT_THRESHOLD_SAT, + LARGE_REDEMPTION_THRESHOLD_SAT, DATA_DIR_PATH, SENTRY_DSN, DISCORD_WEBHOOK_URL, @@ -52,6 +53,7 @@ export const context = { ethereumUrl: resolveEthereumUrl(), electrumUrl: resolveElectrumUrl(), largeDepositThresholdSat: LARGE_DEPOSIT_THRESHOLD_SAT ?? 1000000000, // 10 BTC by default + largeRedemptionThresholdSat: LARGE_REDEMPTION_THRESHOLD_SAT ?? 1000000000, // 10 BTC by default dataDirPath: DATA_DIR_PATH ?? "./data", sentryDsn: SENTRY_DSN, discordWebhookUrl: DISCORD_WEBHOOK_URL, diff --git a/monitoring/src/deposit-monitor.ts b/monitoring/src/deposit-monitor.ts index 751ffbd10..f11a1e8e2 100644 --- a/monitoring/src/deposit-monitor.ts +++ b/monitoring/src/deposit-monitor.ts @@ -1,43 +1,21 @@ import { BigNumber } from "ethers" import { SystemEventType } from "./system-event" -import { context, Environment } from "./context" +import { context } from "./context" +import { createBtcTxUrl, createEthTxUrl } from "./block-explorer" import type { SystemEvent, Monitor as SystemEventMonitor } from "./system-event" import type { DepositRevealedEvent as DepositRevealedChainEvent } from "@keep-network/tbtc-v2.ts/dist/src/deposit" import type { Bridge } from "@keep-network/tbtc-v2.ts/dist/src/chain" -const satsToRoundedBTC = (sats: BigNumber): string => +export const satsToRoundedBTC = (sats: BigNumber): string => (sats.div(BigNumber.from(1e6)).toNumber() / 100).toFixed(2) -const hashUrls = (chainEvent: DepositRevealedChainEvent) => { - let fundingHashUrlPrefix = "" - let revealHashUrlPrefix = "" - switch (context.environment) { - case Environment.Mainnet: { - fundingHashUrlPrefix = "https://mempool.space/tx/" - revealHashUrlPrefix = "https://etherscan.io/tx/" - break - } - case Environment.Testnet: { - fundingHashUrlPrefix = "https://mempool.space/testnet/tx/" - revealHashUrlPrefix = "https://goerli.etherscan.io/tx/" - break - } - } - - const fundingHash = chainEvent.fundingTxHash.toString() - const transactionHash = chainEvent.transactionHash.toPrefixedString() - return { - btcFundingTxHashURL: fundingHashUrlPrefix + fundingHash, - ethRevealTxHashURL: revealHashUrlPrefix + transactionHash, - } -} - const DepositRevealed = ( chainEvent: DepositRevealedChainEvent ): SystemEvent => { - const { btcFundingTxHashURL, ethRevealTxHashURL } = hashUrls(chainEvent) + const btcFundingTxHashURL = createBtcTxUrl(chainEvent.fundingTxHash) + const ethRevealTxHashURL = createEthTxUrl(chainEvent.transactionHash) return { title: "Deposit revealed", @@ -57,7 +35,8 @@ const DepositRevealed = ( const LargeDepositRevealed = ( chainEvent: DepositRevealedChainEvent ): SystemEvent => { - const { btcFundingTxHashURL, ethRevealTxHashURL } = hashUrls(chainEvent) + const btcFundingTxHashURL = createBtcTxUrl(chainEvent.fundingTxHash) + const ethRevealTxHashURL = createEthTxUrl(chainEvent.transactionHash) return { title: "Large deposit revealed", diff --git a/monitoring/src/index.ts b/monitoring/src/index.ts index 900c48f36..a382e3b19 100644 --- a/monitoring/src/index.ts +++ b/monitoring/src/index.ts @@ -13,6 +13,7 @@ import { context } from "./context" import { MintingMonitor } from "./minting-monitor" import { WalletMonitor } from "./wallet-monitor" import { SupplyMonitor } from "./supply-monitor" +import { RedemptionMonitor } from "./redemption-monitor" import type { Client as BitcoinClient } from "@keep-network/tbtc-v2.ts/dist/src/bitcoin" import type { @@ -27,6 +28,7 @@ const monitors: SystemEventMonitor[] = [ new MintingMonitor(contracts.bridge, contracts.tbtcVault, btcClient), new SupplyMonitor(contracts.tbtcToken, new SupplyMonitorFilePersistence()), new WalletMonitor(contracts.bridge), + new RedemptionMonitor(contracts.bridge), ] const receivers: SystemEventReceiver[] = ((): SystemEventReceiver[] => { diff --git a/monitoring/src/redemption-monitor.ts b/monitoring/src/redemption-monitor.ts new file mode 100644 index 000000000..45048cf4d --- /dev/null +++ b/monitoring/src/redemption-monitor.ts @@ -0,0 +1,88 @@ +import { BigNumber } from "ethers" + +import { context } from "./context" +import { SystemEventType } from "./system-event" +import { satsToRoundedBTC } from "./deposit-monitor" +import { createEthTxUrl } from "./block-explorer" + +import type { RedemptionRequestedEvent as RedemptionRequestedChainEvent } from "@keep-network/tbtc-v2.ts/dist/src/redemption" +import type { Bridge } from "@keep-network/tbtc-v2.ts/dist/src/chain" +import type { Monitor as SystemEventMonitor, SystemEvent } from "./system-event" + +const RedemptionRequested = ( + chainEvent: RedemptionRequestedChainEvent +): SystemEvent => { + const ethRequestTxHashURL = createEthTxUrl(chainEvent.transactionHash) + + return { + title: "Redemption requested", + type: SystemEventType.Informational, + data: { + walletPublicKeyHash: chainEvent.walletPublicKeyHash, + redeemerOutputScript: chainEvent.redeemerOutputScript, + requestedAmountBTC: satsToRoundedBTC(chainEvent.requestedAmount), + ethRequestTxHash: chainEvent.transactionHash.toPrefixedString(), + ethRequestTxHashURL, + }, + block: chainEvent.blockNumber, + } +} + +const LargeRedemptionRequested = ( + chainEvent: RedemptionRequestedChainEvent +): SystemEvent => { + const ethRequestTxHashURL = createEthTxUrl(chainEvent.transactionHash) + + return { + title: "Large redemption requested", + type: SystemEventType.Warning, + data: { + walletPublicKeyHash: chainEvent.walletPublicKeyHash, + redeemerOutputScript: chainEvent.redeemerOutputScript, + requestedAmountBTC: satsToRoundedBTC(chainEvent.requestedAmount), + ethRequestTxHash: chainEvent.transactionHash.toPrefixedString(), + ethRequestTxHashURL, + }, + block: chainEvent.blockNumber, + } +} + +export class RedemptionMonitor implements SystemEventMonitor { + private bridge: Bridge + + constructor(bridge: Bridge) { + this.bridge = bridge + } + + async check(fromBlock: number, toBlock: number): Promise { + // eslint-disable-next-line no-console + console.log("running redemption monitor check") + + const chainEvents = await this.bridge.getRedemptionRequestedEvents({ + fromBlock, + toBlock, + }) + + const systemEvents: SystemEvent[] = [] + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < chainEvents.length; i++) { + const chainEvent = chainEvents[i] + + systemEvents.push(RedemptionRequested(chainEvent)) + + if ( + chainEvent.requestedAmount.gt( + BigNumber.from(context.largeRedemptionThresholdSat) + ) + ) { + systemEvents.push(LargeRedemptionRequested(chainEvent)) + } + } + + // eslint-disable-next-line no-console + console.log("completed redemption monitor check") + + return systemEvents + } +}