diff --git a/system-tests/test/deposit-redemption.test.ts b/system-tests/test/deposit-redemption.test.ts index f72a13894..f40c0c6d0 100644 --- a/system-tests/test/deposit-redemption.test.ts +++ b/system-tests/test/deposit-redemption.test.ts @@ -21,6 +21,7 @@ import type { UnspentTransactionOutput } from "@keep-network/tbtc-v2.ts/dist/src import type { SystemTestsContext } from "./utils/context" import type { RedemptionRequest } from "@keep-network/tbtc-v2.ts/dist/src/redemption" import type { Deposit } from "@keep-network/tbtc-v2.ts/dist/src/deposit" +import { TBTCToken } from "@keep-network/tbtc-v2.ts/dist/src/ethereum" chai.use(chaiAsPromised) @@ -46,11 +47,15 @@ chai.use(chaiAsPromised) describe("System Test - Deposit and redemption", () => { let systemTestsContext: SystemTestsContext let electrumClient: ElectrumClient + let tbtcTokenAddress: string let bridgeAddress: string + let vaultAddress: string + let tbtcTokenHandle: TBTCToken let maintainerBridgeHandle: EthereumBridge let depositorBridgeHandle: EthereumBridge let bank: Contract let relay: Contract + let tbtc: Contract const depositAmount = BigNumber.from(2000000) const depositSweepTxFee = BigNumber.from(10000) @@ -59,10 +64,13 @@ describe("System Test - Deposit and redemption", () => { // Initial backoff step in milliseconds that will be increased exponentially for // subsequent Electrum retry attempts. const ELECTRUM_RETRY_BACKOFF_STEP_MS = 10000 // 10sec + // Multiplier to convert satoshi to TBTC token units. + const SATOSHI_MULTIPLIER = BigNumber.from(10000000000) let deposit: Deposit let depositUtxo: UnspentTransactionOutput let sweepUtxo: UnspentTransactionOutput + let redemptionUtxo: UnspentTransactionOutput | undefined before(async () => { systemTestsContext = await setupSystemTestsContext() @@ -76,7 +84,14 @@ describe("System Test - Deposit and redemption", () => { ELECTRUM_RETRY_BACKOFF_STEP_MS ) + tbtcTokenAddress = deployedContracts.TBTC.address bridgeAddress = deployedContracts.Bridge.address + vaultAddress = deployedContracts.TBTCVault.address + + tbtcTokenHandle = new TBTCToken({ + address: tbtcTokenAddress, + signerOrProvider: depositor, + }) maintainerBridgeHandle = new EthereumBridge({ address: bridgeAddress, @@ -101,9 +116,16 @@ describe("System Test - Deposit and redemption", () => { relayDeploymentInfo.abi, maintainer ) + + const tbtcDeploymentInfo = deployedContracts.TBTC + tbtc = new Contract( + tbtcDeploymentInfo.address, + tbtcDeploymentInfo.abi, + maintainer + ) }) - context("when deposit is made and revealed", () => { + context("when deposit is made and revealed without a vault", () => { before("make and reveal deposit", async () => { deposit = generateDeposit( systemTestsContext.depositor.address, @@ -299,6 +321,302 @@ describe("System Test - Deposit and redemption", () => { ) }) + context( + "when redemption is made and redemption proof submitted", + () => { + let redemptionTxHash: BitcoinTransactionHash + + before( + "make the redemption and submit redemption proof", + async () => { + ;({ transactionHash: redemptionTxHash, newMainUtxo: redemptionUtxo } = + await submitRedemptionTransaction( + electrumClient, + maintainerBridgeHandle, + systemTestsContext.walletBitcoinKeyPair.wif, + sweepUtxo, + [redemptionRequest.redeemerOutputScript], + true + )) + + console.log( + "Redemption made on Bitcoin chain:\n" + + `- Transaction hash: ${redemptionTxHash}` + ) + + await waitTransactionConfirmed(electrumClient, redemptionTxHash) + + await fakeRelayDifficulty( + relay, + electrumClient, + redemptionTxHash + ) + + await SpvMaintainer.submitRedemptionProof( + redemptionTxHash, + sweepUtxo, + systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, + maintainerBridgeHandle, + electrumClient + ) + + console.log("Redemption proved on the bridge") + } + ) + + it("should broadcast the redemption transaction on the Bitcoin network", async () => { + expect( + (await electrumClient.getRawTransaction(redemptionTxHash)) + .transactionHex.length + ).to.be.greaterThan(0) + }) + + it("should close the redemption request on the bridge", async () => { + await expect( + TBTC.getRedemptionRequest( + systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, + redemptionRequest.redeemerOutputScript, + "pending", + maintainerBridgeHandle + ) + ).to.be.rejectedWith( + "Provided redeemer output script and wallet public key do not identify a redemption request" + ) + }) + + it("should decrease Bridge's balance in the bank", async () => { + const actualBalance = await bank.balanceOf(bridgeAddress) + + expect(actualBalance).to.be.equal(0) + }) + } + ) + }) + }) + }) + + context("when deposit is made and revealed with a vault", () => { + before("make and reveal deposit", async () => { + deposit = generateDeposit( + systemTestsContext.depositor.address, + depositAmount, + systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, + vaultAddress, + ) + + console.log(` + Generated deposit data: + ${JSON.stringify(deposit)} + `) + ;({ depositUtxo } = await submitDepositTransaction( + deposit, + systemTestsContext.depositorBitcoinKeyPair.wif, + electrumClient, + true + )) + + console.log(` + Deposit made on BTC chain: + - Transaction hash: ${depositUtxo.transactionHash} + - Output index: ${depositUtxo.outputIndex} + `) + + // Since the reveal deposit logic does not perform SPV proof, we + // can reveal the deposit transaction immediately without waiting + // for confirmations. + await TBTC.revealDeposit( + depositUtxo, + deposit, + electrumClient, + depositorBridgeHandle, + deposit.vault, + ) + + console.log(` + Deposit revealed on Ethereum chain + `) + }) + + it("should broadcast the deposit transaction on the Bitcoin network", async () => { + expect( + (await electrumClient.getRawTransaction(depositUtxo.transactionHash)) + .transactionHex.length + ).to.be.greaterThan(0) + }) + + it("should reveal the deposit to the bridge", async () => { + const { revealedAt } = await TBTC.getRevealedDeposit( + depositUtxo, + maintainerBridgeHandle + ) + expect(revealedAt).to.be.greaterThan(0) + }) + + context("when deposit is swept and sweep proof submitted", () => { + before("sweep the deposit and submit sweep proof", async () => { + ;({ newMainUtxo: sweepUtxo } = await submitDepositSweepTransaction( + electrumClient, + depositSweepTxFee, + systemTestsContext.walletBitcoinKeyPair.wif, + true, + [depositUtxo], + [deposit], + redemptionUtxo // The UTXO from the previous test became the new main UTXO. + )) + + console.log(` + Deposit swept on Bitcoin chain: + - Transaction hash: ${sweepUtxo.transactionHash} + `) + + // Unlike in the deposit transaction case, we must wait for the sweep + // transaction to have an enough number of confirmations. This is + // because the bridge performs the SPV proof of that transaction. + await waitTransactionConfirmed( + electrumClient, + sweepUtxo.transactionHash + ) + + await fakeRelayDifficulty( + relay, + electrumClient, + sweepUtxo.transactionHash + ) + + // If the redemption transaction from the previous test created a new + // main UTXO, use it. Otherwise call it with a zero-filled main UTXO. + const mainUtxo = redemptionUtxo ? redemptionUtxo : { + transactionHash: BitcoinTransactionHash.from(constants.HashZero), + outputIndex: 0, + value: BigNumber.from(0), + }; + await SpvMaintainer.submitDepositSweepProof( + sweepUtxo.transactionHash, + mainUtxo, + maintainerBridgeHandle, + electrumClient, + deposit.vault, + ) + + console.log(` + Deposit sweep proved on the bridge + `) + }) + + it("should broadcast the sweep transaction on the Bitcoin network", async () => { + expect( + (await electrumClient.getRawTransaction(sweepUtxo.transactionHash)) + .transactionHex.length + ).to.be.greaterThan(0) + }) + + it("should sweep the deposit on the bridge", async () => { + const { sweptAt } = await TBTC.getRevealedDeposit( + depositUtxo, + maintainerBridgeHandle + ) + expect(sweptAt).to.be.greaterThan(0) + }) + + it("should increase vault's balance in the bank", async () => { + const { treasuryFee } = await TBTC.getRevealedDeposit( + depositUtxo, + maintainerBridgeHandle + ) + + const expectedBalance = depositAmount + .sub(treasuryFee) + .sub(depositSweepTxFee) + + const actualBalance = await bank.balanceOf( + vaultAddress + ) + + expect(actualBalance).to.be.equal(expectedBalance) + }) + + it("should mint TBTC tokens for the depositor", async () => { + const { treasuryFee } = await TBTC.getRevealedDeposit( + depositUtxo, + maintainerBridgeHandle + ) + + const balanceInSatoshis = depositAmount + .sub(treasuryFee) + .sub(depositSweepTxFee) + + const expectedTbtcBalance = balanceInSatoshis.mul(SATOSHI_MULTIPLIER) + + const actualBalance = await tbtc.balanceOf( + systemTestsContext.depositor.address + ) + + expect(actualBalance).to.be.equal(expectedTbtcBalance) + }) + + context("when redemption is requested", () => { + let requestedAmount: BigNumber + let redeemerOutputScript: string + let redemptionRequest: RedemptionRequest + + before("request the redemption", async () => { + // Redeem all of the depositor's TBTC tokens. + const tbtcBalanceOfDepositor = await tbtc.balanceOf( + systemTestsContext.depositor.address + ) + + // The depositor's balance converted to satoshis. + requestedAmount = tbtcBalanceOfDepositor.div(SATOSHI_MULTIPLIER) + + // Request redemption to depositor's address. + redeemerOutputScript = `0014${computeHash160( + systemTestsContext.depositorBitcoinKeyPair.publicKey.compressed + )}` + + await TBTC.requestRedemption( + systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, + sweepUtxo, + redeemerOutputScript, + tbtcBalanceOfDepositor, + tbtcTokenHandle, + ) + + console.log( + `Requested redemption of ${tbtcBalanceOfDepositor} TBTC tokens to script ${redeemerOutputScript} on the bridge` + ) + + redemptionRequest = await TBTC.getRedemptionRequest( + systemTestsContext.walletBitcoinKeyPair.publicKey.compressed, + redeemerOutputScript, + "pending", + maintainerBridgeHandle + ) + }) + + it("should unmint depositor's TBTC tokens", async () => { + const tbtcBalance = await tbtc.balanceOf( + systemTestsContext.depositor.address + ) + + expect(tbtcBalance).to.be.equal(0) + }) + + it("should transfer vault's bank balance to the Bridge", async () => { + expect(await bank.balanceOf(vaultAddress)).to.be.equal(0) + + expect(await bank.balanceOf(bridgeAddress)).to.be.equal( + requestedAmount + ) + }) + + it("should register the redemption request on the bridge", async () => { + expect(redemptionRequest.requestedAt).to.be.greaterThan(0) + expect(redemptionRequest.requestedAmount).to.be.equal(requestedAmount) + expect(redemptionRequest.redeemerOutputScript).to.be.equal( + redeemerOutputScript + ) + }) + context( "when redemption is made and redemption proof submitted", () => {