Skip to content

Commit

Permalink
Merge pull request #156 from rsksmart/add-pegout-request-from-contrac…
Browse files Browse the repository at this point in the history
…t-test

Adds 'should reject and not refund a pegout when a contract is trying…
  • Loading branch information
marcos-iov authored Nov 6, 2024
2 parents 1aecc22 + 253411e commit b65a362
Show file tree
Hide file tree
Showing 6 changed files with 466 additions and 2,794 deletions.
15 changes: 15 additions & 0 deletions contracts/CallReleaseBtcContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pragma solidity ^0.8.0;

interface BridgeInterface {
function releaseBtc() external payable;
}

contract CallReleaseBtcContract {

BridgeInterface public bridgeContract = BridgeInterface(0x0000000000000000000000000000000001000006);

function callBridgeReleaseBtc() external payable {
bridgeContract.releaseBtc{value:msg.value}();
}

}
37 changes: 37 additions & 0 deletions lib/contractDeployer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

const fs = require('fs');
const path = require('path');
const solUtils = require('./sol-utils');
const TEST_RELEASE_BTC_CONTRACT = '../contracts/CallReleaseBtcContract.sol';
const TEST_RELEASE_BTC_CONTRACT_NAME = 'CallReleaseBtcContract';
const SOLIDITY_COMPILER_VERSION = 'v0.8.26+commit.8a97fa7a';

/**
* Deploys the CallReleaseBtcContract contract.
* @param {RskTransactionHelper} rskTxHelper
* @param {string} from the funded rsk address from which the contract will be deployed.
* @returns {Promise<Contract>} the deployed contract.
*/
const deployCallReleaseBtcContract = async (rskTxHelper, from) => {

const fullPath = path.resolve(__dirname, TEST_RELEASE_BTC_CONTRACT);
const source = fs.readFileSync(fullPath).toString();

const callReleaseBtcContract = await solUtils.compileAndDeploy(
SOLIDITY_COMPILER_VERSION,
source,
TEST_RELEASE_BTC_CONTRACT_NAME,
[],
rskTxHelper,
{
from
}
);

return callReleaseBtcContract;

};

module.exports = {
deployCallReleaseBtcContract,
};
20 changes: 15 additions & 5 deletions lib/rsk-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ const {
FEE_PER_KB_CHANGER_PRIVATE_KEY,
FEE_PER_KB_CHANGER_ADDRESS,
FEE_PER_KB_RESPONSE_CODES,
DEFAULT_RSK_ADDRESS_FUNDING_IN_BTC,
} = require('./constants');
const BtcTransactionHelper = require('btc-transaction-helper/btc-transaction-helper');
const { ethToWeis } = require('@rsksmart/btc-eth-unit-converter');

