diff --git a/.github/workflows/slither.yml b/.github/workflows/slither.yml index 957a426..098d088 100644 --- a/.github/workflows/slither.yml +++ b/.github/workflows/slither.yml @@ -13,13 +13,15 @@ jobs: steps: - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - - name: Use Node.js 19.6.0 + - name: Use Node.js 20.15.1 uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: - node-version: "19.6.0" + node-version: "20.15.1" - name: NPM Login - run: npm config set //npm.pkg.github.com/:_authToken ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm config set //npm.pkg.github.com/:_authToken $GITHUB_TOKEN - name: Install dependencies run: npm ci diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd0dda9..3023cd6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - repo: local hooks: - id: code-style - name: Check contact code style + name: Check contract code style entry: npm run lint:sol language: system types: [file] diff --git a/scripts/deployment-utils/deploy-proxy.ts b/scripts/deployment-utils/deploy-proxy.ts index 3e77ddc..4aac246 100644 --- a/scripts/deployment-utils/deploy-proxy.ts +++ b/scripts/deployment-utils/deploy-proxy.ts @@ -20,7 +20,7 @@ interface LiquidityBridgeContractLibraries { bridge: string; } -const BRIDGE_ADDRESS = "0x0000000000000000000000000000000001000006"; +export const BRIDGE_ADDRESS = "0x0000000000000000000000000000000001000006"; async function deployProxyLibraries( network: string diff --git a/test/deployment.test.ts b/test/deployment.test.ts index 1417b75..14966f5 100644 --- a/test/deployment.test.ts +++ b/test/deployment.test.ts @@ -1,8 +1,12 @@ import { expect } from "chai"; import hre from "hardhat"; import { ethers } from "hardhat"; -import { deployLbcProxy } from "../scripts/deployment-utils/deploy-proxy"; +import { + BRIDGE_ADDRESS, + deployLbcProxy, +} from "../scripts/deployment-utils/deploy-proxy"; import { upgradeLbcProxy } from "../scripts/deployment-utils/upgrade-proxy"; +import { ZERO_ADDRESS } from "./utils/constants"; describe("LiquidityBridgeContract deployment process should", function () { let proxyAddress: string; @@ -28,4 +32,121 @@ describe("LiquidityBridgeContract deployment process should", function () { const version = await lbc.version(); expect(version).to.equal("1.3.0"); }); + + it("validate minimiumCollateral arg in initialize", async () => { + const LiquidityBridgeContract = await ethers.getContractFactory( + "LiquidityBridgeContract", + { + libraries: { + BtcUtils: ZERO_ADDRESS, + SignatureValidator: ZERO_ADDRESS, + Quotes: ZERO_ADDRESS, + }, + } + ); + const lbc = await LiquidityBridgeContract.deploy(); + const MINIMUM_COLLATERAL = ethers.parseEther("0.02"); + const RESIGN_DELAY_BLOCKS = 15; + const initializeTx = lbc.initialize( + BRIDGE_ADDRESS, + MINIMUM_COLLATERAL, + 1, + 50, + RESIGN_DELAY_BLOCKS, + 1, + 1, + false + ); + await expect(initializeTx).to.be.revertedWith("LBC072"); + }); + + it("validate resignDelayBlocks arg in initialize", async () => { + const LiquidityBridgeContract = await ethers.getContractFactory( + "LiquidityBridgeContract", + { + libraries: { + BtcUtils: ZERO_ADDRESS, + SignatureValidator: ZERO_ADDRESS, + Quotes: ZERO_ADDRESS, + }, + } + ); + const lbc = await LiquidityBridgeContract.deploy(); + const MINIMUM_COLLATERAL = ethers.parseEther("0.6"); + const RESIGN_DELAY_BLOCKS = 14; + const initializeTx = lbc.initialize( + BRIDGE_ADDRESS, + MINIMUM_COLLATERAL, + 1, + 50, + RESIGN_DELAY_BLOCKS, + 1, + 1, + false + ); + await expect(initializeTx).to.be.revertedWith("LBC073"); + }); + + it("validate reward percentage arg in initialize", async () => { + const LiquidityBridgeContract = await ethers.getContractFactory( + "LiquidityBridgeContract", + { + libraries: { + BtcUtils: ZERO_ADDRESS, + SignatureValidator: ZERO_ADDRESS, + Quotes: ZERO_ADDRESS, + }, + } + ); + const MINIMUM_COLLATERAL = ethers.parseEther("0.6"); + const RESIGN_DELAY_BLOCKS = 60; + + const parameters = { + bridge: BRIDGE_ADDRESS, + minCollateral: MINIMUM_COLLATERAL, + minPegin: 1, + resignBlocks: RESIGN_DELAY_BLOCKS, + dustThreshold: 1, + btcBlockTime: 1, + mainnet: false, + }; + const percentages = [ + { value: 0, ok: true }, + { value: 1, ok: true }, + { value: 99, ok: true }, + { value: 100, ok: true }, + { value: 101, ok: false }, + ]; + + for (const { value, ok } of percentages) { + const lbc = await LiquidityBridgeContract.deploy(); + const initializeTx = lbc.initialize( + parameters.bridge, + parameters.minCollateral, + parameters.minPegin, + value, + parameters.resignBlocks, + parameters.dustThreshold, + parameters.btcBlockTime, + parameters.mainnet + ); + if (ok) { + await expect(initializeTx).not.to.be.reverted; + const reinitializeTx = lbc.initialize( + parameters.bridge, + parameters.minCollateral, + parameters.minPegin, + value, + parameters.resignBlocks, + parameters.dustThreshold, + parameters.btcBlockTime, + parameters.mainnet + ); + // check that only initializes once + await expect(reinitializeTx).to.be.reverted; + } else { + await expect(initializeTx).to.be.revertedWith("LBC004"); + } + } + }); }); diff --git a/test/pegin.test.ts b/test/pegin.test.ts index 637b20b..a18a6ad 100644 --- a/test/pegin.test.ts +++ b/test/pegin.test.ts @@ -1,5 +1,10 @@ import hre, { ethers } from "hardhat"; -import { anyHex, anyNumber, ZERO_ADDRESS } from "./utils/constants"; +import { + anyHex, + anyNumber, + LP_COLLATERAL, + ZERO_ADDRESS, +} from "./utils/constants"; import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { expect } from "chai"; import { deployContract } from "../scripts/deployment-utils/utils"; @@ -17,6 +22,8 @@ import { import * as bs58check from "bs58check"; import bs58 from "bs58"; import { QuotesV2 } from "../typechain-types"; +import { getTestMerkleProof } from "./utils/btc"; +import * as hardhatHelpers from "@nomicfoundation/hardhat-network-helpers"; describe("LiquidityBridgeContractV2 pegin process should", () => { it("call contract for user", async () => { @@ -540,7 +547,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("6000000000000000"), gasFee: BigInt("3000000000000000"), }, - address: "2NF8GF7whNAJY28RfHr8vhPMpciNDXphruv", + address: "2Mvz5NDrXSSBCXzhrxnuo9TDS8Co4Wk1t8N", }, { quote: { @@ -572,7 +579,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("7000000000000000"), gasFee: BigInt("4000000000000000"), }, - address: "2NAxWxN1WaX3rgota4G6Yy8PKxwkv6PRw5R", + address: "2Mxb4NdDaDBX4kjxhBrzjMG5WbCjok6LWab", }, { quote: { @@ -604,7 +611,7 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { productFeeAmount: BigInt("8000000000000000"), gasFee: BigInt("5000000000000000"), }, - address: "2NC5eXFxoQqbjVwdFL8LuP3Rrcy2SA6SS7j", + address: "2NCx9M8j7nZTp3GmP36CFvFgYNLBQwQPwDR", }, ]; @@ -779,4 +786,978 @@ describe("LiquidityBridgeContractV2 pegin process should", () => { .withArgs(quoteHash, modifiedRefundAmount); } }); + + it("transfer value and refund remaining", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + let lbc = fixtureResult.lbc; + const provider = liquidityProviders[1]; + lbc = lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const rskRefundAddress = accounts[2].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: rskRefundAddress, + value: ethers.parseEther("10"), + }); + + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const additionalFunds = 1000000000000n; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + + await bridgeMock.setPegin(quoteHash, { + value: peginAmount + additionalFunds, + }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + + const destinationBalanceDiffAsserttion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: destinationAddress, + expectedDiff: quote.value, + message: "Incorrect destination balance after pegin", + }); + const lbcBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await lbc.getAddress(), + expectedDiff: peginAmount - BigInt(quote.productFeeAmount), + message: "Incorrect LBC balance after pegin", + }); + const lpBalanceDiffAfterCfuAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect LP balance after call for user", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: rskRefundAddress, + expectedDiff: additionalFunds, + message: "Incorrect refund address balance after refund", + }); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + + const cfuTx = await lbc.callForUser(quote, { value: quote.value }); + await cfuTx.wait(); + + await lpBalanceDiffAfterCfuAssertion(); + const lpBalanceDiffAfterRegisterAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: peginAmount - BigInt(quote.productFeeAmount), + message: "Incorrect LP balance after register pegin", + }); + + const registerPeginResult = await lbc.registerPegIn.staticCall( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + const registerPeginTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + + await expect(registerPeginTx) + .to.emit(lbc, "PegInRegistered") + .withArgs(quoteHash, peginAmount + additionalFunds); + await expect(registerPeginTx) + .to.emit(lbc, "Refund") + .withArgs(rskRefundAddress, additionalFunds, true, quoteHash); + expect(registerPeginResult).to.be.eq(peginAmount + additionalFunds); + await destinationBalanceDiffAsserttion(); + await lbcBalanceDiffAssertion(); + await lpBalanceDiffAfterRegisterAssertion(); + await refundBalanceDiffAssertion(); + await collateralAssertion(); + }); + + it("refund remaining amount to LP in case refunding to quote.rskRefundAddress fails", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const deploymentInfo = await deployContract("WalletMock", hre.network.name); + const walletMock = await ethers.getContractAt( + "WalletMock", + deploymentInfo.address + ); + const walletMockAddress = await walletMock.getAddress(); + await walletMock.setRejectFunds(true).then((tx) => tx.wait()); + let lbc = fixtureResult.lbc; + const provider = liquidityProviders[0]; + lbc = lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const rskRefundAddress = walletMockAddress; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: rskRefundAddress, + value: ethers.parseEther("10"), + }); + + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const additionalFunds = 1000000000000n; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + + await bridgeMock.setPegin(quoteHash, { + value: peginAmount + additionalFunds, + }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + + const destinationBalanceDiffAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: destinationAddress, + expectedDiff: quote.value, + message: "Incorrect destination balance after pegin", + }); + const lbcBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await lbc.getAddress(), + expectedDiff: + peginAmount - BigInt(quote.productFeeAmount) + additionalFunds, + message: "Incorrect LBC balance after pegin", + }); + const lpBalanceDiffAfterCfuAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect LP balance after call for user", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: rskRefundAddress, + expectedDiff: 0, + message: "Incorrect refund address balance after refund", + }); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + + const cfuTx = await lbc.callForUser(quote, { value: quote.value }); + await cfuTx.wait(); + await lpBalanceDiffAfterCfuAssertion(); + + const lpBalanceDiffAfterRegisterAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: + peginAmount - BigInt(quote.productFeeAmount) + additionalFunds, + message: "Incorrect LP balance after register pegin", + }); + + const registerPeginResult = await lbc.registerPegIn.staticCall( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + + const registerPeginTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerPeginTx) + .to.emit(lbc, "PegInRegistered") + .withArgs(quoteHash, peginAmount + additionalFunds); + await expect(registerPeginTx) + .to.emit(lbc, "Refund") + .withArgs(walletMockAddress, additionalFunds, false, quoteHash); + await expect(registerPeginTx) + .to.emit(lbc, "BalanceIncrease") + .withArgs(provider.signer.address, additionalFunds); + expect(registerPeginResult).to.be.eq(peginAmount + additionalFunds); + await destinationBalanceDiffAssertion(); + await lbcBalanceDiffAssertion(); + await lpBalanceDiffAfterRegisterAssertion(); + await refundBalanceDiffAssertion(); + await collateralAssertion(); + }); + + it("refund user on failed call", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const rskRefundAddress = accounts[2].address; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const deploymentInfo = await deployContract("Mock", hre.network.name); + const mockContract = await ethers.getContractAt( + "Mock", + deploymentInfo.address + ); + const mockAddress = await mockContract.getAddress(); + const data = mockContract.interface.encodeFunctionData("fail"); + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: mockAddress, + refundAddress: rskRefundAddress, + value: ethers.parseEther("10"), + data: data, + }); + + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + + const destinationBalanceDiffAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: mockAddress, + expectedDiff: 0, + message: "Incorrect refund balance after pegin", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: rskRefundAddress, + expectedDiff: quote.value, + message: "Incorrect refund balance after pegin", + }); + const lpBalanceDiffAfterCfuAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: quote.value, + message: "Incorrect LP balance after call for user", + }); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + + await lbc + .callForUser(quote, { value: quote.value }) + .then((tx) => tx.wait()); + await lpBalanceDiffAfterCfuAssertion(); + + const lpBalanceDiffAfterRegisterAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: BigInt(quote.callFee) + BigInt(quote.gasFee), + message: "Incorrect LP balance after register pegin", + }); + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "Refund") + .withArgs(rskRefundAddress, quote.value, true, quoteHash); + await lpBalanceDiffAfterRegisterAssertion(); + await refundBalanceDiffAssertion(); + await collateralAssertion(); + await destinationBalanceDiffAssertion(); + }); + + it("refund user on missed call", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const rskRefundAddress = accounts[2].address; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: accounts[1].address, + refundAddress: rskRefundAddress, + value: ethers.parseEther("10"), + }); + + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + const rewardPercentage = await lbc.getRewardPercentage(); + const reward = (BigInt(quote.penaltyFee) * rewardPercentage) / 100n; + + const destinationBalanceDiffAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: accounts[1].address, + expectedDiff: 0, + message: "Incorrect refund balance after pegin", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: rskRefundAddress, + expectedDiff: + BigInt(quote.value) + BigInt(quote.callFee) + BigInt(quote.gasFee), + message: "Incorrect refund balance after pegin", + }); + const lpBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: reward, + message: "Incorrect LP balance after call for user", + }); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: BigInt(quote.penaltyFee) * -1n, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const lbcBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await lbc.getAddress(), + expectedDiff: 0, + message: "Incorrect LBC balance after pegin", + }); + + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "Refund") + .withArgs(rskRefundAddress, peginAmount, true, quoteHash); + await destinationBalanceDiffAssertion(); + await refundBalanceDiffAssertion(); + await lpBalanceDiffAssertion(); + await collateralAssertion(); + await lbcBalanceDiffAssertion(); + }); + + it("no one be refunded in registerPegIn on missed call in case refunding to quote.rskRefundAddress fails", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const deploymentInfo = await deployContract("WalletMock", hre.network.name); + const walletMock = await ethers.getContractAt( + "WalletMock", + deploymentInfo.address + ); + const walletMockAddress = await walletMock.getAddress(); + await walletMock.setRejectFunds(true).then((tx) => tx.wait()); + const destinationAddress = accounts[1].address; + const registerCaller = accounts[2]; + const lbc = fixtureResult.lbc.connect(registerCaller); + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: walletMockAddress, + value: ethers.parseEther("10"), + }); + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + const rewardPercentage = await lbc.getRewardPercentage(); + const reward = (BigInt(quote.penaltyFee) * rewardPercentage) / 100n; + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(10, firstConfirmationHeader); + await bridgeMock.setHeader(11, nConfirmationHeader); + + const registerCallerBalanceDiffAssertion = + await createBalanceDifferenceAssertion({ + source: lbc, + expectedDiff: reward, + address: registerCaller.address, + message: "Incorrect refund balance after pegin", + }); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: BigInt(quote.penaltyFee) * -1n, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: walletMockAddress, + expectedDiff: 0, + message: "Incorrect refund balance after pegin", + }); + const lbcBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await lbc.getAddress(), + expectedDiff: peginAmount - BigInt(quote.productFeeAmount), + message: "Incorrect LBC balance after pegin", + }); + + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "Refund") + .withArgs( + walletMockAddress, + peginAmount - BigInt(quote.productFeeAmount), + false, + quoteHash + ); + await collateralAssertion(); + await refundBalanceDiffAssertion(); + await lbcBalanceDiffAssertion(); + await registerCallerBalanceDiffAssertion(); + }); + + it("not penalize with late deposit", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const refundAddress = accounts[2].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: accounts[1].address, + refundAddress: refundAddress, + value: ethers.parseEther("10"), + }); + quote.timeForDeposit = 1; + + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const peginAmount = totalValue(quote); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: refundAddress, + expectedDiff: peginAmount, + message: "Incorrect destination balance after pegin", + }); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "PegInRegistered") + .withArgs(quoteHash, peginAmount); + await expect(registerTx).not.to.emit(lbc, "Penalized"); + await collateralAssertion(); + await refundBalanceDiffAssertion(); + }); + + it("not penalize with insufficient deposit", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const refundAddress = accounts[2].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: accounts[1].address, + refundAddress: refundAddress, + value: ethers.parseEther("10"), + }); + + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const peginAmount = totalValue(quote); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const insufficientDeposit = peginAmount - 1n; + await bridgeMock.setPegin(quoteHash, { value: insufficientDeposit }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const refundBalanceDiffAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: refundAddress, + expectedDiff: insufficientDeposit, + message: "Incorrect destination balance after pegin", + }); + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "PegInRegistered") + .withArgs(quoteHash, insufficientDeposit); + await expect(registerTx).not.to.emit(lbc, "Penalized"); + await collateralAssertion(); + await refundBalanceDiffAssertion(); + }); + + it("should penalize on late call", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: accounts[2].address, + value: ethers.parseEther("10"), + }); + quote.callTime = 1; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 100, + nConfirmationSeconds: 200, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const rewardPercentage = await lbc.getRewardPercentage(); + const reward = (BigInt(quote.penaltyFee) * rewardPercentage) / 100n; + await hardhatHelpers.time.increase(300); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: BigInt(quote.penaltyFee) * -1n, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const destinationBalanceDiffAssertion = + await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: destinationAddress, + expectedDiff: quote.value, + message: "Incorrect destination balance after pegin", + }); + const lpBalanceAssertion = await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: reward + peginAmount, + message: "Incorrect LP balance after call for user", + }); + + await lbc + .callForUser(quote, { value: quote.value }) + .then((tx) => tx.wait()); + + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "Penalized") + .withArgs(provider.signer.address, quote.penaltyFee, quoteHash); + await collateralAssertion(); + await destinationBalanceDiffAssertion(); + await lpBalanceAssertion(); + }); + + it("not underflow when penalty is higher than collateral", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: accounts[2].address, + value: ethers.parseEther("10"), + }); + quote.penaltyFee = LP_COLLATERAL + 1n; + quote.callTime = 1; + const peginAmount = totalValue(quote); + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 100, + nConfirmationSeconds: 200, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + const rewardPercentage = await lbc.getRewardPercentage(); + const reward = (BigInt(quote.penaltyFee / 2n) * rewardPercentage) / 100n; + await hardhatHelpers.time.increase(300); + + const lpBalanceAssertion = await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: reward + peginAmount - BigInt(quote.productFeeAmount), + message: "Incorrect LP balance after pegin", + }); + const userBalanceAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: destinationAddress, + expectedDiff: quote.value, + message: "Incorrect destination balance after pegin", + }); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + await lbc + .callForUser(quote, { value: quote.value }) + .then((tx) => tx.wait()); + const registerTx = await lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx) + .to.emit(lbc, "Penalized") + .withArgs(provider.signer.address, LP_COLLATERAL / 2n, quoteHash); + await lpBalanceAssertion(); + await userBalanceAssertion(); + await expect( + lbc.getCollateral(provider.signer.address) + ).to.eventually.be.eq(0); + }); + + it("should not allow attacker to steal funds", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, accounts, bridgeMock } = fixtureResult; + let { lbc } = fixtureResult; + + // The attacker controls a liquidity provider and also a destination address + // Note that these could be the same address, separated for clarity + const attackingLP = liquidityProviders[0]; + const attackerDestAddress = accounts[9]; + + const goodLP = liquidityProviders[1]; + // Add funds from an innocent liquidity provider, note again this could be + // done by an attacker + lbc = lbc.connect(goodLP.signer); + await lbc.deposit({ value: ethers.parseEther("20") }); + + // The quote value in wei should be bigger than 2**63-1. 10 RBTC is a good approximation. + const quoteValue = ethers.parseEther("10"); + // Let's create the evil quote. + const quote: QuotesV2.PeginQuoteStruct = { + fedBtcAddress: "0x0000000000000000000000000000000000000000", + btcRefundAddress: "0x000000000000000000000000000000000000000000", + liquidityProviderBtcAddress: + "0x000000000000000000000000000000000000000000", + rskRefundAddress: attackerDestAddress, + liquidityProviderRskAddress: attackerDestAddress, + data: "0x", + gasLimit: 30000, + callFee: 1n, + nonce: 1, + lbcAddress: await lbc.getAddress(), + agreementTimestamp: 1661788988, + timeForDeposit: 600, + callTime: 600, + depositConfirmations: 10, + penaltyFee: 0n, + callOnRegister: true, + productFeeAmount: 1, + gasFee: 1n, + value: quoteValue, + contractAddress: attackerDestAddress, + }; + const btcRawTransaction = "0x0101"; + const partialMerkleTree = "0x0202"; + const height = 10; + // Let's now register our quote in the bridge... note that the + // value is only a hundred wei + const transferredInBTC = 100; + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await attackingLP.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 300, + nConfirmationSeconds: 600, + }); + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) - 1, + nConfirmationHeader + ); + await bridgeMock.setPegin(quoteHash, { value: transferredInBTC }); + lbc = lbc.connect(attackingLP.signer); + const registerTx = lbc.registerPegIn( + quote, + signature, + btcRawTransaction, + partialMerkleTree, + height + ); + await expect(registerTx).to.be.revertedWith("LBC057"); + }); + + it("pay with insufficient deposit that is not lower than (agreed amount - delta)", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: accounts[2].address, + value: ethers.parseEther("0.7"), + }); + quote.callFee = ethers.parseEther("0.00001"); + quote.gasFee = ethers.parseEther("0.00003"); + const delta = totalValue(quote) / 10000n; + const peginAmount = totalValue(quote) - delta; + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 100, + nConfirmationSeconds: 200, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) + 1, + nConfirmationHeader + ); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + + const collateralAssertion = await createCollateralUpdateAssertion({ + lbc: lbc, + address: provider.signer.address, + expectedDiff: 0, + message: "Incorrect collateral after pegin", + type: "pegin", + }); + const lpBalanceAssertion = await createBalanceDifferenceAssertion({ + source: lbc, + address: provider.signer.address, + expectedDiff: peginAmount, + message: "Incorrect LP balance after pegin", + }); + const lbcBalanceAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: await lbc.getAddress(), + expectedDiff: peginAmount, + message: "Incorrect LBC balance after pegin", + }); + const destinationBalanceAssertion = await createBalanceDifferenceAssertion({ + source: ethers.provider, + address: destinationAddress, + expectedDiff: quote.value, + message: "Incorrect destination balance after pegin", + }); + + await lbc + .callForUser(quote, { value: quote.value }) + .then((tx) => tx.wait()); + + const registerResult = await lbc.registerPegIn.staticCall( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await lbc + .registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ) + .then((tx) => tx.wait()); + expect(registerResult).to.be.eq(peginAmount); + await collateralAssertion(); + await lpBalanceAssertion(); + await lbcBalanceAssertion(); + await destinationBalanceAssertion(); + }); + + it("revert on insufficient deposit", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { liquidityProviders, bridgeMock, accounts } = fixtureResult; + const provider = liquidityProviders[0]; + const lbc = fixtureResult.lbc.connect(provider.signer); + const destinationAddress = accounts[1].address; + const quote = getTestPeginQuote({ + lbcAddress: await lbc.getAddress(), + liquidityProvider: provider.signer, + destinationAddress: destinationAddress, + refundAddress: accounts[2].address, + value: ethers.parseEther("0.7"), + }); + quote.callFee = ethers.parseEther("0.000005"); + quote.gasFee = ethers.parseEther("0.000006"); + const delta = totalValue(quote) / 10000n; + const peginAmount = totalValue(quote) - delta - 1n; + const quoteHash = await lbc.hashQuote(quote).then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + const { firstConfirmationHeader, nConfirmationHeader } = + getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: 100, + nConfirmationSeconds: 200, + }); + const { blockHeaderHash, partialMerkleTree } = getTestMerkleProof(); + const height = 10; + await bridgeMock.setHeader(height, firstConfirmationHeader); + await bridgeMock.setHeader( + height + Number(quote.depositConfirmations) + 1, + nConfirmationHeader + ); + await bridgeMock.setPegin(quoteHash, { value: peginAmount }); + const registerTx = lbc.registerPegIn( + quote, + signature, + blockHeaderHash, + partialMerkleTree, + height + ); + await expect(registerTx).to.be.revertedWith("LBC057"); + }); }); diff --git a/test/pegout.test.ts b/test/pegout.test.ts index bb5645c..72c4619 100644 --- a/test/pegout.test.ts +++ b/test/pegout.test.ts @@ -390,7 +390,7 @@ describe("LiquidityBridgeContractV2 pegout process should", () => { const { blockHeaderHash, partialMerkleTree, merkleBranchHashes } = getTestMerkleProof(); // so its expired after deposit - quote.expireDate = Math.round(Date.now() / 1000) + 35; + quote.expireDate = BigInt(quote.agreementTimestamp) + 300n; quote.expireBlock = await ethers.provider .getBlockNumber() .then((result) => result + 10); @@ -404,9 +404,9 @@ describe("LiquidityBridgeContractV2 pegout process should", () => { value: totalValue(quote), }); await expect(depositTx).to.emit(lbc, "PegOutDeposit"); - // increase 9 blocks and then 1 block that takes 50s to mine to force expiration + // increase 9 blocks and then 1 block that takes 500s to mine to force expiration await hardhatHelpers.mine(9); - await hardhatHelpers.time.increase(50); + await hardhatHelpers.time.increase(500); const refundUserTx = await lbc.refundUserPegOut(quoteHash); await expect(refundUserTx).to.emit(lbc, "PegOutUserRefunded"); @@ -1229,4 +1229,56 @@ describe("LiquidityBridgeContractV2 pegout process should", () => { } } }); + + it("penalize LP on pegout if the transfer was not made on time", async () => { + const fixtureResult = await loadFixture(deployLbcWithProvidersFixture); + const { accounts, liquidityProviders, bridgeMock } = fixtureResult; + let lbc = fixtureResult.lbc; + const user = accounts[3]; + const provider = liquidityProviders[0]; + const quote = getTestPegoutQuote({ + lbcAddress: await lbc.getAddress(), + value: ethers.parseEther("0.5"), + refundAddress: user.address, + liquidityProvider: provider.signer, + }); + const quoteHash = await lbc + .hashPegoutQuote(quote) + .then((hash) => getBytes(hash)); + const signature = await provider.signer.signMessage(quoteHash); + + lbc = lbc.connect(user); + const pegoutAmount = totalValue(quote); + const depositTx = await lbc.depositPegout(quote, signature, { + value: pegoutAmount, + }); + await depositTx.wait(); + await expect(depositTx).to.emit(lbc, "PegOutDeposit"); + const BTC_BLOCK_TIME = 5400; // 1.5h + const expirationTime = + Number(quote.agreementTimestamp) + + Number(quote.transferTime) + + BTC_BLOCK_TIME; + const { firstConfirmationHeader } = getBtcPaymentBlockHeaders({ + quote: quote, + firstConfirmationSeconds: expirationTime + 1, + nConfirmationSeconds: expirationTime + 600, + }); + const { blockHeaderHash, merkleBranchHashes, partialMerkleTree } = + getTestMerkleProof(); + await bridgeMock.setHeaderByHash(blockHeaderHash, firstConfirmationHeader); + const btcTx = await generateRawTx(lbc, quote); + lbc = lbc.connect(provider.signer); + const refundTx = await lbc.refundPegOut( + quoteHash, + btcTx, + blockHeaderHash, + partialMerkleTree, + merkleBranchHashes + ); + await expect(refundTx).to.emit(lbc, "PegOutRefunded"); + await expect(refundTx) + .to.emit(lbc, "Penalized") + .withArgs(provider.signer.address, quote.penaltyFee, quoteHash); + }); });