diff --git a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol index 7216b83f0..6c09dc1c4 100644 --- a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol +++ b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol @@ -30,7 +30,7 @@ contract PaymentInFlightExitRouterMock is FailFastReentrancyGuard, PaymentInFlig } /** override and calls processInFlightExit for test */ - function processExit(uint168 exitId, uint256, address ercContract) external { + function processExit(uint168 exitId, uint256, address ercContract, address payable) external { PaymentInFlightExitRouter.processInFlightExit(exitId, ercContract); } diff --git a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol index 2439f9e5e..6eb3bca9a 100644 --- a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol +++ b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol @@ -20,8 +20,8 @@ contract PaymentStandardExitRouterMock is PaymentStandardExitRouter { } /** override and calls processStandardExit for test */ - function processExit(uint168 exitId, uint256, address ercContract) external { - PaymentStandardExitRouter.processStandardExit(exitId, ercContract); + function processExit(uint168 exitId, uint256, address ercContract, address payable processor) external { + PaymentStandardExitRouter.processStandardExit(exitId, ercContract, processor); } /** helper functions for testing */ diff --git a/plasma_framework/contracts/mocks/framework/DummyExitGame.sol b/plasma_framework/contracts/mocks/framework/DummyExitGame.sol index 85a996b50..2f980958c 100644 --- a/plasma_framework/contracts/mocks/framework/DummyExitGame.sol +++ b/plasma_framework/contracts/mocks/framework/DummyExitGame.sol @@ -23,7 +23,7 @@ contract DummyExitGame is IExitProcessor { ); // override ExitProcessor interface - function processExit(uint168 exitId, uint256 vaultId, address ercContract) public { + function processExit(uint168 exitId, uint256 vaultId, address ercContract, address payable) public { emit ExitFinalizedFromDummyExitGame(exitId, vaultId, ercContract); } diff --git a/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol b/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol index 76e810f1a..467eac296 100644 --- a/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol +++ b/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol @@ -19,7 +19,7 @@ contract ReentrancyExitGame is IExitProcessor { // override ExitProcessor interface // This would call the processExits back to mimic reentracy attack - function processExit(uint168, uint256, address) public { + function processExit(uint168, uint256, address, address payable) public { exitGameController.processExits(vaultId, testToken, 0, reentryMaxExitToProcess); } diff --git a/plasma_framework/contracts/mocks/utils/ExitBountyWrapper.sol b/plasma_framework/contracts/mocks/utils/ExitBountyWrapper.sol new file mode 100644 index 000000000..a348d9f48 --- /dev/null +++ b/plasma_framework/contracts/mocks/utils/ExitBountyWrapper.sol @@ -0,0 +1,11 @@ +pragma solidity 0.5.11; + +import "../../src/exits/utils/ExitBounty.sol"; + +contract ExitBountyWrapper { + + function processStandardExitBountySize(uint256 gasPriceStartExit) public view returns (uint256) { + return ExitBounty.processStandardExitBountySize(gasPriceStartExit); + } + +} diff --git a/plasma_framework/contracts/poc/fast_exits/Liquidity.sol b/plasma_framework/contracts/poc/fast_exits/Liquidity.sol index 37e56645a..e8aa23216 100644 --- a/plasma_framework/contracts/poc/fast_exits/Liquidity.sol +++ b/plasma_framework/contracts/poc/fast_exits/Liquidity.sol @@ -8,6 +8,7 @@ import "../../src/utils/PosLib.sol"; import "../../src/framework/models/BlockModel.sol"; import "../../src/utils/Merkle.sol"; import "../../src/exits/payment/routers/PaymentStandardExitRouter.sol"; +import "../../src/exits/utils/ExitBounty.sol"; import "openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol"; import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; @@ -165,7 +166,12 @@ contract Liquidity is ERC721Full { FungibleTokenOutputModel.Output memory outputFromSecondTransaction = decodedSecondTx.outputs[0]; - exitData[exitId] = ExitData(msg.value, msg.sender, outputFromSecondTransaction.amount, outputFromSecondTransaction.token); + exitData[exitId] = ExitData( + msg.value - ExitBounty.processStandardExitBountySize(tx.gasprice), + msg.sender, + outputFromSecondTransaction.amount, + outputFromSecondTransaction.token + ); } /** diff --git a/plasma_framework/contracts/src/exits/payment/PaymentExitDataModel.sol b/plasma_framework/contracts/src/exits/payment/PaymentExitDataModel.sol index 0248ad3eb..5c32e8edd 100644 --- a/plasma_framework/contracts/src/exits/payment/PaymentExitDataModel.sol +++ b/plasma_framework/contracts/src/exits/payment/PaymentExitDataModel.sol @@ -15,6 +15,7 @@ library PaymentExitDataModel { * @param exitTarget The address to which the exit withdraws funds * @param amount The amount of funds to withdraw with this exit * @param bondSize The size of the bond put up for this exit to start, and which is used to cover the cost of challenges + * @param bountySize The size of the bounty put up to cover the cost of processing the exit */ struct StandardExit { bool exitable; @@ -23,6 +24,7 @@ library PaymentExitDataModel { address payable exitTarget; uint256 amount; uint256 bondSize; + uint256 bountySize; } /** diff --git a/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol b/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol index 74505ecde..23b5a2caa 100644 --- a/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol +++ b/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol @@ -41,10 +41,11 @@ contract PaymentExitGame is IExitProcessor, OnlyFromAddress, PaymentStandardExit * @notice Callback processes exit function for the PlasmaFramework to call * @param exitId The exit ID * @param token Token (ERC20 address or address(0) for ETH) of the exiting output + * @param processor The processExit initiator */ - function processExit(uint168 exitId, uint256, address token) external onlyFrom(address(paymentExitGameArgs.framework)) { + function processExit(uint168 exitId, uint256, address token, address payable processor) external onlyFrom(address(paymentExitGameArgs.framework)) { if (ExitId.isStandardExit(exitId)) { - PaymentStandardExitRouter.processStandardExit(exitId, token); + PaymentStandardExitRouter.processStandardExit(exitId, token, processor); } else { PaymentInFlightExitRouter.processInFlightExit(exitId, token); } diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol index db09a3c5d..b7d8d6910 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeStandardExit.sol @@ -90,7 +90,7 @@ library PaymentChallengeStandardExit { exitMap.exits[args.exitId].exitable = false; - SafeEthTransfer.transferRevertOnError(msg.sender, data.exitData.bondSize, self.safeGasStipend); + SafeEthTransfer.transferRevertOnError(msg.sender, data.exitData.bondSize + data.exitData.bountySize, self.safeGasStipend); emit ExitChallenged(data.exitData.utxoPos); } diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol index 30147b3f9..18f8a0d07 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol @@ -29,6 +29,11 @@ library PaymentProcessStandardExit { uint256 amount ); + event BountyRewardFailed( + address indexed receiver, + uint256 amount + ); + /** * @notice Main logic function to process standard exit * @dev emits ExitOmitted event if the exit is omitted @@ -42,7 +47,8 @@ library PaymentProcessStandardExit { Controller memory self, PaymentExitDataModel.StandardExitMap storage exitMap, uint168 exitId, - address token + address token, + address payable processor ) public { @@ -57,11 +63,16 @@ library PaymentProcessStandardExit { self.framework.flagOutputFinalized(exit.outputId, exitId); // we do not want to block a queue if bond return is unsuccessful - bool success = SafeEthTransfer.transferReturnResult(exit.exitTarget, exit.bondSize, self.safeGasStipend); - if (!success) { + bool successBondReturn = SafeEthTransfer.transferReturnResult(exit.exitTarget, exit.bondSize, self.safeGasStipend); + if (!successBondReturn) { emit BondReturnFailed(exit.exitTarget, exit.bondSize); } + bool successBountyReturn = SafeEthTransfer.transferReturnResult(processor, exit.bountySize, self.safeGasStipend); + if (!successBountyReturn) { + emit BountyRewardFailed(processor, exit.bountySize); + } + if (token == address(0)) { self.ethVault.withdraw(exit.exitTarget, exit.amount); } else { diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol index 79423607e..0fd61c414 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol @@ -7,10 +7,10 @@ import "../../utils/ExitableTimestamp.sol"; import "../../utils/ExitId.sol"; import "../../utils/OutputId.sol"; import "../../utils/MoreVpFinalization.sol"; +import "../../utils/ExitBounty.sol"; import "../../../transactions/PaymentTransactionModel.sol"; import "../../../utils/PosLib.sol"; import "../../../framework/PlasmaFramework.sol"; -import "../../utils/ExitableTimestamp.sol"; library PaymentStartStandardExit { using ExitableTimestamp for ExitableTimestamp.Calculator; @@ -170,7 +170,8 @@ library PaymentStartStandardExit { outputId: data.outputId, exitTarget: msg.sender, amount: data.output.amount, - bondSize: msg.value + bondSize: msg.value - ExitBounty.processStandardExitBountySize(tx.gasprice), + bountySize: ExitBounty.processStandardExitBountySize(tx.gasprice) }); } diff --git a/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol b/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol index 4daaa32b3..b5f79b20f 100644 --- a/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol +++ b/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol @@ -129,6 +129,13 @@ contract PaymentStandardExitRouter is emit StandardExitBondUpdated(newBondSize); } + /** + * @notice Retrieves the process standard exit bounty size + */ + function processStandardExitBountySize(uint256 gasPriceStartExit) public view returns (uint256) { + return ExitBounty.processStandardExitBountySize(gasPriceStartExit); + } + /** * @notice Starts a standard exit of a given output, using output-age priority */ @@ -138,7 +145,7 @@ contract PaymentStandardExitRouter is public payable nonReentrant(framework) - onlyWithValue(startStandardExitBondSize()) + onlyWithValue(startStandardExitBondSize() + processStandardExitBountySize(tx.gasprice)) { startStandardExitController.run(standardExitMap, args); } @@ -158,8 +165,9 @@ contract PaymentStandardExitRouter is * @dev This function is designed to be called in the main processExit function, using internal * @param exitId The standard exit ID * @param token The token (in erc20 address or address(0) for ETH) of the exiting output + * @param processor The processExit initiator */ - function processStandardExit(uint168 exitId, address token) internal { - processStandardExitController.run(standardExitMap, exitId, token); + function processStandardExit(uint168 exitId, address token, address payable processor) internal { + processStandardExitController.run(standardExitMap, exitId, token, processor); } } diff --git a/plasma_framework/contracts/src/exits/utils/ExitBounty.sol b/plasma_framework/contracts/src/exits/utils/ExitBounty.sol new file mode 100644 index 000000000..ca3c4f91f --- /dev/null +++ b/plasma_framework/contracts/src/exits/utils/ExitBounty.sol @@ -0,0 +1,14 @@ +pragma solidity 0.5.11; + +library ExitBounty { + + /** + * @notice Returns the Process Exit Bounty size for standard exits + * @dev See https://github.com/omgnetwork/plasma-contracts/issues/658 for discussion about size + * 107000 is the approx gas usage for calling processExit() + */ + function processStandardExitBountySize(uint256 gasPriceStartExit) internal view returns (uint256) { + return 107000 * gasPriceStartExit; + } + +} diff --git a/plasma_framework/contracts/src/framework/ExitGameController.sol b/plasma_framework/contracts/src/framework/ExitGameController.sol index 92094aa5a..64e821653 100644 --- a/plasma_framework/contracts/src/framework/ExitGameController.sol +++ b/plasma_framework/contracts/src/framework/ExitGameController.sol @@ -170,7 +170,7 @@ contract ExitGameController is ExitGameRegistry { queue.delMin(); processedNum++; - processor.processExit(exitId, vaultId, token); + processor.processExit(exitId, vaultId, token, msg.sender); if (queue.currentSize() == 0) { break; diff --git a/plasma_framework/contracts/src/framework/interfaces/IExitProcessor.sol b/plasma_framework/contracts/src/framework/interfaces/IExitProcessor.sol index f0db22659..79bb0c665 100644 --- a/plasma_framework/contracts/src/framework/interfaces/IExitProcessor.sol +++ b/plasma_framework/contracts/src/framework/interfaces/IExitProcessor.sol @@ -10,6 +10,7 @@ interface IExitProcessor { * @param exitId Unique ID for exit per tx type * @param vaultId ID of the vault that funds the exit * @param token Address of the token contract + * @param processor Address of the processExit intitiator */ - function processExit(uint168 exitId, uint256 vaultId, address token) external; + function processExit(uint168 exitId, uint256 vaultId, address token, address payable processor) external; } diff --git a/plasma_framework/python_tests/testlang/testlang.py b/plasma_framework/python_tests/testlang/testlang.py index cda49ddd5..46bd9686a 100644 --- a/plasma_framework/python_tests/testlang/testlang.py +++ b/plasma_framework/python_tests/testlang/testlang.py @@ -21,18 +21,20 @@ class StandardExit: exitable (boolean): whether will exit at processing output_id (str): output exit identifier (not exit id) bond_size (int): value of paid bond + bounty_size (int): value of process exit bounty """ - def __init__(self, exitable, utxo_pos, output_id, exit_target, amount, bond_size): + def __init__(self, exitable, utxo_pos, output_id, exit_target, amount, bond_size, bounty_size): self.owner = exit_target self.amount = amount self.position = utxo_pos self.exitable = exitable self.output_id = output_id self.bond_size = bond_size + self.bounty_size = bounty_size def to_list(self): - return [self.owner, self.amount, self.position, self.exitable, self.output_id, self.bond_size] + return [self.owner, self.amount, self.position, self.exitable, self.output_id, self.bond_size, self.bounty_size] def __str__(self): return self.to_list().__str__() @@ -224,9 +226,9 @@ def start_standard_exit_with_tx_body(self, output_id, output_tx, account, bond=N transactions = block.transactions merkle = FixedMerkle(16, list(map(lambda tx: tx.encoded, transactions))) proof = merkle.create_membership_proof(output_tx.encoded) - bond = bond if bond is not None else self.root_chain.standardExitBond() + bond = bond if bond is not None else self.root_chain.standardExitBond() + self.root_chain.processStandardExitBounty() self.root_chain.startStandardExit(output_id, output_tx.encoded, proof, - **{'value': bond, 'from': account.address}) + **{'value': bond, 'from': account.address, 'gasPrice': 100}) def challenge_standard_exit(self, output_id, spend_id, input_index=None, signature=None): spend_tx = self.child_chain.get_transaction(spend_id) diff --git a/plasma_framework/python_tests/tests/contracts/root_chain/test_process_exits.py b/plasma_framework/python_tests/tests/contracts/root_chain/test_process_exits.py index 06a509168..5f5431434 100644 --- a/plasma_framework/python_tests/tests/contracts/root_chain/test_process_exits.py +++ b/plasma_framework/python_tests/tests/contracts/root_chain/test_process_exits.py @@ -22,7 +22,7 @@ def prepare_exitable_utxo(testlang, owners, amount, outputs, num_outputs=1): @pytest.mark.parametrize("num_outputs", [1, 2, 3, 4]) -def test_process_exits_standard_exit_should_succeed(testlang, num_outputs, plasma_framework): +def test_process_exits_standard_exit_should_succeed(testlang, w3, num_outputs, plasma_framework): amount = 100 utxo_pos, output_owner = prepare_exitable_utxo(testlang, [], amount, [], num_outputs) @@ -30,6 +30,7 @@ def test_process_exits_standard_exit_should_succeed(testlang, num_outputs, plasm testlang.flush_events() testlang.start_standard_exit(utxo_pos, output_owner) + gasCost = w3.eth.last_gas_used * 100 _, _, exit_id = plasma_framework.getNextExit(plasma_framework.eth_vault_id, NULL_ADDRESS_HEX) start_exit_events = testlang.flush_events() @@ -45,7 +46,7 @@ def test_process_exits_standard_exit_should_succeed(testlang, num_outputs, plasm ('ExitFinalized', {"exitId": exit_id}), ('ProcessedExitsNum', {'processedNum': 1, 'token': NULL_ADDRESS_HEX})]) - assert testlang.get_balance(output_owner) == pre_balance + amount + assert testlang.get_balance(output_owner) == pre_balance + amount - gasCost - testlang.root_chain.processStandardExitBounty() def test_successful_process_exit_should_clear_exit_fields_and_set_output_as_spent(testlang): @@ -424,7 +425,7 @@ def test_finalize_exits_priority_for_in_flight_exits_corresponds_to_the_age_of_y balance = testlang.get_balance(owner) testlang.process_exits(NULL_ADDRESS, testlang.get_standard_exit_id(spend_00_id), 1) - assert testlang.get_balance(owner) == balance + 30 + testlang.root_chain.standardExitBond() + assert testlang.get_balance(owner) == balance + 30 + testlang.root_chain.standardExitBond() + testlang.root_chain.processStandardExitBounty() balance = testlang.get_balance(owner) testlang.process_exits(NULL_ADDRESS, testlang.get_in_flight_exit_id(spend_1_id), 1) @@ -433,7 +434,7 @@ def test_finalize_exits_priority_for_in_flight_exits_corresponds_to_the_age_of_y balance = testlang.get_balance(owner) testlang.process_exits(NULL_ADDRESS, testlang.get_standard_exit_id(spend_2_id), 1) - assert testlang.get_balance(owner) == balance + 100 + testlang.root_chain.standardExitBond() + assert testlang.get_balance(owner) == balance + 100 + testlang.root_chain.standardExitBond() + testlang.root_chain.processStandardExitBounty() def test_finalize_in_flight_exit_with_erc20_token_should_succeed(testlang, token, plasma_framework): @@ -912,7 +913,7 @@ def test_should_not_allow_to_withdraw_outputs_from_two_ifes_marked_as_canonical_ # assert alice_token_balance == alice_token_balance_before -def test_not_challenged_standard_exit_blocks_ife_output_exit(testlang, plasma_framework, token): +def test_not_challenged_standard_exit_blocks_ife_output_exit(testlang, w3, plasma_framework, token): alice, amount_token = testlang.accounts[1], 200 caroline, amount_eth_small, amount_eth_big = testlang.accounts[2], 1, 100 @@ -931,6 +932,7 @@ def test_not_challenged_standard_exit_blocks_ife_output_exit(testlang, plasma_fr caroline_eth_balance_before = testlang.get_balance(caroline) testlang.start_standard_exit(deposit_id_eth_small, caroline) + gasCost = w3.eth.last_gas_used * 100 testlang.start_in_flight_exit(swap_tx_id) testlang.piggyback_in_flight_exit_output(swap_tx_id, 0, alice) @@ -951,7 +953,7 @@ def test_not_challenged_standard_exit_blocks_ife_output_exit(testlang, plasma_fr assert caroline_token_balance == caroline_token_balance_before # but gets her Eth back caroline_eth_balance = testlang.get_balance(caroline) - assert caroline_eth_balance == caroline_eth_balance_before + amount_eth_big + amount_eth_small + assert caroline_eth_balance == caroline_eth_balance_before + amount_eth_big + amount_eth_small - gasCost - testlang.root_chain.processStandardExitBounty() # alice gets tokens alice_token_balance = token.balanceOf(alice.address) @@ -961,7 +963,7 @@ def test_not_challenged_standard_exit_blocks_ife_output_exit(testlang, plasma_fr assert alice_eth_balance == alice_eth_balance_before -def test_challenged_standard_exit_does_not_block_ife_output_exit(testlang, plasma_framework, token): +def test_challenged_standard_exit_does_not_block_ife_output_exit(testlang, w3, plasma_framework, token): alice, amount_token = testlang.accounts[1], 200 caroline, amount_eth_small, amount_eth_big = testlang.accounts[2], 1, 100 @@ -980,6 +982,7 @@ def test_challenged_standard_exit_does_not_block_ife_output_exit(testlang, plasm caroline_eth_balance_before = testlang.get_balance(caroline) testlang.start_standard_exit(deposit_id_eth_small, caroline) + gasCost = w3.eth.last_gas_used * 100 testlang.challenge_standard_exit(deposit_id_eth_small, swap_tx_id) testlang.start_in_flight_exit(swap_tx_id) @@ -1001,7 +1004,7 @@ def test_challenged_standard_exit_does_not_block_ife_output_exit(testlang, plasm assert caroline_token_balance == caroline_token_balance_before + amount_token # and does not get the Eth back caroline_eth_balance = testlang.get_balance(caroline) - assert caroline_eth_balance == caroline_eth_balance_before - testlang.root_chain.standardExitBond() + assert caroline_eth_balance == caroline_eth_balance_before - testlang.root_chain.standardExitBond() - gasCost - testlang.root_chain.processStandardExitBounty() # alice exits with her Eth output alice_eth_balance = testlang.get_balance(alice) diff --git a/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py b/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py index e6b3e82e8..e7177d417 100644 --- a/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py +++ b/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py @@ -343,6 +343,10 @@ def childBlockInterval(self): def standardExitBond(self): return self.payment_exit_game.startStandardExitBondSize() + def processStandardExitBounty(self): + dummy_gasprice = 100 + return self.payment_exit_game.processStandardExitBountySize(dummy_gasprice) + def inFlightExitBond(self): return self.payment_exit_game.startIFEBondSize() diff --git a/plasma_framework/test/endToEndTests/FastExit.e2e.test.js b/plasma_framework/test/endToEndTests/FastExit.e2e.test.js index c3e6f4934..98d195cdf 100644 --- a/plasma_framework/test/endToEndTests/FastExit.e2e.test.js +++ b/plasma_framework/test/endToEndTests/FastExit.e2e.test.js @@ -35,7 +35,7 @@ contract( alice = await web3.eth.personal.importRawKey(alicePrivateKey, password); alice = web3.utils.toChecksumAddress(alice); web3.eth.personal.unlockAccount(alice, password, 3600); - web3.eth.sendTransaction({ to: alice, from: richDad, value: web3.utils.toWei('1', 'ether') }); + web3.eth.sendTransaction({ to: alice, from: richDad, value: web3.utils.toWei('2', 'ether') }); }; const deployStableContracts = async () => { @@ -59,6 +59,12 @@ contract( this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); + this.dummyGasPrice = 1000000; + + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); + + this.startStandardExitTxValue = this.startStandardExitBondSize.add(this.processExitBountySize); + this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); this.liquidity = await Liquidity.new(this.framework.address, { from: authority }); @@ -192,7 +198,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: bob, value: this.startStandardExitBondSize }, + { + from: bob, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ), 'Was not called by the first Tx owner', ); @@ -216,7 +226,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ), "Provided Transaction isn't finalized or doesn't exist", ); @@ -239,7 +253,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ); }); @@ -283,7 +301,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ), 'Exit has already started.', ); @@ -429,7 +451,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ); }); @@ -550,7 +576,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ); }); @@ -685,7 +715,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }, ); }); @@ -711,7 +745,11 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: bob, value: this.updatedStandardExitBondSize }, + { + from: bob, + value: this.updatedStandardExitBondSize.add(this.processExitBountySize), + gasPrice: this.dummyGasPrice, + }, ); }); diff --git a/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js b/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js index e48a57ab1..03c2fcfda 100644 --- a/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js +++ b/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js @@ -2,16 +2,16 @@ const EthVault = artifacts.require('EthVault'); const PaymentExitGame = artifacts.require('PaymentExitGame'); const PlasmaFramework = artifacts.require('PlasmaFramework'); -const { - constants, expectEvent, -} = require('openzeppelin-test-helpers'); +const { constants, expectEvent } = require('openzeppelin-test-helpers'); const Testlang = require('../helpers/testlang.js'); const config = require('../../config.js'); const { - PaymentTransactionOutput, PaymentTransaction, - FeeTransaction, FeeClaimOutput, + PaymentTransactionOutput, + PaymentTransaction, + FeeTransaction, + FeeClaimOutput, } = require('../helpers/transaction.js'); const { sign } = require('../helpers/sign.js'); const { hashTx } = require('../helpers/paymentEip712.js'); @@ -42,12 +42,12 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, alice = await web3.eth.personal.importRawKey(alicePrivateKey, password); alice = web3.utils.toChecksumAddress(alice); web3.eth.personal.unlockAccount(alice, password, 3600); - web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('1', 'ether') }); + web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('2', 'ether') }); operatorFeeAddress = await web3.eth.personal.importRawKey(operatorFeeAddressPrivateKey, password); operatorFeeAddress = web3.utils.toChecksumAddress(operatorFeeAddress); web3.eth.personal.unlockAccount(operatorFeeAddress, password, 3600); - web3.eth.sendTransaction({ to: operatorFeeAddress, from: richFather, value: web3.utils.toWei('1', 'ether') }); + web3.eth.sendTransaction({ to: operatorFeeAddress, from: richFather, value: web3.utils.toWei('2', 'ether') }); }; describe('Given contracts deployed, ETH exitQueue added to the framework', () => { @@ -61,6 +61,8 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, ); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.dummyGasPrice = 1000000; + this.processExitBountySize = await this.paymentExitGame.processStandardExitBountySize(this.dummyGasPrice); }); describe('When Alice deposits ETH to the plasma', () => { @@ -83,10 +85,13 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, before(async () => { const transferAmount = 1000; - alicePlasmaBalance -= (transferAmount + FEE_AMOUNT); + alicePlasmaBalance -= transferAmount + FEE_AMOUNT; const outputBob = new PaymentTransactionOutput(PAYMENT_OUTPUT_TYPE, transferAmount, bob, ETH); const outputAlice = new PaymentTransactionOutput( - PAYMENT_OUTPUT_TYPE, alicePlasmaBalance, alice, ETH, + PAYMENT_OUTPUT_TYPE, + alicePlasmaBalance, + alice, + ETH, ); const aliceBalanceOutputIndex = 1; @@ -108,14 +113,11 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, before(async () => { const nextBlockNum = (await this.framework.nextChildBlock()).toNumber(); - const feeOutputs = [ - new FeeClaimOutput(FEE_OUTPUT_TYPE, FEE_AMOUNT, operatorFeeAddress, ETH), - ]; + const feeOutputs = [new FeeClaimOutput(FEE_OUTPUT_TYPE, FEE_AMOUNT, operatorFeeAddress, ETH)]; - const nonce = web3.utils.sha3(web3.eth.abi.encodeParameters( - ['uint256', 'address'], - [nextBlockNum, ETH], - )); + const nonce = web3.utils.sha3( + web3.eth.abi.encodeParameters(['uint256', 'address'], [nextBlockNum, ETH]), + ); const outputIndex = 0; const feeTxIndex = 1; @@ -138,12 +140,18 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, before(async () => { const transferAmount = 1000; - alicePlasmaBalance -= (transferAmount + FEE_AMOUNT); + alicePlasmaBalance -= transferAmount + FEE_AMOUNT; const outputCarol = new PaymentTransactionOutput( - PAYMENT_OUTPUT_TYPE, transferAmount, carol, ETH, + PAYMENT_OUTPUT_TYPE, + transferAmount, + carol, + ETH, ); const outputAlice = new PaymentTransactionOutput( - PAYMENT_OUTPUT_TYPE, alicePlasmaBalance, alice, ETH, + PAYMENT_OUTPUT_TYPE, + alicePlasmaBalance, + alice, + ETH, ); const txObj = new PaymentTransaction( @@ -164,10 +172,9 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, const feeOutputs = [ new FeeClaimOutput(FEE_OUTPUT_TYPE, FEE_AMOUNT, operatorFeeAddress, ETH), ]; - const nonce = web3.utils.sha3(web3.eth.abi.encodeParameters( - ['uint256', 'address'], - [nextBlockNum, ETH], - )); + const nonce = web3.utils.sha3( + web3.eth.abi.encodeParameters(['uint256', 'address'], [nextBlockNum, ETH]), + ); const secondFeeTx = new FeeTransaction(FEE_TX_TYPE, [], feeOutputs, nonce); secondFeeTxBytes = web3.utils.bytesToHex(secondFeeTx.rlpEncoded()); @@ -220,14 +227,12 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, }; const bondSize = await this.paymentExitGame.startStandardExitBondSize(); - const tx = await this.paymentExitGame.startStandardExit( - args, { from: operatorFeeAddress, value: bondSize }, - ); - await expectEvent.inLogs( - tx.logs, - 'ExitStarted', - { owner: operatorFeeAddress }, - ); + const tx = await this.paymentExitGame.startStandardExit(args, { + from: operatorFeeAddress, + value: bondSize.add(this.processExitBountySize), + gasPrice: this.dummyGasPrice, + }); + await expectEvent.inLogs(tx.logs, 'ExitStarted', { owner: operatorFeeAddress }); }); it('should be able to in-flight exit the fee via Payment transaction', async () => { @@ -242,14 +247,11 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, }; const bondSize = await this.paymentExitGame.startIFEBondSize(); - const tx = await this.paymentExitGame.startInFlightExit( - args, - { from: alice, value: bondSize }, - ); - await expectEvent.inLogs( - tx.logs, - 'InFlightExitStarted', - ); + const tx = await this.paymentExitGame.startInFlightExit(args, { + from: alice, + value: bondSize, + }); + await expectEvent.inLogs(tx.logs, 'InFlightExitStarted'); }); }); }); diff --git a/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js b/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js index 6ed972982..d43fbba78 100644 --- a/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js +++ b/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js @@ -39,7 +39,7 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai alice = await web3.eth.personal.importRawKey(alicePrivateKey, password); alice = web3.utils.toChecksumAddress(alice); web3.eth.personal.unlockAccount(alice, password, 3600); - web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('1', 'ether') }); + web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('2', 'ether') }); }; const deployStableContracts = async () => { @@ -66,6 +66,9 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai this.piggybackBondSize = await this.exitGame.piggybackBondSize(); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.dummyGasPrice = 1000000; + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); + this.startStandardExitTxValue = this.startStandardExitBondSize.add(this.processExitBountySize); }; const aliceDepositsETH = async () => { @@ -110,7 +113,7 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai it('should not allow to call processExit from outside of exit game controller contract', async () => { await expectRevert( - this.exitGame.processExit(0, config.registerKeys.vaultId.eth, constants.ZERO_ADDRESS), + this.exitGame.processExit(0, config.registerKeys.vaultId.eth, constants.ZERO_ADDRESS, alice), 'Caller address is unauthorized.', ); }); @@ -142,9 +145,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai rlpOutputTx: this.depositTx, outputTxInclusionProof: this.merkleProofForDepositTx, }; - await this.exitGame.startStandardExit( - args, { from: alice, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); }); it('should save the StandardExit data when successfully done', async () => { @@ -183,13 +188,17 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai expect(uniquePriority).to.be.bignumber.equal(priorityExpected); }); - describe('And then someone processes the exits for ETH after a week', () => { + describe('And then Bob processes the exits for ETH after a week', () => { before(async () => { await time.increase(time.duration.weeks(1).add(time.duration.seconds(1))); this.aliceBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(alice)); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1); + this.bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); + this.tx = await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1, { + from: bob, + gasPrice: this.dummyGasPrice, + }); }); it('should return the fund plus standard exit bond to Alice', async () => { @@ -200,6 +209,15 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai expect(actualAliceBalanceAfterProcessExit).to.be.bignumber.equal(expectedAliceBalance); }); + + it('should return the process exit bounty to the processor', async () => { + const actualBobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + const expectedBobBalance = this.bobBalanceBeforeProcessExit + .add(this.processExitBountySize) + .sub(await spentOnGas(this.tx.receipt)); + + expect(actualBobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); + }); }); }); }); @@ -218,9 +236,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai outputTxInclusionProof: this.merkleProofForTransferTx, }; - await this.exitGame.startStandardExit( - args, { from: bob, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: bob, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); }); it('should start successully', async () => { @@ -238,14 +258,18 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai this.bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1); + this.processTx = await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1, { + from: bob, + }); }); - it('should return the output amount plus standard exit bond to Bob', async () => { + it('should return the output amount plus standard exit bond plus process exit bounty to Bob', async () => { const actualBobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); const expectedBobBalance = this.bobBalanceBeforeProcessExit .add(this.startStandardExitBondSize) - .add(new BN(this.transferTxObject.outputs[0].amount)); + .add(this.processExitBountySize) + .add(new BN(this.transferTxObject.outputs[0].amount)) + .sub(await spentOnGas(this.processTx.receipt)); expect(actualBobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); }); @@ -267,9 +291,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai outputTxInclusionProof: this.merkleProofForDepositTx, }; - await this.exitGame.startStandardExit( - this.startStandardExitArgs, { from: alice, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(this.startStandardExitArgs, { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); this.exitId = await this.exitGame.getStandardExitId( true, this.depositTx, this.depositUtxoPos, @@ -309,10 +335,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai ); }); - it('should transfer the bond to Bob', async () => { + it('should transfer the bond and bounty to Bob', async () => { const actualBobBalanceAfterChallenge = new BN(await web3.eth.getBalance(bob)); const expectedBobBalanceAfterChallenge = this.bobBalanceBeforeChallenge .add(this.startStandardExitBondSize) + .add(this.processExitBountySize) .sub(await spentOnGas(this.challengeTx.receipt)); expect(actualBobBalanceAfterChallenge).to.be.bignumber.equal(expectedBobBalanceAfterChallenge); @@ -320,9 +347,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai it('should not allow Alice to restart the exit', async () => { await expectRevert( - this.exitGame.startStandardExit( - this.startStandardExitArgs, { from: alice, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(this.startStandardExitArgs, { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Exit has already started.', ); }); @@ -398,9 +427,11 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai outputTxInclusionProof: this.merkleProofForDepositTx, }; - await this.exitGame.startStandardExit( - args, { from: alice, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: alice, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); }); it('should start successully', async () => { @@ -413,18 +444,20 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai expect(standardExitData.exitable).to.be.true; }); - describe('And then someone processes the exits for the ERC20 token after a week', () => { + describe('And then Bob processes the exits for the ERC20 token after a week', () => { before(async () => { await time.increase(time.duration.weeks(1).add(time.duration.seconds(1))); this.aliceEthBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(alice)); + this.bobEthBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); this.aliceErc20BalanceBeforeProcessExit = new BN(await this.erc20.balanceOf(alice)); - await this.framework.processExits( + this.processTx = await this.framework.processExits( config.registerKeys.vaultId.erc20, this.erc20.address, 0, 1, + { from: bob }, ); }); @@ -437,6 +470,16 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai .to.be.bignumber.equal(expectedAliceEthBalance); }); + it('should return the process exit bounty in ETH to Bob', async () => { + const actualBobEthBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + const expectedBobEthBalance = this.bobEthBalanceBeforeProcessExit + .add(this.processExitBountySize) + .sub(await spentOnGas(this.processTx.receipt)); + + expect(actualBobEthBalanceAfterProcessExit) + .to.be.bignumber.equal(expectedBobEthBalance); + }); + it('should return ERC20 token with deposited amount to Alice', async () => { const actualAliceErc20BalanceAfterProcessExit = new BN(await this.erc20.balanceOf(alice)); const expectedAliceErc20Balance = this.aliceErc20BalanceBeforeProcessExit diff --git a/plasma_framework/test/endToEndTests/PaymentV2Experiment.e2e.test.js b/plasma_framework/test/endToEndTests/PaymentV2Experiment.e2e.test.js index 1c78d7799..6ae5a3448 100644 --- a/plasma_framework/test/endToEndTests/PaymentV2Experiment.e2e.test.js +++ b/plasma_framework/test/endToEndTests/PaymentV2Experiment.e2e.test.js @@ -6,9 +6,7 @@ const ERC20Mintable = artifacts.require('ERC20Mintable'); const PaymentExitGame = artifacts.require('PaymentExitGame'); const PlasmaFramework = artifacts.require('PlasmaFramework'); -const { - BN, constants, time, -} = require('openzeppelin-test-helpers'); +const { BN, constants, time } = require('openzeppelin-test-helpers'); const { expect } = require('chai'); const { MerkleTree } = require('../helpers/merkle.js'); @@ -61,6 +59,8 @@ contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, this.piggybackBondSize = await this.exitGame.piggybackBondSize(); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.dummyGasPrice = 1000000000; + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); }; const aliceDepositsETH = async () => { @@ -107,14 +107,18 @@ contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, outputTxInclusionProof: this.merkleProofForUpgradeTx, }; - await this.exitGame.startStandardExit( - args, { from: alice, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: alice, + value: this.startStandardExitBondSize.add(this.processExitBountySize), + gasPrice: this.dummyGasPrice, + }); }); it('should start successfully', async () => { const exitId = await this.exitGame.getStandardExitId( - false, this.upgradeTx, this.upgradeUtxoPos, + false, + this.upgradeTx, + this.upgradeUtxoPos, ); const exitIds = [exitId]; const standardExitData = (await this.exitGame.standardExits(exitIds))[0]; diff --git a/plasma_framework/test/endToEndTests/StandardExit.load.test.js b/plasma_framework/test/endToEndTests/StandardExit.load.test.js index db8cf3c3f..1f6f126d7 100644 --- a/plasma_framework/test/endToEndTests/StandardExit.load.test.js +++ b/plasma_framework/test/endToEndTests/StandardExit.load.test.js @@ -5,12 +5,9 @@ const PlasmaFramework = artifacts.require('PlasmaFramework'); const { BN, constants } = require('openzeppelin-test-helpers'); const { expect } = require('chai'); -const { EMPTY_BYTES, SAFE_GAS_STIPEND } = require('../helpers/constants.js'); const { MerkleTree } = require('../helpers/merkle.js'); -const { - computeDepositOutputId, -} = require('../helpers/utils.js'); +const { computeDepositOutputId } = require('../helpers/utils.js'); const { buildUtxoPos } = require('../helpers/positions.js'); const Testlang = require('../helpers/testlang.js'); const config = require('../../config.js'); @@ -29,7 +26,7 @@ contract('StandardExit getter Load Test', ([_deployer, _maintainer, richFather]) alice = await web3.eth.personal.importRawKey(alicePrivateKey, password); alice = web3.utils.toChecksumAddress(alice); web3.eth.personal.unlockAccount(alice, password, 3600); - web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('2', 'ether') }); + web3.eth.sendTransaction({ to: alice, from: richFather, value: web3.utils.toWei('4', 'ether') }); }; before(async () => { @@ -42,6 +39,8 @@ contract('StandardExit getter Load Test', ([_deployer, _maintainer, richFather]) this.exitGame = await PaymentExitGame.at(await this.framework.exitGames(config.registerKeys.txTypes.payment)); this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.dummyGasPrice = 1000000; + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); }; const aliceDepositsETH = async () => { @@ -79,8 +78,13 @@ contract('StandardExit getter Load Test', ([_deployer, _maintainer, richFather]) outputType: OUTPUT_TYPE_PAYMENT, outputTxInclusionProof: this.merkleProofForDepositTx[i], }; - startExits.push(this.exitGame.startStandardExit(args, - { from: alice, value: this.startStandardExitBondSize })); + startExits.push( + this.exitGame.startStandardExit(args, { + from: alice, + value: this.startStandardExitBondSize.add(this.processExitBountySize), + gasPrice: this.dummyGasPrice, + }), + ); } await Promise.all([startExits]); }); @@ -88,21 +92,22 @@ contract('StandardExit getter Load Test', ([_deployer, _maintainer, richFather]) it('should save the StandardExit data when successfully done', async () => { let exitIds = []; for (let i = 0; i < NUMBER_OF_EXITS; i++) { - exitIds.push(this.exitGame.getStandardExitId(true, - this.depositTx[i], this.depositUtxoPos[i])); + exitIds.push(this.exitGame.getStandardExitId(true, this.depositTx[i], this.depositUtxoPos[i])); } exitIds = await Promise.all(exitIds); - const standardExitData = (await this.exitGame.standardExits(exitIds)); + const standardExitData = await this.exitGame.standardExits(exitIds); const outputIndexForDeposit = 0; const outputId = []; for (let i = 0; i < NUMBER_OF_EXITS; i++) { - outputId.push(computeDepositOutputId(this.depositTx[i], - outputIndexForDeposit, this.depositUtxoPos[i])); + outputId.push( + computeDepositOutputId(this.depositTx[i], outputIndexForDeposit, this.depositUtxoPos[i]), + ); expect(standardExitData[i].exitable).to.be.true; expect(standardExitData[i].outputId).to.equal(outputId[i]); - expect(new BN(standardExitData[i].utxoPos)).to.be.bignumber - .equal(new BN(this.depositUtxoPos[i])); + expect(new BN(standardExitData[i].utxoPos)).to.be.bignumber.equal( + new BN(this.depositUtxoPos[i]), + ); expect(standardExitData[i].exitTarget).to.equal(alice); expect(new BN(standardExitData[i].amount)).to.be.bignumber.equal(new BN(DEPOSIT_VALUE)); } diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeStandardExit.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeStandardExit.test.js index 1a89c091a..f1f6ea58e 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeStandardExit.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeStandardExit.test.js @@ -16,16 +16,17 @@ const { const { expect } = require('chai'); const { - TX_TYPE, OUTPUT_TYPE, PROTOCOL, VAULT_ID, - DUMMY_INPUT_1, SAFE_GAS_STIPEND, + TX_TYPE, + OUTPUT_TYPE, + PROTOCOL, + VAULT_ID, + DUMMY_INPUT_1, + SAFE_GAS_STIPEND, } = require('../../../../helpers/constants.js'); const { buildUtxoPos } = require('../../../../helpers/positions.js'); -const { - spentOnGas, computeNormalOutputId, -} = require('../../../../helpers/utils.js'); +const { spentOnGas, computeNormalOutputId } = require('../../../../helpers/utils.js'); const { PaymentTransactionOutput, PaymentTransaction } = require('../../../../helpers/transaction.js'); - contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) => { const ETH = constants.ZERO_ADDRESS; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week in seconds @@ -65,7 +66,7 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) }; }; - const getTestExitData = (args, exitTarget, bondSize) => ({ + const getTestExitData = (args, exitTarget, bondSize, bountySize) => ({ exitable: true, utxoPos: EXITING_TX_UTXOPOS, outputId: computeNormalOutputId(args.exitingTx, TEST_OUTPUT_INDEX), @@ -73,11 +74,14 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) exitTarget, amount: TEST_AMOUNT, bondSize: bondSize.toString(), + bountySize: bountySize.toString(), }); beforeEach(async () => { this.framework = await SpyPlasmaFramework.new( - MIN_EXIT_PERIOD, DUMMY_INITIAL_IMMUNE_VAULTS_NUM, INITIAL_IMMUNE_EXIT_GAME_NUM, + MIN_EXIT_PERIOD, + DUMMY_INITIAL_IMMUNE_VAULTS_NUM, + INITIAL_IMMUNE_EXIT_GAME_NUM, ); this.ethVault = await SpyEthVault.new(this.framework.address); @@ -107,33 +111,48 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) await this.framework.registerExitGame(TX_TYPE.PAYMENT, this.exitGame.address, PROTOCOL.MORE_VP); this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); + + this.dummyGasPrice = 1000000000; + + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); }); describe('When spending condition not registered', () => { it('should fail by not able to find the spending condition contract', async () => { const args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); - const exitData = getTestExitData(args, alice, this.startStandardExitBondSize); + const exitData = getTestExitData( + args, + alice, + this.startStandardExitBondSize, + this.processExitBountySize, + ); await this.exitGame.setExit(args.exitId, exitData); - await expectRevert( - this.exitGame.challengeStandardExit(args), - 'Spending condition contract not found', - ); + await expectRevert(this.exitGame.challengeStandardExit(args), 'Spending condition contract not found'); }); }); describe('Given everything registered', () => { beforeEach(async () => { await this.spendingConditionRegistry.registerSpendingCondition( - OUTPUT_TYPE.PAYMENT, TX_TYPE.PAYMENT, this.spendingCondition.address, + OUTPUT_TYPE.PAYMENT, + TX_TYPE.PAYMENT, + this.spendingCondition.address, ); }); it('should fail when malicious user tries attack when paying out bond', async () => { - await this.exitGame.depositFundForTest({ value: this.startStandardExitBondSize }); + await this.exitGame.depositFundForTest({ + value: this.startStandardExitBondSize.add(this.processExitBountySize), + }); this.args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); - this.exitData = getTestExitData(this.args, alice, this.startStandardExitBondSize); + this.exitData = getTestExitData( + this.args, + alice, + this.startStandardExitBondSize, + this.processExitBountySize, + ); await this.exitGame.setExit(this.args.exitId, this.exitData); const attacker = await Attacker.new(); @@ -147,10 +166,7 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) it('should fail when exit for such exit id does not exist', async () => { const args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); - await expectRevert( - this.exitGame.challengeStandardExit(args), - 'The exit does not exist', - ); + await expectRevert(this.exitGame.challengeStandardExit(args), 'The exit does not exist'); }); it('should fail when try to challenge with a tx that is not of MoreVP protocol', async () => { @@ -167,7 +183,10 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) const challengeTx = web3.utils.bytesToHex(challengeTxObj.rlpEncoded()); args.challengeTx = challengeTx; - await this.exitGame.setExit(args.exitId, getTestExitData(args, alice, this.startStandardExitBondSize)); + await this.exitGame.setExit( + args.exitId, + getTestExitData(args, alice, this.startStandardExitBondSize, this.processExitBountySize), + ); await expectRevert( this.exitGame.challengeStandardExit(args), @@ -179,12 +198,12 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) const args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); await this.spendingCondition.mockResult(false); - await this.exitGame.setExit(args.exitId, getTestExitData(args, alice, this.startStandardExitBondSize)); - - await expectRevert( - this.exitGame.challengeStandardExit(args), - 'Spending condition failed', + await this.exitGame.setExit( + args.exitId, + getTestExitData(args, alice, this.startStandardExitBondSize, this.processExitBountySize), ); + + await expectRevert(this.exitGame.challengeStandardExit(args), 'Spending condition failed'); }); it('should fail when spending condition contract reverts', async () => { @@ -192,17 +211,20 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) await this.spendingCondition.mockRevert(); - await this.exitGame.setExit(args.exitId, getTestExitData(args, alice, this.startStandardExitBondSize)); - - await expectRevert( - this.exitGame.challengeStandardExit(args), - 'Test spending condition reverts', + await this.exitGame.setExit( + args.exitId, + getTestExitData(args, alice, this.startStandardExitBondSize, this.processExitBountySize), ); + + await expectRevert(this.exitGame.challengeStandardExit(args), 'Test spending condition reverts'); }); it('should fail when provided exiting transaction does not match stored exiting transaction', async () => { const args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); - await this.exitGame.setExit(args.exitId, getTestExitData(args, alice, this.startStandardExitBondSize)); + await this.exitGame.setExit( + args.exitId, + getTestExitData(args, alice, this.startStandardExitBondSize, this.processExitBountySize), + ); const output = new PaymentTransactionOutput(OUTPUT_TYPE.PAYMENT, TEST_AMOUNT, alice, ETH); const exitingTxObj = new PaymentTransaction(2, [DUMMY_INPUT_1], [output]); @@ -225,10 +247,17 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) }); it('should call the Spending Condition contract with expected params', async () => { - await this.exitGame.depositFundForTest({ value: this.startStandardExitBondSize }); + await this.exitGame.depositFundForTest({ + value: this.startStandardExitBondSize.add(this.processExitBountySize), + }); const args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); - const exitData = getTestExitData(args, alice, this.startStandardExitBondSize); + const exitData = getTestExitData( + args, + alice, + this.startStandardExitBondSize, + this.processExitBountySize, + ); await this.exitGame.setExit(args.exitId, exitData); const expectedArgs = { @@ -246,11 +275,18 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) describe('When successfully challenged', () => { beforeEach(async () => { - await this.exitGame.depositFundForTest({ value: this.startStandardExitBondSize }); + await this.exitGame.depositFundForTest({ + value: this.startStandardExitBondSize.add(this.processExitBountySize), + }); this.args = getTestInputArgs(OUTPUT_TYPE.PAYMENT, alice); this.args.senderData = web3.utils.keccak256(bob); - this.exitData = getTestExitData(this.args, alice, this.startStandardExitBondSize); + this.exitData = getTestExitData( + this.args, + alice, + this.startStandardExitBondSize, + this.processExitBountySize, + ); await this.exitGame.setExit(this.args.exitId, this.exitData); @@ -263,21 +299,20 @@ contract('PaymentChallengeStandardExit', ([txSender, alice, bob, otherAddress]) expect(exitData.exitable).to.be.false; }); - it('should transfer the standard exit bond to challenger when successfully challenged', async () => { + it('should transfer the standard exit bond plus exit bounty to challenger when successfully challenged', async () => { const actualPostBalance = new BN(await web3.eth.getBalance(bob)); const expectedPostBalance = this.preBalance .add(this.startStandardExitBondSize) + .add(this.processExitBountySize) .sub(await spentOnGas(this.tx.receipt)); expect(actualPostBalance).to.be.bignumber.equal(expectedPostBalance); }); it('should emit ExitChallenged event when successfully challenged', async () => { - await expectEvent.inLogs( - this.tx.logs, - 'ExitChallenged', - { utxoPos: new BN(this.exitData.utxoPos) }, - ); + await expectEvent.inLogs(this.tx.logs, 'ExitChallenged', { + utxoPos: new BN(this.exitData.utxoPos), + }); }); }); }); diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentProcessInFlightExit.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentProcessInFlightExit.test.js index a00253c13..0a648e096 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentProcessInFlightExit.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentProcessInFlightExit.test.js @@ -26,7 +26,7 @@ const { } = require('../../../../helpers/constants.js'); const { buildUtxoPos } = require('../../../../helpers/positions.js'); -contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwner2, inputOwner3, outputOwner1, outputOwner2, outputOwner3]) => { +contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwner2, inputOwner3, outputOwner1, outputOwner2, outputOwner3, otherAddress]) => { const MAX_INPUT_NUM = 4; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week in seconds const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; @@ -175,7 +175,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should omit the exit if the exit does not exist', async () => { const nonExistingExitId = 666; - const { logs } = await this.exitGame.processExit(nonExistingExitId, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(nonExistingExitId, VAULT_ID.ETH, ETH, otherAddress); await expectEvent.inLogs( logs, 'InFlightExitOmitted', @@ -195,7 +195,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne this.exit = await buildInFlightExitData(inputOwner1, outputOwner1, this.attacker.address); await this.exitGame.setInFlightExit(DUMMY_EXIT_ID, this.exit); - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); this.receipt = receipt; }); @@ -226,7 +226,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne this.exit.exitMap = exitMap; this.exit.isCanonical = isCanonical; await this.exitGame.setInFlightExit(DUMMY_EXIT_ID, this.exit); - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); return receipt; }; @@ -296,11 +296,11 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne await this.exitGame.setInFlightExitOutputPiggybacked(DUMMY_EXIT_ID, 0); await this.exitGame.setInFlightExitOutputPiggybacked(DUMMY_EXIT_ID, 2); - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); }); it('should transfer exit bond to the IFE bond owner if all piggybacked inputs/outputs are cleaned up', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const postBalance = new BN(await web3.eth.getBalance(ifeBondOwner)); const expectedBalance = this.ifeBondOwnerPreBalance.add(this.startIFEBondSize); @@ -331,7 +331,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne describe('When all piggybacks are resolved', () => { beforeEach(async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); }); it('should transfer exit bond to the IFE bond owner', async () => { @@ -360,7 +360,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should withdraw output if there are no inputs spent by other exit', async () => { await this.exitGame.proxyFlagOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_1, DUMMY_EXIT_ID); - const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); await expectEvent.inLogs( logs, 'InFlightExitOutputWithdrawn', @@ -371,7 +371,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should be treated as non-canonical when there is an input spent by other exit', async () => { const otherExitId = DUMMY_EXIT_ID + 1; await this.exitGame.proxyFlagOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_1, otherExitId); - const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); const inputIndexForInput2 = 1; await expectEvent.inLogs( logs, @@ -401,7 +401,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should withdraw ETH from vault for the piggybacked input', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); await expectEvent.inTransaction( receipt.transactionHash, SpyEthVault, @@ -415,7 +415,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should NOT withdraw fund from vault for the piggybacked but already spent input', async () => { await this.exitGame.proxyFlagOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_1, DUMMY_EXIT_ID); - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); let didNotCallEthWithdraw = false; try { await expectEvent.inTransaction( @@ -435,7 +435,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should NOT withdraw fund from vault for the non piggybacked input', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); let didNotCallEthWithdraw = false; try { await expectEvent.inTransaction( @@ -455,7 +455,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should withdraw ERC20 from vault for the piggybacked input', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); await expectEvent.inTransaction( receipt.transactionHash, SpyErc20Vault, @@ -469,7 +469,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should return piggyback bond to the input owner', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const postBalance = new BN(await web3.eth.getBalance(inputOwner3)); const expectedBalance = this.inputOwner3PreBalance.add(this.piggybackBondSize); @@ -477,7 +477,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should return piggyback bond to the output owner', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const postBalance = new BN(await web3.eth.getBalance(outputOwner3)); const expectedBalance = this.outputOwner3PreBalance.add(this.piggybackBondSize); @@ -485,7 +485,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should only flag piggybacked inputs with the same token as spent', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); // piggybacked input expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_1)).to.be.true; // non-piggybacked input @@ -495,7 +495,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should NOT flag output as spent', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_OUTPUT_1)).to.be.false; expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_OUTPUT_2)).to.be.false; @@ -503,7 +503,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should emit InFlightExitInputWithdrawn event', async () => { - const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const inputIndexForThirdInput = 2; await expectEvent.inLogs( logs, @@ -533,7 +533,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should withdraw ETH from vault for the piggybacked output', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); await expectEvent.inTransaction( receipt.transactionHash, SpyEthVault, @@ -547,7 +547,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should NOT withdraw from fund vault for the piggybacked but already spent output', async () => { await this.exitGame.proxyFlagOutputFinalized(TEST_OUTPUT_ID_FOR_OUTPUT_1, DUMMY_EXIT_ID); - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); let didNotCallEthWithdraw = false; try { await expectEvent.inTransaction( @@ -567,7 +567,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should NOT withdraw from fund vault for the non piggybacked output', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); let didNotCallEthWithdraw = false; try { await expectEvent.inTransaction( @@ -587,7 +587,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should withdraw ERC20 from vault for the piggybacked output', async () => { - const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + const { receipt } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); await expectEvent.inTransaction( receipt.transactionHash, SpyErc20Vault, @@ -601,7 +601,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should return piggyback bond to the output owner', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const postBalance = new BN(await web3.eth.getBalance(outputOwner3)); const expectedBalance = this.outputOwner3PreBalance.add(this.piggybackBondSize); @@ -609,7 +609,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should return piggyback bond to the input owner', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const postBalance = new BN(await web3.eth.getBalance(inputOwner3)); const expectedBalance = this.inputOwner3PreBalance.add(this.piggybackBondSize); @@ -617,7 +617,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should flag ALL inputs as spent', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); // same token, both piggybacked and non-piggybacked cases expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_1)).to.be.true; expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_INPUT_2)).to.be.true; @@ -626,7 +626,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should only flag piggybacked output with the same token as spent', async () => { - await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); // piggybacked output of same token expect(await this.framework.isOutputFinalized(TEST_OUTPUT_ID_FOR_OUTPUT_1)).to.be.true; @@ -637,7 +637,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); it('should emit InFlightExitOutputWithdrawn event', async () => { - const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20); + const { logs } = await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); const outputIndexForThirdOutput = 2; await expectEvent.inLogs( logs, diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentProcessStandardExit.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentProcessStandardExit.test.js index 67000bbc6..9626b049e 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentProcessStandardExit.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentProcessStandardExit.test.js @@ -10,23 +10,22 @@ const SpyErc20Vault = artifacts.require('SpyErc20VaultForExitGame'); const StateTransitionVerifierMock = artifacts.require('StateTransitionVerifierMock'); const Attacker = artifacts.require('FallbackFunctionFailAttacker'); -const { - BN, constants, expectEvent, -} = require('openzeppelin-test-helpers'); +const { BN, constants, expectEvent } = require('openzeppelin-test-helpers'); const { expect } = require('chai'); const { buildUtxoPos } = require('../../../../helpers/positions.js'); const { PROTOCOL, VAULT_ID, TX_TYPE, SAFE_GAS_STIPEND, } = require('../../../../helpers/constants.js'); +const { spentOnGas } = require('../../../../helpers/utils.js'); -contract('PaymentProcessStandardExit', ([_, alice]) => { +contract('PaymentProcessStandardExit', ([_, alice, bob, otherAddress]) => { const ETH = constants.ZERO_ADDRESS; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week in seconds const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; const INITIAL_IMMUNE_EXIT_GAME_NUM = 1; const EMPTY_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000'; - const EMPTY_EXIT_DATA = [false, '0', EMPTY_BYTES32, ETH, '0', '0']; + const EMPTY_EXIT_DATA = [false, '0', EMPTY_BYTES32, ETH, '0', '0', '0']; before('deploy and link with controller lib', async () => { const startStandardExit = await PaymentStartStandardExit.new(); @@ -41,7 +40,9 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { describe('processStandardExit', () => { beforeEach(async () => { this.framework = await SpyPlasmaFramework.new( - MIN_EXIT_PERIOD, DUMMY_INITIAL_IMMUNE_VAULTS_NUM, INITIAL_IMMUNE_EXIT_GAME_NUM, + MIN_EXIT_PERIOD, + DUMMY_INITIAL_IMMUNE_VAULTS_NUM, + INITIAL_IMMUNE_EXIT_GAME_NUM, ); const ethVault = await SpyEthVault.new(this.framework.address); @@ -68,7 +69,14 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { // prepare the bond that should be set when exit starts this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); - await this.exitGame.depositFundForTest({ value: this.startStandardExitBondSize }); + + this.dummyGasPrice = 1000000000; + + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); + + await this.exitGame.depositFundForTest({ + value: this.startStandardExitBondSize.add(this.processExitBountySize), + }); }); const getTestExitData = (exitable, token, exitTarget = alice) => ({ @@ -79,6 +87,7 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { exitTarget, amount: web3.utils.toWei('3', 'ether'), bondSize: this.startStandardExitBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }); describe('when paying out bond fails', () => { @@ -90,13 +99,15 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { await this.exitGame.setExit(exitId, testExitData); this.preBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); - const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + this.bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); + const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, bob, { from: bob }); this.receiptAfterAttack = receipt; }); it('should not pay out bond', async () => { const postBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); - expect(postBalance).to.be.bignumber.equal(this.preBalance); + const expectedBalance = this.preBalance.sub(this.processExitBountySize); + expect(postBalance).to.be.bignumber.equal(expectedBalance); }); it('should publish an event informing that bond pay out failed', async () => { @@ -110,6 +121,54 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { }, ); }); + + it('should still pay out exit bounty', async () => { + const bobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + const expectedBobBalance = this.bobBalanceBeforeProcessExit + .add(this.processExitBountySize) + .sub(await spentOnGas(this.receiptAfterAttack)); + expect(bobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); + }); + }); + + describe('when paying out bond fails', () => { + beforeEach(async () => { + const exitId = 1; + this.attacker = await Attacker.new(); + + const testExitData = getTestExitData(true, ETH, bob); + await this.exitGame.setExit(exitId, testExitData); + + this.preBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); + this.bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); + const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, this.attacker.address); + this.receiptAfterAttack = receipt; + }); + + it('should not pay out bounty', async () => { + const postBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); + const expectedBalance = this.preBalance.sub(this.startStandardExitBondSize); + expect(postBalance).to.be.bignumber.equal(expectedBalance); + }); + + it('should publish an event informing that bounty pay out failed', async () => { + await expectEvent.inTransaction( + this.receiptAfterAttack.transactionHash, + PaymentProcessStandardExit, + 'BountyRewardFailed', + { + receiver: this.attacker.address, + amount: new BN(this.processExitBountySize), + }, + ); + }); + + it('should still pay out the bond', async () => { + const bobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + const expectedBobBalance = this.bobBalanceBeforeProcessExit + .add(this.startStandardExitBondSize); + expect(bobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); + }); }); it('should not process the exit when such exit is not exitable', async () => { @@ -118,13 +177,9 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { const testExitData = getTestExitData(exitable, ETH); await this.exitGame.setExit(exitId, testExitData); - const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); - await expectEvent.inLogs( - logs, - 'ExitOmitted', - { exitId: new BN(exitId) }, - ); + await expectEvent.inLogs(logs, 'ExitOmitted', { exitId: new BN(exitId) }); const exitData = (await this.exitGame.standardExits([exitId]))[0]; expect(exitData).to.deep.equal(EMPTY_EXIT_DATA); @@ -136,13 +191,9 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { await this.exitGame.setExit(exitId, testExitData); await this.exitGame.proxyFlagOutputFinalized(testExitData.outputId, exitId); - const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); - await expectEvent.inLogs( - logs, - 'ExitOmitted', - { exitId: new BN(exitId) }, - ); + await expectEvent.inLogs(logs, 'ExitOmitted', { exitId: new BN(exitId) }); const exitData = (await this.exitGame.standardExits([exitId]))[0]; expect(exitData).to.deep.equal(EMPTY_EXIT_DATA); @@ -153,7 +204,7 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { const testExitData = getTestExitData(true, ETH); await this.exitGame.setExit(exitId, testExitData); - await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); expect(await this.framework.isOutputFinalized(testExitData.outputId)).to.be.true; }); @@ -164,13 +215,29 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { await this.exitGame.setExit(exitId, testExitData); const preBalance = new BN(await web3.eth.getBalance(testExitData.exitTarget)); - await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); const postBalance = new BN(await web3.eth.getBalance(testExitData.exitTarget)); const expectBalance = preBalance.add(this.startStandardExitBondSize); expect(postBalance).to.be.bignumber.equal(expectBalance); }); + it('should return exit bounty to processor when the exit token is ETH', async () => { + const exitId = 1; + const testExitData = getTestExitData(true, ETH); + await this.exitGame.setExit(exitId, testExitData); + + const bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); + const tx = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, bob, { from: bob }); + const bobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + + const expectedBobBalance = bobBalanceBeforeProcessExit + .add(this.processExitBountySize) + .sub(await spentOnGas(tx.receipt)); + + expect(bobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); + }); + it('should return standard exit bond to exit target when the exit token is ERC20', async () => { const exitId = 1; const erc20Token = (await ERC20Mintable.new()).address; @@ -178,28 +245,39 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { await this.exitGame.setExit(exitId, testExitData); const preBalance = new BN(await web3.eth.getBalance(testExitData.exitTarget)); - await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(exitId, VAULT_ID.ERC20, erc20Token, otherAddress); const postBalance = new BN(await web3.eth.getBalance(testExitData.exitTarget)); const expectBalance = preBalance.add(this.startStandardExitBondSize); expect(postBalance).to.be.bignumber.equal(expectBalance); }); + it('should return exit bounty to processor when the exit token is ERC20', async () => { + const exitId = 1; + const erc20Token = (await ERC20Mintable.new()).address; + const testExitData = getTestExitData(true, erc20Token); + await this.exitGame.setExit(exitId, testExitData); + + const bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(bob)); + const tx = await this.exitGame.processExit(exitId, VAULT_ID.ERC20, erc20Token, bob, { from: bob }); + const bobBalanceAfterProcessExit = new BN(await web3.eth.getBalance(bob)); + const expectedBobBalance = bobBalanceBeforeProcessExit + .add(this.processExitBountySize) + .sub(await spentOnGas(tx.receipt)); + + expect(bobBalanceAfterProcessExit).to.be.bignumber.equal(expectedBobBalance); + }); + it('should call the ETH vault with exit amount when the exit token is ETH', async () => { const exitId = 1; const testExitData = getTestExitData(true, ETH); await this.exitGame.setExit(exitId, testExitData); - const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); - await expectEvent.inTransaction( - receipt.transactionHash, - SpyEthVault, - 'EthWithdrawCalled', - { - target: testExitData.exitTarget, - amount: new BN(testExitData.amount), - }, - ); + const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); + await expectEvent.inTransaction(receipt.transactionHash, SpyEthVault, 'EthWithdrawCalled', { + target: testExitData.exitTarget, + amount: new BN(testExitData.amount), + }); }); it('should call the Erc20 vault with exit amount when the exit token is an ERC 20 token', async () => { @@ -208,18 +286,13 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { const testExitData = getTestExitData(true, erc20Token); await this.exitGame.setExit(exitId, testExitData); - const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ERC20, erc20Token); - - await expectEvent.inTransaction( - receipt.transactionHash, - SpyErc20Vault, - 'Erc20WithdrawCalled', - { - target: testExitData.exitTarget, - token: testExitData.token, - amount: new BN(testExitData.amount), - }, - ); + const { receipt } = await this.exitGame.processExit(exitId, VAULT_ID.ERC20, erc20Token, otherAddress); + + await expectEvent.inTransaction(receipt.transactionHash, SpyErc20Vault, 'Erc20WithdrawCalled', { + target: testExitData.exitTarget, + token: testExitData.token, + amount: new BN(testExitData.amount), + }); }); it('should deletes the standard exit data', async () => { @@ -227,7 +300,7 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { const testExitData = getTestExitData(true, ETH); await this.exitGame.setExit(exitId, testExitData); - await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); const exitData = (await this.exitGame.standardExits([exitId]))[0]; expect(exitData).to.deep.equal(EMPTY_EXIT_DATA); @@ -238,13 +311,9 @@ contract('PaymentProcessStandardExit', ([_, alice]) => { const testExitData = getTestExitData(true, ETH); await this.exitGame.setExit(exitId, testExitData); - const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH); + const { logs } = await this.exitGame.processExit(exitId, VAULT_ID.ETH, ETH, otherAddress); - await expectEvent.inLogs( - logs, - 'ExitFinalized', - { exitId: new BN(exitId) }, - ); + await expectEvent.inLogs(logs, 'ExitFinalized', { exitId: new BN(exitId) }); }); }); }); diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentStartStandardExit.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentStartStandardExit.test.js index 1ce5ca326..9db348ea7 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentStartStandardExit.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentStartStandardExit.test.js @@ -16,17 +16,18 @@ const { const { expect } = require('chai'); const { - OUTPUT_TYPE, PROTOCOL, TX_TYPE, VAULT_ID, DUMMY_INPUT_1, SAFE_GAS_STIPEND, + OUTPUT_TYPE, + PROTOCOL, + TX_TYPE, + VAULT_ID, + DUMMY_INPUT_1, + SAFE_GAS_STIPEND, } = require('../../../../helpers/constants.js'); const { MerkleTree } = require('../../../../helpers/merkle.js'); const { buildUtxoPos, txPostionForExitPriority } = require('../../../../helpers/positions.js'); -const { - computeDepositOutputId, - computeNormalOutputId, spentOnGas, -} = require('../../../../helpers/utils.js'); +const { computeDepositOutputId, computeNormalOutputId, spentOnGas } = require('../../../../helpers/utils.js'); const { PaymentTransactionOutput, PaymentTransaction } = require('../../../../helpers/transaction.js'); - contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const ETH = constants.ZERO_ADDRESS; const CHILD_BLOCK_INTERVAL = 1000; @@ -45,11 +46,7 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { }); describe('startStandardExit', () => { - const buildTestData = ( - amount, owner, blockNum, - txType = TX_TYPE.PAYMENT, - outputType = OUTPUT_TYPE.PAYMENT, - ) => { + const buildTestData = (amount, owner, blockNum, txType = TX_TYPE.PAYMENT, outputType = OUTPUT_TYPE.PAYMENT) => { const output = new PaymentTransactionOutput(outputType, amount, owner, ETH); const txObj = new PaymentTransaction(txType, [DUMMY_INPUT_1], [output]); const tx = web3.utils.bytesToHex(txObj.rlpEncoded()); @@ -66,15 +63,13 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { }; return { - args, outputIndex, merkleTree, + args, + outputIndex, + merkleTree, }; }; - const buildTestData2 = ( - outputs, - blockNum, - txType = TX_TYPE.PAYMENT, - ) => { + const buildTestData2 = (outputs, blockNum, txType = TX_TYPE.PAYMENT) => { const txObj = new PaymentTransaction(txType, [DUMMY_INPUT_1], outputs); const tx = web3.utils.bytesToHex(txObj.rlpEncoded()); @@ -88,7 +83,8 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { })); return { - args, merkleTree, + args, + merkleTree, }; }; @@ -103,7 +99,9 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { beforeEach(async () => { this.framework = await SpyPlasmaFramework.new( - MIN_EXIT_PERIOD, DUMMY_INITIAL_IMMUNE_VAULTS_NUM, INITIAL_IMMUNE_EXIT_GAME_NUM, + MIN_EXIT_PERIOD, + DUMMY_INITIAL_IMMUNE_VAULTS_NUM, + INITIAL_IMMUNE_EXIT_GAME_NUM, ); const ethVault = await SpyEthVault.new(this.framework.address); @@ -130,6 +128,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.registerExitGame(TX_TYPE.PAYMENT, this.exitGame.address, PROTOCOL.MORE_VP); this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); + + this.dummyGasPrice = 1000000; + + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(this.dummyGasPrice); + this.startStandardExitTxValue = this.startStandardExitBondSize.add(this.processExitBountySize); }); it('should fail when the transaction is not standard finalized', async () => { @@ -139,9 +142,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, fakeRoot, this.dummyBlockTimestamp); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'The transaction must be standard finalized', ); }); @@ -153,9 +158,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Output amount must not be 0', ); }); @@ -164,15 +171,20 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const nonSupportedTxType = TX_TYPE.PAYMENT + 1; const { args, merkleTree } = buildTestData( - this.dummyAmount, outputOwner, this.dummyBlockNum, nonSupportedTxType, + this.dummyAmount, + outputOwner, + this.dummyBlockNum, + nonSupportedTxType, ); await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Unsupported transaction type of the exit game', ); }); @@ -181,9 +193,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const { args } = buildTestData(this.dummyAmount, outputOwner, this.dummyBlockNum); // test by not stubbing the block data accordingly await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'There is no block for the position', ); }); @@ -195,9 +209,27 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const invalidBond = this.startStandardExitBondSize.subn(100); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: invalidBond }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: invalidBond.add(this.processExitBountySize), + gasPrice: this.dummyGasPrice, + }), + 'Input value must match msg.value', + ); + }); + + it('should fail when amount of bounty is invalid', async () => { + const { args, merkleTree } = buildTestData(this.dummyAmount, outputOwner, this.dummyBlockNum); + + await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); + + const invalidBounty = this.processExitBountySize.subn(1000); + await expectRevert( + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitBondSize.add(invalidBounty), + gasPrice: this.dummyGasPrice, + }), 'Input value must match msg.value', ); }); @@ -208,9 +240,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); await expectRevert( - this.exitGame.startStandardExit( - args, { from: nonOutputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: nonOutputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Only output owner can start an exit', ); }); @@ -220,14 +254,18 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); - await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Exit has already started', ); }); @@ -240,25 +278,30 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }), 'Output is already spent', ); }); - it('should charge the bond from the user', async () => { + it('should charge the bond and take bounty from the user', async () => { const { args, merkleTree } = buildTestData(this.dummyAmount, outputOwner, this.dummyBlockNum); await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); const preBalance = new BN(await web3.eth.getBalance(outputOwner)); - const tx = await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + const tx = await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const actualPostBalance = new BN(await web3.eth.getBalance(outputOwner)); const expectedPostBalance = preBalance .sub(this.startStandardExitBondSize) + .sub(this.processExitBountySize) .sub(await spentOnGas(tx.receipt)); expect(actualPostBalance).to.be.bignumber.equal(expectedPostBalance); @@ -270,9 +313,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(depositBlockNum, merkleTree.root, this.dummyBlockTimestamp); - await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const isTxDeposit = true; const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -293,9 +338,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(nonDepositBlockNum, merkleTree.root, this.dummyBlockTimestamp); - await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const isTxDeposit = false; const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -315,9 +362,11 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.setBlock(this.dummyBlockNum, merkleTree.root, this.dummyBlockTimestamp); - const { receipt } = await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + const { receipt } = await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const isTxDeposit = await this.framework.isDeposit(this.dummyBlockNum); const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -325,18 +374,13 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const currentTimestamp = await time.latest(); const exitableAt = await this.exitableHelper.calculateDepositTxOutputExitableTimestamp(currentTimestamp); - await expectEvent.inTransaction( - receipt.transactionHash, - SpyPlasmaFramework, - 'EnqueueTriggered', - { - token: ETH, - exitableAt, - txPos: new BN(txPostionForExitPriority(args.utxoPos)), - exitProcessor: this.exitGame.address, - exitId, - }, - ); + await expectEvent.inTransaction(receipt.transactionHash, SpyPlasmaFramework, 'EnqueueTriggered', { + token: ETH, + exitableAt, + txPos: new BN(txPostionForExitPriority(args.utxoPos)), + exitProcessor: this.exitGame.address, + exitId, + }); }); it('should emit ExitStarted event', async () => { @@ -346,15 +390,13 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const isTxDeposit = await this.framework.isDeposit(this.dummyBlockNum); const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); - const { logs } = await this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ); + const { logs } = await this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); - await expectEvent.inLogs( - logs, - 'ExitStarted', - { owner: outputOwner, exitId }, - ); + await expectEvent.inLogs(logs, 'ExitStarted', { owner: outputOwner, exitId }); }); it('should allow 2 outputs on the same transaction to exit', async () => { @@ -371,29 +413,29 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const isTxDeposit = await this.framework.isDeposit(BLOCK_NUM); - const { logs: logs1 } = await this.exitGame.startStandardExit( - args[0], { from: outputOwner, value: this.startStandardExitBondSize }, - ); + const { logs: logs1 } = await this.exitGame.startStandardExit(args[0], { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const exitId1 = await this.exitIdHelper.getStandardExitId( - isTxDeposit, args[0].rlpOutputTx, args[0].utxoPos, - ); - await expectEvent.inLogs( - logs1, - 'ExitStarted', - { owner: outputOwner, exitId: exitId1 }, + isTxDeposit, + args[0].rlpOutputTx, + args[0].utxoPos, ); + await expectEvent.inLogs(logs1, 'ExitStarted', { owner: outputOwner, exitId: exitId1 }); - const { logs: logs2 } = await this.exitGame.startStandardExit( - args[1], { from: outputOwner, value: this.startStandardExitBondSize }, - ); + const { logs: logs2 } = await this.exitGame.startStandardExit(args[1], { + from: outputOwner, + value: this.startStandardExitTxValue, + gasPrice: this.dummyGasPrice, + }); const exitId2 = await this.exitIdHelper.getStandardExitId( - isTxDeposit, args[1].rlpOutputTx, args[1].utxoPos, - ); - await expectEvent.inLogs( - logs2, - 'ExitStarted', - { owner: outputOwner, exitId: exitId2 }, + isTxDeposit, + args[1].rlpOutputTx, + args[1].utxoPos, ); + await expectEvent.inLogs(logs2, 'ExitStarted', { owner: outputOwner, exitId: exitId2 }); }); }); }); diff --git a/plasma_framework/test/src/exits/utils/BountySize.test.js b/plasma_framework/test/src/exits/utils/BountySize.test.js new file mode 100644 index 000000000..166e5b163 --- /dev/null +++ b/plasma_framework/test/src/exits/utils/BountySize.test.js @@ -0,0 +1,25 @@ +const ExitBounty = artifacts.require('ExitBountyWrapper'); +const { BN } = require('openzeppelin-test-helpers'); +const { expect } = require('chai'); + +contract('ExitBounty', () => { + const GAS_USED_PROCESS_EXIT = 107000; // approx gas usage of processExit + + before('setup', async () => { + this.contract = await ExitBounty.new(); + }); + + describe('calculate process standard exit bounty', () => { + it('should return the correct bounty for a gas price of 10 gwei', async () => { + const gasPrice = 10000000000; + expect(await this.contract.processStandardExitBountySize(gasPrice)) + .to.be.bignumber.equal(new BN(GAS_USED_PROCESS_EXIT * gasPrice)); + }); + + it('should return the correct bounty for a gas price of 80 gwei', async () => { + const gasPrice = 80000000000; + expect(await this.contract.processStandardExitBountySize(gasPrice)) + .to.be.bignumber.equal(new BN(GAS_USED_PROCESS_EXIT * gasPrice)); + }); + }); +});