const BTC_TO_RSK_MINIMUM_ACCEPTABLE_CONFIRMATIONS = 3;
const RSK_TO_BTC_MINIMUM_ACCEPTABLE_CONFIRMATIONS = 3;
Expand Down Expand Up @@ -337,13 +339,13 @@ const triggerRelease = async (rskTransactionHelpers, btcClient, callbacks = {})
* @param {RskTransactionHelper} rskTxHelper to make transactions to the rsk network
* @param {web3.eth.Contract.ContractSendMethod} method contract method to be invoked
* @param {string} from rsk address to send the transaction from
* @returns {web3.eth.TransactionReceipt} txReceipt
* @param {number} valueInWeis amount in weis to be sent with the transaction
* @param {number} gas to be used in the transaction. Defaults to 100000
* @returns {Promise<web3.eth.TransactionReceipt>} txReceipt
*/
const sendTransaction = async (rskTxHelper, method, from) => {

const estimatedGas = await method.estimateGas({ from });
const sendTransaction = async (rskTxHelper, method, from, valueInWeis = 0, gas = 100000) => {

const txReceiptPromise = method.send({ from, value: 0, gasPrice: 0, gas: estimatedGas });
const txReceiptPromise = method.send({ from, value: valueInWeis, gas });

await waitForRskMempoolToGetNewTxs(rskTxHelper);
await mineAndSync(getRskTransactionHelpers());
Expand Down Expand Up @@ -529,6 +531,13 @@ const setFeePerKb = async (rskTxHelper, feePerKbInSatoshis) => {

};

const getNewFundedRskAddress = async (rskTxHelper, fundingAmountInRbtc = DEFAULT_RSK_ADDRESS_FUNDING_IN_BTC) => {
const address = await rskTxHelper.getClient().eth.personal.newAccount('');
await sendFromCow(rskTxHelper, address, Number(ethToWeis(fundingAmountInRbtc)));
await rskTxHelper.getClient().eth.personal.unlockAccount(address, '');
return address;
};

module.exports = {
mineAndSync,
waitForBlock,
Expand All @@ -548,4 +557,5 @@ module.exports = {
sendTransaction,
getPegoutEventsInBlockRange,
setFeePerKb,
getNewFundedRskAddress,
};
133 changes: 132 additions & 1 deletion lib/tests/2wp.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const { getBridge } = require('../precompiled-abi-forks-util');
const { getBtcClient } = require('../btc-client-provider');
const { getRskTransactionHelper, getRskTransactionHelpers } = require('../rsk-tx-helper-provider');
const { satoshisToBtc, btcToSatoshis, satoshisToWeis } = require('@rsksmart/btc-eth-unit-converter');
const { findEventInBlock, triggerRelease, getPegoutEventsInBlockRange, setFeePerKb } = require('../rsk-utils');
const { findEventInBlock, triggerRelease, getPegoutEventsInBlockRange, setFeePerKb, sendTransaction, getNewFundedRskAddress } = require('../rsk-utils');
const BridgeTransactionParser = require('@rsksmart/bridge-transaction-parser');
const {
PEGIN_REJECTION_REASONS,
PEGIN_UNREFUNDABLE_REASONS,
Expand All @@ -30,6 +31,7 @@ const { getBtcAddressBalanceInSatoshis } = require('../btc-utils');
const { ensure0x, removePrefix0x } = require('../utils');
const { getBridgeState } = require('@rsksmart/bridge-state-data-parser');
const bitcoinJsLib = require('bitcoinjs-lib');
const { deployCallReleaseBtcContract } = require('../contractDeployer');

let btcTxHelper;
let rskTxHelpers;
Expand Down Expand Up @@ -819,6 +821,90 @@ const execute = (description, getRskHost) => {

});

it('should reject and not refund a pegout when a contract is trying to execute it', async () => {

// Arrange

const initial2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
const pegoutValueInSatoshis = MINIMUM_PEGOUT_AMOUNT_IN_SATOSHIS;

const creatorAddress = await getNewFundedRskAddress(rskTxHelper);
const callReleaseBtcContract = await deployCallReleaseBtcContract(rskTxHelper, creatorAddress);
const initialRskSenderBalanceInWeisBN = await rskTxHelper.getBalance(creatorAddress);
const initialContractBalanceInWeisBN = await rskTxHelper.getBalance(callReleaseBtcContract.options.address);

// Act

const callBridgeReleaseBtcMethod = callReleaseBtcContract.methods.callBridgeReleaseBtc();
const contractCallTxReceipt = await sendTransaction(rskTxHelper, callBridgeReleaseBtcMethod, creatorAddress, satoshisToWeis(pegoutValueInSatoshis));

// Assert

const contractAddressChecksummed = rskTxHelper.getClient().utils.toChecksumAddress(ensure0x(callReleaseBtcContract.options.address));
const expectedEvent = createExpectedReleaseRequestRejectedEvent(contractAddressChecksummed, pegoutValueInSatoshis, PEGOUT_REJECTION_REASONS.CALLER_CONTRACT);
const actualReleaseRequestRejectedEvent = getReleaseRequestRejectedEventFromContractCallTxReceipt(contractCallTxReceipt);
expect(actualReleaseRequestRejectedEvent).to.be.deep.equal(expectedEvent);

await assert2wpBalancesAfterPegoutFromContract(initial2wpBalances, pegoutValueInSatoshis);

// The rsk sender should lose the funds since there's no refund when a smart contract is trying to do a pegout
const expectedRskSenderBalanceInWeisBN = initialRskSenderBalanceInWeisBN.sub(new BN(`${satoshisToWeis(pegoutValueInSatoshis)}`));
const finalRskSenderBalanceInWeisBN = await rskTxHelper.getBalance(creatorAddress);
expect(finalRskSenderBalanceInWeisBN.eq(expectedRskSenderBalanceInWeisBN)).to.be.true;

// The contract balance should be the same as the initial balance since the contract is not paying for the pegout
const finalContractBalanceInWeisBN = await rskTxHelper.getBalance(callReleaseBtcContract.options.address);
expect(finalContractBalanceInWeisBN.eq(initialContractBalanceInWeisBN)).to.be.true;

});

it('should do a pegout and round down the weis to satoshis as expected', async () => {

// Arrange

const senderRecipientInfo = await createSenderRecipientInfo(rskTxHelper, btcTxHelper);
await fundRskAccountThroughAPegin(rskTxHelper, btcTxHelper, senderRecipientInfo.btcSenderAddressInfo);

const initial2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);
const initialSenderAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo.btcSenderAddressInfo.address);
const pegoutValueInWeisBN = new BN('41234567891234560'); // 0.04123456789123456 in RBTC
const expectedPegoutValueInSatoshis = 4123456; // 0.04123456 in BTC

// Act

const pegoutTransaction = await sendTxToBridge(rskTxHelper, pegoutValueInWeisBN, senderRecipientInfo.rskRecipientRskAddressInfo.address);

// Assert

let bridgeStateAfterPegoutCreation;

// Callback to get the bridge state after the pegout is created
const pegoutCreatedCallback = async () => {
bridgeStateAfterPegoutCreation = await getBridgeState(rskTxHelper.getClient());
};

const callbacks = {
pegoutCreatedCallback
};

await triggerRelease(rskTxHelpers, btcTxHelper, callbacks);

// Checking all the pegout events are emitted and in order
const blockNumberAfterPegoutRelease = await rskTxHelper.getBlockNumber();
const pegoutsEvents = await getPegoutEventsInBlockRange(rskTxHelper, pegoutTransaction.blockNumber, blockNumberAfterPegoutRelease);
await assertSuccessfulPegoutEventsAreEmitted(pegoutsEvents, pegoutTransaction.transactionHash, senderRecipientInfo, expectedPegoutValueInSatoshis, bridgeStateAfterPegoutCreation);

await assert2wpBalanceAfterSuccessfulPegoutWithLargeWeis(initial2wpBalances, pegoutValueInWeisBN, expectedPegoutValueInSatoshis);

// Assert that the sender address balance is increased by the actual pegout value
const finalSenderAddressBalanceInSatoshis = await getBtcAddressBalanceInSatoshis(btcTxHelper, senderRecipientInfo.btcSenderAddressInfo.address);
const releaseBtcEvent = pegoutsEvents[pegoutsEvents.length - 1];
const releaseBtcTransaction = bitcoinJsLib.Transaction.fromHex(removePrefix0x(releaseBtcEvent.arguments.btcRawTransaction));
const actualPegoutValueReceivedInSatoshis = releaseBtcTransaction.outs[0].value;
expect(finalSenderAddressBalanceInSatoshis).to.be.equal(initialSenderAddressBalanceInSatoshis + actualPegoutValueReceivedInSatoshis);

});

});

};
Expand Down Expand Up @@ -907,6 +993,19 @@ const assert2wpBalanceIsUnchanged = async (initial2wpBalances) => {
expect(final2wpBalances).to.be.deep.equal(initial2wpBalances);
};

