diff --git a/bridge/.env.development b/bridge/.env.development index 5e24655..bd67b0d 100644 --- a/bridge/.env.development +++ b/bridge/.env.development @@ -76,3 +76,6 @@ MAX_GAS_PRICE="300000000000" PAGERDUTY_ROUTING_KEY="..." SLACK_CHANNEL_NAME="#nine-chronicles-bridge-bot-test" + +# Libplanet account to collect fees +FEE_COLLECTOR_ADDRESS="0x123456" diff --git a/bridge/.env.example b/bridge/.env.example index 4ffbe63..344a3b7 100644 --- a/bridge/.env.example +++ b/bridge/.env.example @@ -132,4 +132,7 @@ FEE_RANGE_DIVIDER_AMOUNT= PLANET_ODIN_ID= PLANET_HEIMDALL_ID= -ODIN_TO_HEIMDALL_VALUT_ADDRESS= \ No newline at end of file +ODIN_TO_HEIMDALL_VALUT_ADDRESS= + +# Libplanet account to collect fees +FEE_COLLECTOR_ADDRESS="0x123456" diff --git a/bridge/src/index.ts b/bridge/src/index.ts index 24fadaa..4670bdc 100644 --- a/bridge/src/index.ts +++ b/bridge/src/index.ts @@ -360,9 +360,7 @@ process.on("uncaughtException", console.error); }; if (!web3.utils.isAddress(NCG_MINTER)) { - throw Error( - "NCG_MINTER variable seems invalid because it is not valid address format." - ); + throw Error("NCG_MINTER is invalid - it is not valid address format."); } const kmsAddresses = await kmsProvider.getAccounts(); @@ -395,6 +393,15 @@ process.on("uncaughtException", console.error); 1 ); + const FEE_COLLECTOR_ADDRESS: string = Configuration.get( + "FEE_COLLECTOR_ADDRESS" + ); + if (!web3.utils.isAddress(FEE_COLLECTOR_ADDRESS)) { + throw Error( + "FEE_COLLECTOR_ADDRESS is invalid - it is not valid address format." + ); + } + async function makeSafeWrappedNCGMinter(): Promise { if ( !USE_SAFE_WRAPPED_NCG_MINTER || @@ -531,7 +538,8 @@ process.on("uncaughtException", console.error); addressBanPolicy, integration, FAILURE_SUBSCRIBERS, - whitelistAccounts + whitelistAccounts, + FEE_COLLECTOR_ADDRESS ); const nineChroniclesMonitor = new NineChroniclesTransferredEventMonitor( await monitorStateStore.load("nineChronicles"), diff --git a/bridge/src/interfaces/wrapped-ncg-minter.ts b/bridge/src/interfaces/wrapped-ncg-minter.ts index 1dbb31f..876987b 100644 --- a/bridge/src/interfaces/wrapped-ncg-minter.ts +++ b/bridge/src/interfaces/wrapped-ncg-minter.ts @@ -1,4 +1,3 @@ -import { TransactionReceipt } from "web3-core"; import Decimal from "decimal.js"; /** diff --git a/bridge/src/messages/wrapped-event.ts b/bridge/src/messages/wrapped-event.ts index 5a5bfba..c02c0b2 100644 --- a/bridge/src/messages/wrapped-event.ts +++ b/bridge/src/messages/wrapped-event.ts @@ -16,6 +16,7 @@ export class WrappedEvent extends WrappingEvent { private readonly _refundTxId: string | null; private readonly _isWhitelistEvent: boolean; private readonly _description: string | undefined; + private readonly _feeTransferTxId: TxId; constructor( explorerUrl: string, @@ -31,7 +32,8 @@ export class WrappedEvent extends WrappingEvent { refundAmount: string | null, refundTxId: TxId | null, isWhitelistEvent: boolean, - description: string | undefined + description: string | undefined, + feeTransferTxId: TxId ) { super(explorerUrl, ncscanUrl, useNcscan, etherscanUrl); @@ -45,6 +47,7 @@ export class WrappedEvent extends WrappingEvent { this._refundTxId = refundTxId; this._isWhitelistEvent = isWhitelistEvent; this._description = description; + this._feeTransferTxId = feeTransferTxId; } render(): ForceOmit, "channel"> { @@ -98,6 +101,10 @@ export class WrappedEvent extends WrappingEvent { title: "fee", value: this._fee.toString(), }, + { + title: "9c network transaction (fee transfer)", + value: this.toExplorerUrl(this._feeTransferTxId), + }, ...refundFields, ], fallback: `NCG ${this._sender} → wNCG ${this._recipient}`, diff --git a/bridge/src/observers/nine-chronicles.ts b/bridge/src/observers/nine-chronicles.ts index 14598c7..29187cf 100644 --- a/bridge/src/observers/nine-chronicles.ts +++ b/bridge/src/observers/nine-chronicles.ts @@ -64,6 +64,7 @@ export class NCGTransferredEventObserver private readonly _integration: Integration; private readonly _whitelistAccounts: WhitelistAccount[]; + private readonly _feeCollectorAddress: string; constructor( ncgTransfer: INCGTransfer, @@ -82,7 +83,8 @@ export class NCGTransferredEventObserver addressBanPolicy: IAddressBanPolicy, integration: Integration, failureSubscribers: string, - whitelistAccounts: WhitelistAccount[] + whitelistAccounts: WhitelistAccount[], + feeCollectorAddress: string ) { this._ncgTransfer = ncgTransfer; this._wrappedNcgTransfer = wrappedNcgTransfer; @@ -101,6 +103,7 @@ export class NCGTransferredEventObserver this._integration = integration; this._failureSubscribers = failureSubscribers; this._whitelistAccounts = whitelistAccounts; + this._feeCollectorAddress = feeCollectorAddress; } async notify(data: { @@ -401,8 +404,18 @@ export class NCGTransferredEventObserver recipient!, ethereumExchangeAmount ); - - console.log("Receipt", transactionHash); + console.log("WNCG mint tx", transactionHash); + + // Transfer fee to the fee collector address if any + let feeTransferTxId: string = "No Fee Incurred"; + if (fee.greaterThan(0)) { + feeTransferTxId = await this._ncgTransfer.transfer( + this._feeCollectorAddress, + fee.toString(), + "I'm bridge and the fee is sent to fee collector." + ); + console.log("Fee transfer tx", feeTransferTxId); + } const isWhitelistEvent: boolean = accountType !== ACCOUNT_TYPE.GENERAL; this._slackMessageSender.sendMessage( @@ -420,7 +433,8 @@ export class NCGTransferredEventObserver refundAmount, refundTxId, isWhitelistEvent, - whitelistDescription + whitelistDescription, + feeTransferTxId ) ); this._opensearchClient.to_opensearch("info", { @@ -428,6 +442,7 @@ export class NCGTransferredEventObserver libplanetTxId: txId, ethereumTxId: transactionHash, fee: fee.toNumber(), + feeTransferTxId: feeTransferTxId, sender: sender, recipient: recipient, amount: exchangeAmount.toNumber(), diff --git a/bridge/test/messages/__snapshots__/wrapped-event.spec.ts.snap b/bridge/test/messages/__snapshots__/wrapped-event.spec.ts.snap index d12fb0f..3dd5959 100644 --- a/bridge/test/messages/__snapshots__/wrapped-event.spec.ts.snap +++ b/bridge/test/messages/__snapshots__/wrapped-event.spec.ts.snap @@ -32,6 +32,10 @@ Object { "title": "fee", "value": "1", }, + Object { + "title": "9c network transaction (fee transfer)", + "value": "https://explorer.libplanet.io/9c-internal/transaction?0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8", + }, ], }, ], @@ -71,6 +75,10 @@ Object { "title": "fee", "value": "1", }, + Object { + "title": "9c network transaction (fee transfer)", + "value": "https://explorer.libplanet.io/9c-internal/transaction?0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8", + }, Object { "title": "refund amount", "value": "9999900000", @@ -118,6 +126,10 @@ Object { "title": "fee", "value": "1", }, + Object { + "title": "9c network transaction (fee transfer)", + "value": "https://explorer.libplanet.io/9c-internal/transaction?0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8", + }, Object { "title": "description", "value": "test description", diff --git a/bridge/test/messages/wrapped-event.spec.ts b/bridge/test/messages/wrapped-event.spec.ts index 73bc3b8..9d100eb 100644 --- a/bridge/test/messages/wrapped-event.spec.ts +++ b/bridge/test/messages/wrapped-event.spec.ts @@ -16,7 +16,9 @@ describe("WrappedEvent", () => { const NINE_CHRONICLES_TX_ID = "3409cdbaa24ec6f7c8d2c0f636325a2b2e9611e5e6df5c593cfcd299860d8043"; const FEE = new Decimal(1); - const isWhitelistEvent = false; + const IS_WHITELIST_EVENT = false; + const FEE_TRANSFER_TX_ID = + "0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8"; expect( new WrappedEvent( EXPLORER_URL, @@ -31,8 +33,9 @@ describe("WrappedEvent", () => { FEE, null, null, - isWhitelistEvent, - undefined + IS_WHITELIST_EVENT, + undefined, + FEE_TRANSFER_TX_ID ).render() ).toMatchSnapshot(); }); @@ -53,7 +56,9 @@ describe("WrappedEvent", () => { const REFUND_AMOUNT = "9999900000"; const REFUND_TX_ID = "a3cd151aa0cb24b3e692f433b857f08bd347dad0d2d6ca3666f26420b8b8d096"; - const isWhitelistEvent = false; + const IS_WHITELIST_EVENT = false; + const FEE_TRANSFER_TX_ID = + "0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8"; expect( new WrappedEvent( EXPLORER_URL, @@ -68,8 +73,9 @@ describe("WrappedEvent", () => { FEE, REFUND_AMOUNT, REFUND_TX_ID, - isWhitelistEvent, - undefined + IS_WHITELIST_EVENT, + undefined, + FEE_TRANSFER_TX_ID ).render() ).toMatchSnapshot(); }); @@ -87,7 +93,9 @@ describe("WrappedEvent", () => { const NINE_CHRONICLES_TX_ID = "3409cdbaa24ec6f7c8d2c0f636325a2b2e9611e5e6df5c593cfcd299860d8044"; const FEE = new Decimal(1); - const isWhitelistEvent = true; + const IS_WHITELIST_EVENT = true; + const FEE_TRANSFER_TX_ID = + "0x9360cd40682a91a71f0afbfac3dd381866cdb319dc01c13531dfe648f8a28bc8"; expect( new WrappedEvent( EXPLORER_URL, @@ -102,8 +110,9 @@ describe("WrappedEvent", () => { FEE, null, null, - isWhitelistEvent, - "test description" + IS_WHITELIST_EVENT, + "test description", + FEE_TRANSFER_TX_ID ).render() ).toMatchSnapshot(); }); diff --git a/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap b/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap index 2dd1daf..11dbe13 100644 --- a/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap +++ b/bridge/test/observers/__snapshots__/nine-chronicles.spec.ts.snap @@ -5,10 +5,11 @@ Array [ Array [ "info", Object { - "amount": 48651.5, + "amount": 97151.5, "content": "NCG -> wNCG request success", "ethereumTxId": "TRANSACTION-HASH", - "fee": 1298.5, + "fee": 2798.5, + "feeTransferTxId": "TX-ID", "libplanetTxId": "TX-A", "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", "sender": "0x2734048eC2892d111b4fbAB224400847544FC872", @@ -18,7 +19,7 @@ Array [ "error", Object { "amount": 150, - "cause": "24 hr transfer maximum 50000 reached. User transferred 49950 NCGs in 24 hrs.", + "cause": "24 hr transfer maximum 100000 reached. User transferred 99950 NCGs in 24 hrs.", "content": "NCG -> wNCG request failure", "libplanetTxId": "TX-SHOULD-REFUND", "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", @@ -69,11 +70,15 @@ Array [ }, Object { "title": "amount", - "value": "48651.5", + "value": "97151.5", }, Object { "title": "fee", - "value": "1298.5", + "value": "2798.5", + }, + Object { + "title": "9c network transaction (fee transfer)", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-ID", }, ], }, @@ -91,7 +96,7 @@ Array [ "fields": Array [ Object { "title": "Reason", - "value": "0x2734048eC2892d111b4fbAB224400847544FC872 tried to exchange 150 and already exchanged 49950 and users can exchange until 50000 in 24 hours so refund NCG as 100", + "value": "0x2734048eC2892d111b4fbAB224400847544FC872 tried to exchange 150 and already exchanged 99950 and users can exchange until 100000 in 24 hours so refund NCG as 100", }, Object { "title": "Address", @@ -278,6 +283,7 @@ Array [ "content": "NCG -> wNCG request success", "ethereumTxId": "TRANSACTION-HASH", "fee": 10, + "feeTransferTxId": "TX-ID", "libplanetTxId": "TX-ID", "recipient": "0x4029bC50b4747A037d38CF2197bCD335e22Ca301", "sender": "0x2734048eC2892d111b4fbAB224400847544FC872", @@ -320,6 +326,10 @@ Array [ "title": "fee", "value": "10", }, + Object { + "title": "9c network transaction (fee transfer)", + "value": "https://explorer.libplanet.io/9c-internal/transaction?TX-ID", + }, ], }, ], diff --git a/bridge/test/observers/nine-chronicles.spec.ts b/bridge/test/observers/nine-chronicles.spec.ts index 7300aaa..bb496d6 100644 --- a/bridge/test/observers/nine-chronicles.spec.ts +++ b/bridge/test/observers/nine-chronicles.spec.ts @@ -15,7 +15,6 @@ import { SlackMessageSender } from "../../src/slack-message-sender"; import { ACCOUNT_TYPE } from "../../src/whitelist/account-type"; import { SpreadsheetClient } from "../../src/spreadsheet-client"; import { google } from "googleapis"; -import { WrappingRetryIgnoreEvent } from "../../src/messages/wrapping-retry-ignore-event"; jest.mock("@slack/web-api", () => { return { @@ -103,8 +102,8 @@ describe(NCGTransferredEventObserver.name, () => { }; const limitationPolicy = { - maximum: 50000, - whitelistMaximum: 200000, + maximum: 100000, + whitelistMaximum: 1000000, minimum: 100, }; const BANNED_ADDRESS = "0x47D082a115c63E7b58B1532d20E631538eaFADde"; @@ -157,6 +156,8 @@ describe(NCGTransferredEventObserver.name, () => { const noLimitOnePercentFeeRecipient = "0x185B5c3d26c12F2BB2A228d209D83eD80CAa03aF"; + const feeCollectorAddress = "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e"; + const observer = new NCGTransferredEventObserver( mockNcgTransfer, mockWrappedNcgMinter, @@ -190,7 +191,8 @@ describe(NCGTransferredEventObserver.name, () => { from: noLimitOnePercentFeeSender, to: noLimitOnePercentFeeRecipient, }, - ] + ], + feeCollectorAddress ); describe(NCGTransferredEventObserver.prototype.notify.name, () => { @@ -355,7 +357,7 @@ describe(NCGTransferredEventObserver.name, () => { blockHash: "BLOCK-HASH", events: [ { - amount: "100.11", + amount: "100.23", memo: "0xa2D738C3442609d92F1C62BDF051D0385F644b8E", blockHash: "BLOCK-HASH", txId: "TX-ID", @@ -377,13 +379,15 @@ describe(NCGTransferredEventObserver.name, () => { expect(mockExchangeHistoryStore.put).toHaveBeenCalledTimes(1); expect(mockExchangeHistoryStore.put).toHaveBeenNthCalledWith(1, { - amount: 100.11, + amount: 100.23, network: "nineChronicles", recipient: "0xa2D738C3442609d92F1C62BDF051D0385F644b8E", sender: "0x2734048eC2892d111b4fbAB224400847544FC872", timestamp: expect.any(String), tx_id: "TX-ID", }); + + expect(mockNcgTransfer.transfer).not.toHaveBeenCalled(); }); it("should post slack message every events", async () => { @@ -688,7 +692,7 @@ describe(NCGTransferredEventObserver.name, () => { }); expect(mockExchangeHistoryStore.put).toHaveBeenNthCalledWith(8, { - amount: 32500, // 50000 - ( 500 + 5000 + 12000 ) + amount: 82500, network: "nineChronicles", recipient: wrappedNcgRecipient, sender: sender, @@ -770,9 +774,9 @@ describe(NCGTransferredEventObserver.name, () => { // accumulated 5500, transfer amount 12000 // -> base 1% for 4500, base + surcharge 3% for 7500 -> fee 270 -> 11730 should be sent [wrappedNcgRecipient, new Decimal(11730000000000000000000)], - // accumulated 17500, transfer amount 32500 - // -> base + surcharge 3% for 32500 -> fee 975 -> 31525 should be sent - [wrappedNcgRecipient, new Decimal(31525000000000000000000)], + // accumulated 17500, transfer amount 82500 + // -> base + surcharge 3% for 82500 -> fee 2475 -> 80025 should be sent + [wrappedNcgRecipient, new Decimal(80025000000000000000000)], // accumulated 0, transfer amount 11000 // -> base 1% for 10000, base + surcharge 3% for 1000 -> fee 130 -> 10870 should be sent [ @@ -788,6 +792,97 @@ describe(NCGTransferredEventObserver.name, () => { new Decimal(10890000000000000000000), ], ]); + + // applied fixed fee ( 10 NCG for transfer under 1000 NCG ) + expect(mockNcgTransfer.transfer.mock.calls).toEqual([ + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "1", + "I'm bridge and you should transfer more NCG than 100.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "1.2", + "I'm bridge and you should transfer more NCG than 100.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "0.01", + "I'm bridge and you should transfer more NCG than 100.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "3.22", + "I'm bridge and you should transfer more NCG than 100.", + ], + // accumulated 0, transfer amount 500 + // -> base fee 10 -> 490 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "10", + "I'm bridge and the fee is sent to fee collector.", + ], + // accumulated 500, transfer amount 5000 + // -> base 1%, fee 50 -> 4950 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "50", + "I'm bridge and the fee is sent to fee collector.", + ], + // accumulated 5500, transfer amount 12000 + // -> base 1% for 4500, base + surcharge 3% for 7500 -> fee 270 -> 11730 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "270", + "I'm bridge and the fee is sent to fee collector.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "9999917500", + "I'm bridge and you should transfer less NCG than 100000.", + ], + // accumulated 17500, transfer amount 32500 + // -> base + surcharge 3% for 82500 -> fee 2475 -> 80025 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "2475", + "I'm bridge and the fee is sent to fee collector.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "99", + "I'm bridge and you should transfer more NCG than 100.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "100.01", + "I'm bridge and you can exchange until 100000 for 24 hours.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "100000", + "I'm bridge and you can exchange until 100000 for 24 hours.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "99999.99", + "I'm bridge and you can exchange until 100000 for 24 hours.", + ], + // accumulated 0, transfer amount 11000 + // -> base 1% for 10000, base + surcharge 3% for 1000 -> fee 130 -> 10870 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "130", + "I'm bridge and the fee is sent to fee collector.", + ], + // accumulated 0, transfer amount 11000 + // -> static 1% for all amount -> fee 110 -> 10890 should be sent + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "110", + "I'm bridge and the fee is sent to fee collector.", + ], + ]); }); it("If overflowed amount is lower than minimum, then refund check", async () => { @@ -835,7 +930,7 @@ describe(NCGTransferredEventObserver.name, () => { } const events = [ - makeEvent(wrappedNcgRecipient, "49950", "TX-A"), + makeEvent(wrappedNcgRecipient, "99950", "TX-A"), makeEvent(wrappedNcgRecipient, "150", "TX-SHOULD-REFUND"), ]; @@ -863,7 +958,7 @@ describe(NCGTransferredEventObserver.name, () => { ); expect(mockExchangeHistoryStore.put).toHaveBeenNthCalledWith(1, { - amount: 49950, + amount: 99950, network: "nineChronicles", recipient: wrappedNcgRecipient, sender: sender, @@ -882,7 +977,25 @@ describe(NCGTransferredEventObserver.name, () => { // applied fixed fee ( 10 NCG for transfer under 1000 NCG ) expect(mockWrappedNcgMinter.mint.mock.calls).toEqual([ - [wrappedNcgRecipient, new Decimal(48651500000000000000000)], + [wrappedNcgRecipient, new Decimal(97151500000000000000000)], + ]); + + expect(mockNcgTransfer.transfer.mock.calls).toEqual([ + [ + "0x5aFDEB6f53C5F9BAf2ff1E9932540Cd6dc45F07e", + "2798.5", + "I'm bridge and the fee is sent to fee collector.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "100", + "I'm bridge and you should transfer less NCG than 100000.", + ], + [ + "0x2734048eC2892d111b4fbAB224400847544FC872", + "50", + "I'm bridge and you should transfer more NCG than 100.", + ], ]); expect(