const assert2wpBalancesAfterPegoutFromContract = async (initial2wpBalances, pegoutValueInSatoshis) => {
const final2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);

expect(final2wpBalances.federationAddressBalanceInSatoshis).to.be.equal(initial2wpBalances.federationAddressBalanceInSatoshis);

expect(final2wpBalances.bridgeUtxosBalanceInSatoshis).to.be.equal(initial2wpBalances.bridgeUtxosBalanceInSatoshis);
// When a contract sends funds to the Bridge to try to do a pegout, the pegout is rejected and the Bridge rsk balance is increased by the pegout value
// because there's no refund when a contract is trying to do a pegout.
const expectedBridgeBalanceInWeisBN = initial2wpBalances.bridgeBalanceInWeisBN.add(new BN(satoshisToWeis(pegoutValueInSatoshis)));

expect(final2wpBalances.bridgeBalanceInWeisBN.eq(expectedBridgeBalanceInWeisBN)).to.be.true;
};

const assertBtcPeginTxHashProcessed = async (btcPeginTxHash) => {
const isBtcTxHashAlreadyProcessed = await bridge.methods.isBtcTxHashAlreadyProcessed(btcPeginTxHash).call();
expect(isBtcTxHashAlreadyProcessed).to.be.true;
Expand Down Expand Up @@ -975,13 +1074,45 @@ const assert2wpBalanceAfterSuccessfulPegout = async (initial2wpBalances, pegoutV

};

/**
* Asserts the 2wp balances after a successful pegout, ensuring that the federation balance is decreased by the expected rounded pegout value in satoshis,
* the bridge rsk balance is increased by the pegout value with many decimals and the bridge utxos balance is decreased by the expected rounded pegout value in satoshis.
* When there is a pegout with big value in weis, that is not fully converted to satoshis and part of it needs to be trimmed.
* Bridge rsk balance will be slightly bigger than the federation balance due to this mismatch, since the Bridge will keep
* those trimmed weis while the federation will not.
* @param {{federationAddressBalanceInSatoshis: number, bridgeUtxosBalanceInSatoshis: number, bridgeBalanceInWeisBN: BN}} initial2wpBalances
* @param {BN} pegoutValueInWeisBN the value in BN of the pegout
* @param {number} expectedPegoutValueInSatoshis the expected pegout value in satoshis the user will receive, rounded down.
*/
const assert2wpBalanceAfterSuccessfulPegoutWithLargeWeis = async (initial2wpBalances, pegoutValueInWeisBN, expectedPegoutValueInSatoshis) => {

const final2wpBalances = await get2wpBalances(rskTxHelper, btcTxHelper);

expect(final2wpBalances.federationAddressBalanceInSatoshis).to.be.equal(initial2wpBalances.federationAddressBalanceInSatoshis - expectedPegoutValueInSatoshis);

const expectedFinalBridgeBalancesInWeisBN = initial2wpBalances.bridgeBalanceInWeisBN.add(pegoutValueInWeisBN);

expect(final2wpBalances.bridgeBalanceInWeisBN.eq(expectedFinalBridgeBalancesInWeisBN)).to.be.true;

expect(final2wpBalances.bridgeUtxosBalanceInSatoshis).to.be.equal(initial2wpBalances.bridgeUtxosBalanceInSatoshis - expectedPegoutValueInSatoshis);

};

const assertExpectedReleaseRequestRejectedEventIsEmitted = async (rskSenderAddress, amountInSatoshis, rejectionReason) => {
const rskSenderAddressChecksummed = rskTxHelper.getClient().utils.toChecksumAddress(ensure0x(rskSenderAddress));
const expectedEvent = createExpectedReleaseRequestRejectedEvent(rskSenderAddressChecksummed, amountInSatoshis, rejectionReason);
const releaseRequestRejectedEvent = await findEventInBlock(rskTxHelper, PEGOUT_EVENTS.RELEASE_REQUEST_REJECTED.name);
expect(releaseRequestRejectedEvent).to.be.deep.equal(expectedEvent);
};

const getReleaseRequestRejectedEventFromContractCallTxReceipt = (contractCallTxReceipt) => {
const bridgeTxParser = new BridgeTransactionParser(rskTxHelper.getClient());
const logData = contractCallTxReceipt.events['0'].raw;
const releaseRequestRejectedAbiElement = bridgeTxParser.jsonInterfaceMap[PEGOUT_EVENTS.RELEASE_REQUEST_REJECTED.signature];
const releaseRequestRejectedEvent = bridgeTxParser.decodeLog(logData, releaseRequestRejectedAbiElement);
return releaseRequestRejectedEvent;
};

module.exports = {
execute,
};
Loading

0 comments on commit b65a362

Please sign in to comment.