diff --git a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol index 7216b83f0..783d225d5 100644 --- a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol +++ b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentInFlightExitRouterMock.sol @@ -30,8 +30,8 @@ contract PaymentInFlightExitRouterMock is FailFastReentrancyGuard, PaymentInFlig } /** override and calls processInFlightExit for test */ - function processExit(uint168 exitId, uint256, address ercContract) external { - PaymentInFlightExitRouter.processInFlightExit(exitId, ercContract); + function processExit(uint168 exitId, uint256, address ercContract, address payable processExitInitiator) external { + PaymentInFlightExitRouter.processInFlightExit(exitId, ercContract, processExitInitiator); } function setInFlightExit(uint168 exitId, PaymentExitDataModel.InFlightExit memory exit) public { diff --git a/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol b/plasma_framework/contracts/mocks/exits/payment/routers/PaymentStandardExitRouterMock.sol index 2439f9e5e..6045a94ba 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 processExitInitiator) external { + PaymentStandardExitRouter.processStandardExit(exitId, ercContract, processExitInitiator); } /** 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..58ff9c47d 100644 --- a/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol +++ b/plasma_framework/contracts/mocks/framework/ReentrancyExitGame.sol @@ -18,9 +18,9 @@ contract ReentrancyExitGame is IExitProcessor { } // override ExitProcessor interface - // This would call the processExits back to mimic reentracy attack - function processExit(uint168, uint256, address) public { - exitGameController.processExits(vaultId, testToken, 0, reentryMaxExitToProcess); + // This would call the processExits back to mimic reentrancy attack + function processExit(uint168, uint256, address, address payable) public { + exitGameController.processExits(vaultId, testToken, 0, reentryMaxExitToProcess, keccak256(abi.encodePacked(msg.sender))); } function enqueue(uint256 _vaultId, address _token, uint32 _exitableAt, uint256 _txPos, uint168 _exitId, IExitProcessor _exitProcessor) diff --git a/plasma_framework/contracts/mocks/utils/ExitBountyMock.sol b/plasma_framework/contracts/mocks/utils/ExitBountyMock.sol new file mode 100644 index 000000000..d4e7151fb --- /dev/null +++ b/plasma_framework/contracts/mocks/utils/ExitBountyMock.sol @@ -0,0 +1,21 @@ +pragma solidity 0.5.11; + +import "../../src/exits/utils/ExitBounty.sol"; + +contract ExitBountyMock { + using ExitBounty for ExitBounty.Params; + + ExitBounty.Params public exitBounty; + + constructor (uint128 initialExitBountySize, uint16 lowerBoundDivisor, uint16 upperBoundMultiplier) public { + exitBounty = ExitBounty.buildParams(initialExitBountySize, lowerBoundDivisor, upperBoundMultiplier); + } + + function exitBountySize() public view returns (uint128) { + return exitBounty.exitBountySize(); + } + + function updateExitBountySize(uint128 newExitBountySize) public { + exitBounty.updateExitBountySize(newExitBountySize); + } +} diff --git a/plasma_framework/contracts/poc/fast_exits/Liquidity.sol b/plasma_framework/contracts/poc/fast_exits/Liquidity.sol index 37e56645a..7769e8a54 100644 --- a/plasma_framework/contracts/poc/fast_exits/Liquidity.sol +++ b/plasma_framework/contracts/poc/fast_exits/Liquidity.sol @@ -8,9 +8,11 @@ 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"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; /** * @title Liquidity Contract @@ -18,6 +20,7 @@ import "openzeppelin-solidity/contracts/token/ERC20/SafeERC20.sol"; */ contract Liquidity is ERC721Full { using SafeERC20 for IERC20; + using SafeMath for uint256; PaymentExitGame public paymentExitGame; @@ -165,7 +168,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.sub(paymentExitGame.processStandardExitBountySize()), + 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..a2a67c4a8 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; } /** @@ -41,6 +43,7 @@ library PaymentExitDataModel { address token; uint256 amount; uint256 piggybackBondSize; + uint256 bountySize; } /** diff --git a/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol b/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol index 74505ecde..a5288e071 100644 --- a/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol +++ b/plasma_framework/contracts/src/exits/payment/PaymentExitGame.sol @@ -41,12 +41,13 @@ 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 processExitInitiator The processExits() initiator */ - function processExit(uint168 exitId, uint256, address token) external onlyFrom(address(paymentExitGameArgs.framework)) { + function processExit(uint168 exitId, uint256, address token, address payable processExitInitiator) external onlyFrom(address(paymentExitGameArgs.framework)) { if (ExitId.isStandardExit(exitId)) { - PaymentStandardExitRouter.processStandardExit(exitId, token); + PaymentStandardExitRouter.processStandardExit(exitId, token, processExitInitiator); } else { - PaymentInFlightExitRouter.processInFlightExit(exitId, token); + PaymentInFlightExitRouter.processInFlightExit(exitId, token, processExitInitiator); } } diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol index b160c5520..bfa7b15b3 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.sol @@ -102,7 +102,8 @@ library PaymentChallengeIFEInputSpent { ife.clearInputPiggybacked(args.inFlightTxInputIndex); uint256 piggybackBondSize = ife.inputs[args.inFlightTxInputIndex].piggybackBondSize; - SafeEthTransfer.transferRevertOnError(msg.sender, piggybackBondSize, self.safeGasStipend); + uint256 bountySize = ife.inputs[args.inFlightTxInputIndex].bountySize; + SafeEthTransfer.transferRevertOnError(msg.sender, piggybackBondSize + bountySize, self.safeGasStipend); emit InFlightExitInputBlocked(msg.sender, keccak256(args.inFlightTx), args.inFlightTxInputIndex); } diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol index 98783a1ad..7fe59ca2c 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.sol @@ -65,7 +65,8 @@ library PaymentChallengeIFEOutputSpent { ife.clearOutputPiggybacked(outputIndex); uint256 piggybackBondSize = ife.outputs[outputIndex].piggybackBondSize; - SafeEthTransfer.transferRevertOnError(msg.sender, piggybackBondSize, controller.safeGasStipend); + uint256 bountySize = ife.outputs[outputIndex].bountySize; + SafeEthTransfer.transferRevertOnError(msg.sender, piggybackBondSize + bountySize, controller.safeGasStipend); emit InFlightExitOutputBlocked(msg.sender, keccak256(args.inFlightTx), outputIndex); } 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/PaymentPiggybackInFlightExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol index 76c475e5c..f7aaad2a6 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentPiggybackInFlightExit.sol @@ -11,10 +11,13 @@ import "../../../framework/interfaces/IExitProcessor.sol"; import "../../../transactions/PaymentTransactionModel.sol"; import "../../../utils/PosLib.sol"; +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; + library PaymentPiggybackInFlightExit { using PosLib for PosLib.Position; using ExitableTimestamp for ExitableTimestamp.Calculator; using PaymentInFlightExitModelUtils for PaymentExitDataModel.InFlightExit; + using SafeMath for uint256; struct Controller { PlasmaFramework framework; @@ -71,7 +74,8 @@ library PaymentPiggybackInFlightExit { function piggybackInput( Controller memory self, PaymentExitDataModel.InFlightExitMap storage inFlightExitMap, - PaymentInFlightExitRouterArgs.PiggybackInFlightExitOnInputArgs memory args + PaymentInFlightExitRouterArgs.PiggybackInFlightExitOnInputArgs memory args, + uint128 processInFlightExitBountySize ) public { @@ -87,7 +91,8 @@ library PaymentPiggybackInFlightExit { PaymentExitDataModel.WithdrawData storage withdrawData = exit.inputs[args.inputIndex]; require(withdrawData.exitTarget == msg.sender, "Can be called only by the exit target"); - withdrawData.piggybackBondSize = msg.value; + withdrawData.bountySize = processInFlightExitBountySize; + withdrawData.piggybackBondSize = msg.value.sub(withdrawData.bountySize); if (isFirstPiggybackOfTheToken(exit, withdrawData.token)) { enqueue(self, withdrawData.token, PosLib.decode(exit.position), exitId); @@ -108,7 +113,8 @@ library PaymentPiggybackInFlightExit { function piggybackOutput( Controller memory self, PaymentExitDataModel.InFlightExitMap storage inFlightExitMap, - PaymentInFlightExitRouterArgs.PiggybackInFlightExitOnOutputArgs memory args + PaymentInFlightExitRouterArgs.PiggybackInFlightExitOnOutputArgs memory args, + uint128 processInFlightExitBountySize ) public { @@ -124,7 +130,8 @@ library PaymentPiggybackInFlightExit { PaymentExitDataModel.WithdrawData storage withdrawData = exit.outputs[args.outputIndex]; require(withdrawData.exitTarget == msg.sender, "Can be called only by the exit target"); - withdrawData.piggybackBondSize = msg.value; + withdrawData.bountySize = processInFlightExitBountySize; + withdrawData.piggybackBondSize = msg.value.sub(withdrawData.bountySize); if (isFirstPiggybackOfTheToken(exit, withdrawData.token)) { enqueue(self, withdrawData.token, PosLib.decode(exit.position), exitId); diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessInFlightExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessInFlightExit.sol index 2e5c1f8b5..edcec5e3f 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessInFlightExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessInFlightExit.sol @@ -39,6 +39,11 @@ library PaymentProcessInFlightExit { uint256 amount ); + event InFlightBountyReturnFailed( + address indexed receiver, + uint256 amount + ); + /** * @notice Main logic function to process in-flight exit * @dev emits InFlightExitOmitted event if the exit is omitted @@ -49,12 +54,14 @@ library PaymentProcessInFlightExit { * @param exitMap The storage of all in-flight exit data * @param exitId The exitId of the in-flight exit * @param token The ERC20 token address of the exit; uses address(0) to represent ETH + @ @param processExitInitiator The processExits() initiator */ function run( Controller memory self, PaymentExitDataModel.InFlightExitMap storage exitMap, uint168 exitId, - address token + address token, + address payable processExitInitiator ) public { @@ -108,8 +115,8 @@ library PaymentProcessInFlightExit { flagOutputsWhenCanonical(self.framework, exit, token, exitId); } - returnInputPiggybackBonds(self, exit, token); - returnOutputPiggybackBonds(self, exit, token); + returnInputPiggybackBonds(self, exit, token, processExitInitiator); + returnOutputPiggybackBonds(self, exit, token, processExitInitiator); clearPiggybackInputFlag(exit, token); clearPiggybackOutputFlag(exit, token); @@ -266,7 +273,8 @@ library PaymentProcessInFlightExit { function returnInputPiggybackBonds( Controller memory self, PaymentExitDataModel.InFlightExit storage exit, - address token + address token, + address payable processExitInitiator ) private { @@ -275,14 +283,23 @@ library PaymentProcessInFlightExit { // If the input has been challenged, isInputPiggybacked() will return false if (token == withdrawal.token && exit.isInputPiggybacked(i)) { - bool success = SafeEthTransfer.transferReturnResult( + bool successBondReturn = SafeEthTransfer.transferReturnResult( withdrawal.exitTarget, withdrawal.piggybackBondSize, self.safeGasStipend ); // we do not want to block a queue if bond return is unsuccessful - if (!success) { + if (!successBondReturn) { emit InFlightBondReturnFailed(withdrawal.exitTarget, withdrawal.piggybackBondSize); } + + bool successBountyReturn = SafeEthTransfer.transferReturnResult( + processExitInitiator, withdrawal.bountySize, self.safeGasStipend + ); + + // we do not want to block a queue if bounty return is unsuccessful + if (!successBountyReturn) { + emit InFlightBountyReturnFailed(processExitInitiator, withdrawal.bountySize); + } } } } @@ -290,7 +307,8 @@ library PaymentProcessInFlightExit { function returnOutputPiggybackBonds( Controller memory self, PaymentExitDataModel.InFlightExit storage exit, - address token + address token, + address payable processExitInitiator ) private { @@ -299,14 +317,23 @@ library PaymentProcessInFlightExit { // If the output has been challenged, isOutputPiggybacked() will return false if (token == withdrawal.token && exit.isOutputPiggybacked(i)) { - bool success = SafeEthTransfer.transferReturnResult( + bool successBondReturn = SafeEthTransfer.transferReturnResult( withdrawal.exitTarget, withdrawal.piggybackBondSize, self.safeGasStipend ); // we do not want to block a queue if bond return is unsuccessful - if (!success) { + if (!successBondReturn) { emit InFlightBondReturnFailed(withdrawal.exitTarget, withdrawal.piggybackBondSize); } + + bool successBountyReturn = SafeEthTransfer.transferReturnResult( + processExitInitiator, withdrawal.bountySize, self.safeGasStipend + ); + + // we do not want to block a queue if bounty return is unsuccessful + if (!successBountyReturn) { + emit InFlightBountyReturnFailed(processExitInitiator, withdrawal.bountySize); + } } } } diff --git a/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol b/plasma_framework/contracts/src/exits/payment/controllers/PaymentProcessStandardExit.sol index 30147b3f9..7a6de796a 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 BountyReturnFailed( + address indexed receiver, + uint256 amount + ); + /** * @notice Main logic function to process standard exit * @dev emits ExitOmitted event if the exit is omitted @@ -37,12 +42,14 @@ library PaymentProcessStandardExit { * @param exitMap The storage of all standard exit data * @param exitId The exitId of the standard exit * @param token The ERC20 token address of the exit. Uses address(0) to represent ETH. + * @param processExitInitiator The processExits() initiator */ function run( Controller memory self, PaymentExitDataModel.StandardExitMap storage exitMap, uint168 exitId, - address token + address token, + address payable processExitInitiator ) public { @@ -57,11 +64,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(processExitInitiator, exit.bountySize, self.safeGasStipend); + if (!successBountyReturn) { + emit BountyReturnFailed(processExitInitiator, 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 e6882b932..988020572 100644 --- a/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol +++ b/plasma_framework/contracts/src/exits/payment/controllers/PaymentStartStandardExit.sol @@ -10,12 +10,14 @@ import "../../utils/MoreVpFinalization.sol"; import "../../../transactions/PaymentTransactionModel.sol"; import "../../../utils/PosLib.sol"; import "../../../framework/PlasmaFramework.sol"; -import "../../utils/ExitableTimestamp.sol"; + +import "openzeppelin-solidity/contracts/math/SafeMath.sol"; library PaymentStartStandardExit { using ExitableTimestamp for ExitableTimestamp.Calculator; using PosLib for PosLib.Position; using PaymentTransactionModel for PaymentTransactionModel.Transaction; + using SafeMath for uint256; struct Controller { IExitProcessor exitProcessor; @@ -82,13 +84,14 @@ library PaymentStartStandardExit { function run( Controller memory self, PaymentExitDataModel.StandardExitMap storage exitMap, - PaymentStandardExitRouterArgs.StartStandardExitArgs memory args + PaymentStandardExitRouterArgs.StartStandardExitArgs memory args, + uint128 processStandardExitBountySize ) public { StartStandardExitData memory data = setupStartStandardExitData(self, args); verifyStartStandardExitData(self, data, exitMap); - saveStandardExitData(data, exitMap); + saveStandardExitData(data, exitMap, processStandardExitBountySize); enqueueStandardExit(data); emit ExitStarted({ @@ -165,7 +168,8 @@ library PaymentStartStandardExit { function saveStandardExitData( StartStandardExitData memory data, - PaymentExitDataModel.StandardExitMap storage exitMap + PaymentExitDataModel.StandardExitMap storage exitMap, + uint128 processStandardExitBountySize ) private { @@ -175,7 +179,8 @@ library PaymentStartStandardExit { outputId: data.outputId, exitTarget: msg.sender, amount: data.output.amount, - bondSize: msg.value + bondSize: msg.value.sub(processStandardExitBountySize), + bountySize: processStandardExitBountySize }); } diff --git a/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouter.sol b/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouter.sol index 17617ec88..d19f82d95 100644 --- a/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouter.sol +++ b/plasma_framework/contracts/src/exits/payment/routers/PaymentInFlightExitRouter.sol @@ -14,6 +14,7 @@ import "../controllers/PaymentProcessInFlightExit.sol"; import "../../registries/SpendingConditionRegistry.sol"; import "../../interfaces/IStateTransitionVerifier.sol"; import "../../utils/BondSize.sol"; +import "../../utils/ExitBounty.sol"; import "../../../utils/FailFastReentrancyGuard.sol"; import "../../../utils/OnlyFromAddress.sol"; import "../../../utils/OnlyWithValue.sol"; @@ -35,6 +36,7 @@ contract PaymentInFlightExitRouter is using PaymentDeleteInFlightExit for PaymentDeleteInFlightExit.Controller; using PaymentProcessInFlightExit for PaymentProcessInFlightExit.Controller; using BondSize for BondSize.Params; + using ExitBounty for ExitBounty.Params; // Initial IFE bond size = 185000 (gas cost of challenge) * 20 gwei (current fast gas price) * 10 (safety margin) uint128 public constant INITIAL_IFE_BOND_SIZE = 37000000000000000 wei; @@ -46,6 +48,13 @@ contract PaymentInFlightExitRouter is uint16 public constant BOND_LOWER_BOUND_DIVISOR = 2; uint16 public constant BOND_UPPER_BOUND_MULTIPLIER = 2; + // Initial exit bounty size = 500000 (approx gas usage for processExit) * 80 gwei (current fast gas price) + uint128 public constant INITIAL_IFE_EXIT_BOUNTY_SIZE = 40000000000000000 wei; + + // Each bounty size upgrade can either at most increase to 200% or decrease to 50% of current size + uint16 public constant EXIT_BOUNTY_LOWER_BOUND_DIVISOR = 2; + uint16 public constant EXIT_BOUNTY_UPPER_BOUND_MULTIPLIER = 2; + PaymentExitDataModel.InFlightExitMap internal inFlightExitMap; PaymentStartInFlightExit.Controller internal startInFlightExitController; PaymentPiggybackInFlightExit.Controller internal piggybackInFlightExitController; @@ -56,12 +65,14 @@ contract PaymentInFlightExitRouter is PaymentProcessInFlightExit.Controller internal processInflightExitController; BondSize.Params internal startIFEBond; BondSize.Params internal piggybackBond; + ExitBounty.Params internal processIFEBounty; PlasmaFramework private framework; bool private bootDone = false; event IFEBondUpdated(uint128 bondSize); event PiggybackBondUpdated(uint128 bondSize); + event ProcessInFlightExitBountyUpdated(uint128 exitBountySize); event InFlightExitStarted( address indexed initiator, @@ -184,6 +195,7 @@ contract PaymentInFlightExitRouter is }); startIFEBond = BondSize.buildParams(INITIAL_IFE_BOND_SIZE, BOND_LOWER_BOUND_DIVISOR, BOND_UPPER_BOUND_MULTIPLIER); piggybackBond = BondSize.buildParams(INITIAL_PB_BOND_SIZE, BOND_LOWER_BOUND_DIVISOR, BOND_UPPER_BOUND_MULTIPLIER); + processIFEBounty = ExitBounty.buildParams(INITIAL_IFE_EXIT_BOUNTY_SIZE, EXIT_BOUNTY_LOWER_BOUND_DIVISOR, EXIT_BOUNTY_UPPER_BOUND_MULTIPLIER); } /** @@ -222,9 +234,10 @@ contract PaymentInFlightExitRouter is public payable nonReentrant(framework) - onlyWithValue(piggybackBondSize()) + onlyWithValue(piggybackBondSize() + processInFlightExitBountySize()) { - piggybackInFlightExitController.piggybackInput(inFlightExitMap, args); + uint128 bountySize = processInFlightExitBountySize(); + piggybackInFlightExitController.piggybackInput(inFlightExitMap, args, bountySize); } /** @@ -237,9 +250,10 @@ contract PaymentInFlightExitRouter is public payable nonReentrant(framework) - onlyWithValue(piggybackBondSize()) + onlyWithValue(piggybackBondSize() + processInFlightExitBountySize()) { - piggybackInFlightExitController.piggybackOutput(inFlightExitMap, args); + uint128 bountySize = processInFlightExitBountySize(); + piggybackInFlightExitController.piggybackOutput(inFlightExitMap, args, bountySize); } /** @@ -308,9 +322,10 @@ contract PaymentInFlightExitRouter is * @dev This function is designed to be called in the main processExit function, thus, using internal * @param exitId The in-flight exit ID * @param token The token (in erc20 address or address(0) for ETH) of the exiting output + * @param processExitInitiator The processExits() initiator */ - function processInFlightExit(uint168 exitId, address token) internal { - processInflightExitController.run(inFlightExitMap, exitId, token); + function processInFlightExit(uint168 exitId, address token, address payable processExitInitiator) internal { + processInflightExitController.run(inFlightExitMap, exitId, token, processExitInitiator); } /** @@ -344,4 +359,20 @@ contract PaymentInFlightExitRouter is piggybackBond.updateBondSize(newBondSize); emit PiggybackBondUpdated(newBondSize); } + + /** + * @notice Retrieves the process IFE bounty size + */ + function processInFlightExitBountySize() public view returns (uint128) { + return processIFEBounty.exitBountySize(); + } + + /** + * @notice Updates the process in-flight exit bounty size, taking two days to become effective + * @param newExitBountySize The new exit bounty size + */ + function updateProcessInFlightExitBountySize(uint128 newExitBountySize) public onlyFrom(framework.getMaintainer()) { + processIFEBounty.updateExitBountySize(newExitBountySize); + emit ProcessInFlightExitBountyUpdated(newExitBountySize); + } } diff --git a/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol b/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol index 4daaa32b3..2ed9097ec 100644 --- a/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol +++ b/plasma_framework/contracts/src/exits/payment/routers/PaymentStandardExitRouter.sol @@ -9,6 +9,7 @@ import "../controllers/PaymentProcessStandardExit.sol"; import "../controllers/PaymentChallengeStandardExit.sol"; import "../../registries/SpendingConditionRegistry.sol"; import "../../utils/BondSize.sol"; +import "../../utils/ExitBounty.sol"; import "../../../vaults/EthVault.sol"; import "../../../vaults/Erc20Vault.sol"; import "../../../framework/PlasmaFramework.sol"; @@ -27,6 +28,7 @@ contract PaymentStandardExitRouter is using PaymentChallengeStandardExit for PaymentChallengeStandardExit.Controller; using PaymentProcessStandardExit for PaymentProcessStandardExit.Controller; using BondSize for BondSize.Params; + using ExitBounty for ExitBounty.Params; // Initial bond size = 70000 (gas cost of challenge) * 20 gwei (current fast gas price) * 10 (safety margin) uint128 public constant INITIAL_BOND_SIZE = 14000000000000000 wei; @@ -35,16 +37,25 @@ contract PaymentStandardExitRouter is uint16 public constant BOND_LOWER_BOUND_DIVISOR = 2; uint16 public constant BOND_UPPER_BOUND_MULTIPLIER = 2; + // Initial exit bounty size = 107000 (approx gas usage for processExit) * 80 gwei (current fast gas price) + uint128 public constant INITIAL_EXIT_BOUNTY_SIZE = 8560000000000000 wei; + + // Each bounty size upgrade can either at most increase to 200% or decrease to 50% of current size + uint16 public constant EXIT_BOUNTY_LOWER_BOUND_DIVISOR = 2; + uint16 public constant EXIT_BOUNTY_UPPER_BOUND_MULTIPLIER = 2; + PaymentExitDataModel.StandardExitMap internal standardExitMap; PaymentStartStandardExit.Controller internal startStandardExitController; PaymentProcessStandardExit.Controller internal processStandardExitController; PaymentChallengeStandardExit.Controller internal challengeStandardExitController; BondSize.Params internal startStandardExitBond; + ExitBounty.Params internal processStandardExitBounty; PlasmaFramework private framework; bool private bootDone = false; event StandardExitBondUpdated(uint128 bondSize); + event ProcessStandardExitBountyUpdated(uint128 exitBountySize); event ExitStarted( address indexed owner, @@ -98,6 +109,7 @@ contract PaymentStandardExitRouter is ); startStandardExitBond = BondSize.buildParams(INITIAL_BOND_SIZE, BOND_LOWER_BOUND_DIVISOR, BOND_UPPER_BOUND_MULTIPLIER); + processStandardExitBounty = ExitBounty.buildParams(INITIAL_EXIT_BOUNTY_SIZE, EXIT_BOUNTY_LOWER_BOUND_DIVISOR, EXIT_BOUNTY_UPPER_BOUND_MULTIPLIER); } /** @@ -129,6 +141,22 @@ contract PaymentStandardExitRouter is emit StandardExitBondUpdated(newBondSize); } + /** + * @notice Retrieves the process standard exit bounty size + */ + function processStandardExitBountySize() public view returns (uint128) { + return processStandardExitBounty.exitBountySize(); + } + + /** + * @notice Updates the process standard exit bounty size, taking two days to become effective + * @param newExitBountySize The new exit bounty size + */ + function updateProcessStandardExitBountySize(uint128 newExitBountySize) public onlyFrom(framework.getMaintainer()) { + processStandardExitBounty.updateExitBountySize(newExitBountySize); + emit ProcessStandardExitBountyUpdated(newExitBountySize); + } + /** * @notice Starts a standard exit of a given output, using output-age priority */ @@ -138,9 +166,10 @@ contract PaymentStandardExitRouter is public payable nonReentrant(framework) - onlyWithValue(startStandardExitBondSize()) + onlyWithValue(startStandardExitBondSize() + processStandardExitBountySize()) { - startStandardExitController.run(standardExitMap, args); + uint128 bountySize = processStandardExitBountySize(); + startStandardExitController.run(standardExitMap, args, bountySize); } /** @@ -158,8 +187,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 processExitInitiator The processExits() initiator */ - function processStandardExit(uint168 exitId, address token) internal { - processStandardExitController.run(standardExitMap, exitId, token); + function processStandardExit(uint168 exitId, address token, address payable processExitInitiator) internal { + processStandardExitController.run(standardExitMap, exitId, token, processExitInitiator); } } 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..bcb33e46f --- /dev/null +++ b/plasma_framework/contracts/src/exits/utils/ExitBounty.sol @@ -0,0 +1,74 @@ +pragma solidity 0.5.11; + +/** + * @notice Stores an updateable exit bounty size + * @dev See https://github.com/omgnetwork/plasma-contracts/issues/658 for discussion about size + */ +library ExitBounty { + uint64 constant public WAITING_PERIOD = 2 days; + + /** + * @param previousExitBountySize The exit bounty size prior to upgrade, which should remain the same until the waiting period completes + * @param updatedExitBountySize The exit bounty size to use once the waiting period completes + * @param effectiveUpdateTime A timestamp for the end of the waiting period, when the updated exit bounty size is implemented + * @param lowerBoundDivisor The divisor that checks the lower bound for an update. Each update cannot be lower than (current bounty / lowerBoundDivisor) + * @param upperBoundMultiplier The multiplier that checks the upper bound for an update. Each update cannot be larger than (current bounty * upperBoundMultiplier) + */ + struct Params { + uint128 previousExitBountySize; + uint128 updatedExitBountySize; + uint128 effectiveUpdateTime; + uint16 lowerBoundDivisor; + uint16 upperBoundMultiplier; + } + + function buildParams(uint128 initialExitBountySize, uint16 lowerBoundDivisor, uint16 upperBoundMultiplier) + internal + pure + returns (Params memory) + { + // Set the initial value far into the future + uint128 initialEffectiveUpdateTime = 2 ** 63; + return Params({ + previousExitBountySize: initialExitBountySize, + updatedExitBountySize: 0, + effectiveUpdateTime: initialEffectiveUpdateTime, + lowerBoundDivisor: lowerBoundDivisor, + upperBoundMultiplier: upperBoundMultiplier + }); + } + + /** + * @notice Updates the Exit Bounty size + * @dev The new bounty size value updates once the two day waiting period completes + * @param newExitBountySize The new bounty size + */ + function updateExitBountySize(Params storage self, uint128 newExitBountySize) internal { + validateExitBountySize(self, newExitBountySize); + + if (self.updatedExitBountySize != 0 && now >= self.effectiveUpdateTime) { + self.previousExitBountySize = self.updatedExitBountySize; + } + self.updatedExitBountySize = newExitBountySize; + self.effectiveUpdateTime = uint64(now) + WAITING_PERIOD; + } + + /** + * @notice Returns the current exit bounty size + */ + function exitBountySize(Params memory self) internal view returns (uint128) { + if (now < self.effectiveUpdateTime) { + return self.previousExitBountySize; + } else { + return self.updatedExitBountySize; + } + } + + function validateExitBountySize(Params memory self, uint128 newExitBountySize) private view { + uint128 currentExitBountySize = exitBountySize(self); + require(newExitBountySize > 0, "Bounty size cannot be zero"); + require(newExitBountySize >= currentExitBountySize / self.lowerBoundDivisor, "Bounty size is too low"); + require(uint256(newExitBountySize) <= uint256(currentExitBountySize) * self.upperBoundMultiplier, "Bounty size is too high"); + } + +} diff --git a/plasma_framework/contracts/src/framework/ExitGameController.sol b/plasma_framework/contracts/src/framework/ExitGameController.sol index 92094aa5a..5c88ff84e 100644 --- a/plasma_framework/contracts/src/framework/ExitGameController.sol +++ b/plasma_framework/contracts/src/framework/ExitGameController.sol @@ -148,9 +148,11 @@ contract ExitGameController is ExitGameRegistry { * @param token The token type to process * @param topExitId Unique identifier for prioritizing the first exit to process. Set to zero to skip this check. * @param maxExitsToProcess Maximum number of exits to process + * @param senderData A keccak256 hash of the sender's address * @return Total number of processed exits */ - function processExits(uint256 vaultId, address token, uint168 topExitId, uint256 maxExitsToProcess) external nonReentrant { + function processExits(uint256 vaultId, address token, uint168 topExitId, uint256 maxExitsToProcess, bytes32 senderData) external nonReentrant { + require(senderData == keccak256(abi.encodePacked(msg.sender)), "Incorrect SenderData"); bytes32 key = exitQueueKey(vaultId, token); require(hasExitQueue(key), "The token is not yet added to the Plasma framework"); PriorityQueue queue = exitsQueues[key]; @@ -170,7 +172,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..3ff2bad2e 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 processExitInitiator Address of the processExit intitiator */ - function processExit(uint168 exitId, uint256 vaultId, address token) external; + function processExit(uint168 exitId, uint256 vaultId, address token, address payable processExitInitiator) external; } diff --git a/plasma_framework/python_tests/testlang/testlang.py b/plasma_framework/python_tests/testlang/testlang.py index 7093886f7..67b87e6e2 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__() @@ -61,12 +63,13 @@ def __init__(self, root, timestamp): class InFlightExit: class WithdrawData: - def __init__(self, output_id, exit_target, token, amount, piggyback_bond_size): + def __init__(self, output_id, exit_target, token, amount, piggyback_bond_size, bounty_size): self.output_id = output_id self.exit_target = exit_target self.token = token self.amount = amount self.piggyback_bond_size = piggyback_bond_size + self.bounty_size = bounty_size def __init__(self, root_chain, in_flight_tx, is_canonical, @@ -224,7 +227,7 @@ 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}) @@ -272,7 +275,7 @@ def __init__(self, deposit_id, owner, token, amount, spend, spend_id): spend = self.child_chain.get_transaction(spend_id) return Utxo(deposit_id, owner, token_address, amount, spend, spend_id) - def process_exits(self, token, exit_id, count=1, vault_id=None, **kwargs): + def process_exits(self, token, exit_id, count=1, vault_id=None, sender=None, **kwargs): """Finalizes exits that have completed the exit period. Args: @@ -282,7 +285,10 @@ def process_exits(self, token, exit_id, count=1, vault_id=None, **kwargs): vault_id (int): Id of the vault that funds the exit """ - return self.root_chain.processExits(token, exit_id, count, vault_id, **kwargs) + if sender is None: + sender = self.accounts[0].address + + return self.root_chain.processExits(token, exit_id, count, sender, vault_id, **kwargs) def get_challenge_proof(self, utxo_id, spend_id): """Returns information required to submit a challenge. @@ -398,7 +404,7 @@ def get_merkle_proof(self, tx_id): def piggyback_in_flight_exit_input(self, tx_id, input_index, account, bond=None, spend_tx=None): if spend_tx is None: spend_tx = self.child_chain.get_transaction(tx_id) - bond = bond if bond is not None else self.root_chain.piggybackBond() + bond = bond if bond is not None else self.root_chain.piggybackBond() + self.root_chain.processInFlightExitBounty() self.root_chain.piggybackInFlightExit(spend_tx.encoded, input_index, **{'value': bond, 'from': account.address}) def piggyback_in_flight_exit_output(self, tx_id, output_index, account, bond=None, spend_tx=None): 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 4694f821b..7ab74f517 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 @@ -1,5 +1,6 @@ import pytest from eth_tester.exceptions import TransactionFailed +from eth_utils import keccak from plasma_core.constants import NULL_ADDRESS, NULL_ADDRESS_HEX, MIN_EXIT_PERIOD from plasma_core.transaction import Transaction @@ -41,7 +42,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 - testlang.root_chain.processStandardExitBounty() def test_successful_process_exit_should_clear_exit_fields_and_set_output_as_spent(testlang): @@ -81,7 +82,7 @@ def test_process_exits_in_flight_exit_should_succeed(testlang): assert input_info.exit_target == NULL_ADDRESS_HEX assert input_info.amount == 0 - expected_balance = pre_balance + amount + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + expected_balance = pre_balance + amount + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + testlang.root_chain.processInFlightExitBounty() assert testlang.get_balance(owner) == expected_balance @@ -251,8 +252,8 @@ def test_finalize_exits_tx_race_short_circuit(testlang, w3, plasma_framework): w3.eth.disable_auto_mine() tx_hash = plasma_framework.plasma_framework.functions \ - .processExits(plasma_framework.eth_vault_id, NULL_ADDRESS, testlang.get_standard_exit_id(utxo1.spend_id), 3) \ - .transact({'gas': 100_000}) # reasonably high amount of gas (otherwise it fails on gas estimation) + .processExits(plasma_framework.eth_vault_id, NULL_ADDRESS, testlang.get_standard_exit_id(utxo1.spend_id), 3, keccak(hexstr=testlang.accounts[0].address)) \ + .transact({'from': testlang.accounts[0].address, 'gas': 100_000}) # reasonably high amount of gas (otherwise it fails on gas estimation) w3.eth.mine(expect_error=True) @@ -364,7 +365,7 @@ def test_finalize_exits_for_in_flight_exit_should_transfer_funds(testlang, plasm testlang.process_exits(NULL_ADDRESS, 0, 10) assert testlang.get_balance(owner) == \ - pre_balance + first_utxo + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + pre_balance + first_utxo + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + testlang.root_chain.processInFlightExitBounty() def test_finalize_in_flight_exit_finalizes_only_piggybacked_outputs(testlang, plasma_framework): @@ -389,7 +390,7 @@ def test_finalize_in_flight_exit_finalizes_only_piggybacked_outputs(testlang, pl testlang.process_exits(NULL_ADDRESS, 0, 10) assert testlang.get_balance(owner) == \ - pre_balance + first_utxo + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + pre_balance + first_utxo + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + testlang.root_chain.processInFlightExitBounty() in_flight_exit = testlang.get_in_flight_exit(spend_id) @@ -420,16 +421,16 @@ 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) assert testlang.get_balance( - owner) == balance + 70 + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + owner) == balance + 70 + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + testlang.root_chain.processInFlightExitBounty() 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): @@ -658,7 +659,7 @@ def test_when_processing_an_ife_it_is_cleaned_up_when_all_piggybacked_outputs_fi assert in_flight_exit.exit_map == 0 # assert bond was sent to the owner - assert testlang.get_balance(testlang.accounts[0]) == pre_balance + testlang.root_chain.inFlightExitBond() + assert testlang.get_balance(testlang.accounts[0]) == pre_balance + testlang.root_chain.inFlightExitBond() + (2 * testlang.root_chain.processInFlightExitBounty()) def test_in_flight_exit_is_cleaned_up_even_though_none_of_outputs_exited(testlang): @@ -682,7 +683,7 @@ def test_in_flight_exit_is_cleaned_up_even_though_none_of_outputs_exited(testlan assert in_flight_exit.exit_map == 0 # assert IFE and piggyback bonds were sent to the owners - assert testlang.get_balance(owner) == pre_balance + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + assert testlang.get_balance(owner) == pre_balance + testlang.root_chain.inFlightExitBond() + testlang.root_chain.piggybackBond() + testlang.root_chain.processInFlightExitBounty() def test_processing_ife_and_se_exit_from_same_output_does_not_fail(testlang): @@ -848,7 +849,7 @@ def test_should_not_allow_to_withdraw_outputs_from_two_ifes_marked_as_canonical_ # but she can not exit with Eth caroline_eth_balance = testlang.get_balance(caroline) - assert caroline_eth_balance == caroline_eth_balance_before + assert caroline_eth_balance == caroline_eth_balance_before - (2 * testlang.root_chain.processInFlightExitBounty()) def test_should_not_allow_to_withdraw_from_non_canonical_and_already_spent_input_but_can_withdraw_from_canonical_tx_outputs(testlang, w3, plasma_framework, token): @@ -974,14 +975,14 @@ 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 - testlang.root_chain.processStandardExitBounty() - (3 * testlang.root_chain.processInFlightExitBounty()) # alice gets tokens alice_token_balance = token.balanceOf(alice.address) assert alice_token_balance == alice_token_balance_before + amount_token # but does not get Eth output alice_eth_balance = testlang.get_balance(alice) - assert alice_eth_balance == alice_eth_balance_before + assert alice_eth_balance == alice_eth_balance_before - (2 * testlang.root_chain.processInFlightExitBounty()) def test_challenged_standard_exit_does_not_block_ife_output_exit(testlang, plasma_framework, token): @@ -1024,11 +1025,11 @@ 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() - testlang.root_chain.processStandardExitBounty() - (3 * testlang.root_chain.processInFlightExitBounty()) # alice exits with her Eth output alice_eth_balance = testlang.get_balance(alice) - assert alice_eth_balance == alice_eth_balance_before + amount_eth_small + amount_eth_big + assert alice_eth_balance == alice_eth_balance_before + amount_eth_small + amount_eth_big - (2 * testlang.root_chain.processInFlightExitBounty()) # and does not get the tokens back alice_token_balance = token.balanceOf(alice.address) assert alice_token_balance == alice_token_balance_before 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..08ede1176 100644 --- a/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py +++ b/plasma_framework/python_tests/tests/tests_utils/plasma_framework.py @@ -1,5 +1,6 @@ import enum +from eth_utils import keccak from plasma_core.constants import CHILD_BLOCK_INTERVAL, EMPTY_BYTES, NULL_ADDRESS from plasma_core.transaction import TxOutputTypes, TxTypes, Transaction from plasma_core.utils.transactions import decode_utxo_id @@ -292,14 +293,14 @@ def challengeInFlightExitOutputSpent(self, in_flight_tx, ) self.payment_exit_game.challengeInFlightExitOutputSpent(args, **kwargs) - def processExits(self, token, top_exit_id, exits_to_process, vault_id=None): + def processExits(self, token, top_exit_id, exits_to_process, sender, vault_id=None): if vault_id is None: if token == NULL_ADDRESS: vault_id = self.eth_vault_id else: vault_id = self.erc20_vault_id - return self.plasma_framework.processExits(vault_id, token, top_exit_id, exits_to_process) + return self.plasma_framework.processExits(vault_id, token, top_exit_id, exits_to_process, keccak(hexstr=sender), **{"from": sender}) def deleteNonPiggybackedInFlightExit(self, exit_id): return self.payment_exit_game.deleteNonPiggybackedInFlightExit(exit_id) @@ -343,6 +344,12 @@ def childBlockInterval(self): def standardExitBond(self): return self.payment_exit_game.startStandardExitBondSize() + def processStandardExitBounty(self): + return self.payment_exit_game.processStandardExitBountySize() + + def processInFlightExitBounty(self): + return self.payment_exit_game.processInFlightExitBountySize() + 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..632b8d55b 100644 --- a/plasma_framework/test/endToEndTests/FastExit.e2e.test.js +++ b/plasma_framework/test/endToEndTests/FastExit.e2e.test.js @@ -19,7 +19,7 @@ const config = require('../../config.js'); contract( 'LiquidityContract - Fast Exits - End to End Tests', - ([_deployer, maintainer, authority, bob, richDad]) => { + ([_deployer, maintainer, authority, bob, richDad, otherAddress]) => { const ETH = constants.ZERO_ADDRESS; const OUTPUT_TYPE_PAYMENT = config.registerKeys.outputTypes.payment; const INITIAL_ERC20_SUPPLY = 10000000000; @@ -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,10 @@ contract( this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(); + + 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 +196,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: bob, value: this.startStandardExitBondSize }, + { + from: bob, + value: this.startStandardExitTxValue, + }, ), 'Was not called by the first Tx owner', ); @@ -216,7 +223,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ), "Provided Transaction isn't finalized or doesn't exist", ); @@ -239,7 +249,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ); }); @@ -283,7 +296,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ), 'Exit has already started.', ); @@ -322,9 +338,14 @@ contract( await web3.eth.getBalance(this.liquidity.address), ); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1, { - from: alice, - }); + await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + 1, + web3.utils.keccak256(alice), + { from: alice }, + ); }); it('should return the output amount plus standard exit bond to the Liquidity Contract', async () => { @@ -429,7 +450,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ); }); @@ -452,7 +476,14 @@ contract( before(async () => { await time.increase(time.duration.weeks(2).add(time.duration.seconds(1))); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1); + await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); }); describe('When Alice tries to claim funds back', () => { @@ -550,7 +581,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ); }); @@ -593,6 +627,8 @@ contract( this.erc20.address, 0, 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, ); }); @@ -685,7 +721,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: alice, value: this.startStandardExitBondSize }, + { + from: alice, + value: this.startStandardExitTxValue, + }, ); }); @@ -711,7 +750,10 @@ contract( rlpDepositTx, depositInclusionProof, depositUtxoPos, - { from: bob, value: this.updatedStandardExitBondSize }, + { + from: bob, + value: this.updatedStandardExitBondSize.add(this.processExitBountySize), + }, ); }); @@ -719,7 +761,14 @@ contract( before(async () => { await time.increase(time.duration.weeks(2).add(time.duration.seconds(1))); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 2); + await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + 2, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); }); describe('When Alice tries to get exit bond back', () => { diff --git a/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js b/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js index b364ef8c4..fcab52e5e 100644 --- a/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js +++ b/plasma_framework/test/endToEndTests/FeeClaim.e2e.test.js @@ -4,16 +4,16 @@ const PlasmaFramework = artifacts.require('PlasmaFramework'); const PaymentStartStandardExit = artifacts.require('PaymentStartStandardExit'); const PaymentStartInFlightExit = artifacts.require('PaymentStartInFlightExit'); -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'); @@ -44,12 +44,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', () => { @@ -63,6 +63,7 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, ); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.processExitBountySize = await this.paymentExitGame.processStandardExitBountySize(); }); describe('When Alice deposits ETH to the plasma', () => { @@ -85,10 +86,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; @@ -110,14 +114,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; @@ -140,12 +141,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( @@ -166,10 +173,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()); @@ -224,7 +230,7 @@ contract('PlasmaFramework - Fee Claim', ([_, _maintainer, authority, richFather, const bondSize = await this.paymentExitGame.startStandardExitBondSize(); const { receipt } = await this.paymentExitGame.startStandardExit(args, { from: operatorFeeAddress, - value: bondSize, + value: bondSize.add(this.processExitBountySize), }); await expectEvent.inTransaction( receipt.transactionHash, diff --git a/plasma_framework/test/endToEndTests/PaymentInFlightExit.e2e.test.js b/plasma_framework/test/endToEndTests/PaymentInFlightExit.e2e.test.js index 60337f952..c2a4230a6 100644 --- a/plasma_framework/test/endToEndTests/PaymentInFlightExit.e2e.test.js +++ b/plasma_framework/test/endToEndTests/PaymentInFlightExit.e2e.test.js @@ -11,14 +11,14 @@ const { expect } = require('chai'); const { EMPTY_BYTES, SAFE_GAS_STIPEND } = require('../helpers/constants.js'); const { MerkleTree } = require('../helpers/merkle.js'); const { PaymentTransactionOutput, PaymentTransaction } = require('../helpers/transaction.js'); -const { computeNormalOutputId } = require('../helpers/utils.js'); +const { computeNormalOutputId, spentOnGas } = require('../helpers/utils.js'); const { sign } = require('../helpers/sign.js'); const { hashTx } = require('../helpers/paymentEip712.js'); const { buildUtxoPos } = require('../helpers/positions.js'); const Testlang = require('../helpers/testlang.js'); const config = require('../../config.js'); -contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _maintainer, authority, carol, richFather]) => { +contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _maintainer, authority, carol, richFather, otherAddress]) => { const ETH = constants.ZERO_ADDRESS; const DEPOSIT_VALUE = 1000000; const TX_TYPE_PAYMENT = config.registerKeys.txTypes.payment; @@ -35,12 +35,12 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma 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') }); bob = await web3.eth.personal.importRawKey(bobPrivateKey, password); bob = web3.utils.toChecksumAddress(bob); web3.eth.personal.unlockAccount(bob, password, 3600); - web3.eth.sendTransaction({ to: bob, from: richFather, value: web3.utils.toWei('1', 'ether') }); + web3.eth.sendTransaction({ to: bob, from: richFather, value: web3.utils.toWei('2', 'ether') }); }; before(setupAccount); @@ -56,6 +56,9 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma this.startIFEBondSize = await this.exitGame.startIFEBondSize(); this.piggybackBondSize = await this.exitGame.piggybackBondSize(); + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + this.piggybackExitTxValue = this.piggybackBondSize.add(this.processExitBountySize); + this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); }; @@ -134,7 +137,7 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma this.piggybackTx = await this.exitGame.piggybackInFlightExitOnOutput( args, - { from: bob, value: this.piggybackBondSize }, + { from: bob, value: this.piggybackExitTxValue }, ); }); @@ -159,8 +162,14 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma await time.increase(time.duration.weeks(2).add(time.duration.seconds(1))); this.exitsToProcess = 1; + this.preBalanceCarol = new BN(await web3.eth.getBalance(carol)); this.processTx = await this.framework.processExits( - config.registerKeys.vaultId.eth, ETH, 0, this.exitsToProcess, + config.registerKeys.vaultId.eth, + ETH, + 0, + this.exitsToProcess, + web3.utils.keccak256(carol), + { from: carol }, ); }); @@ -173,6 +182,15 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma expect(postBalanceBob).to.be.bignumber.equal(expectedBalance); }); + it('should transfer the exit bounty to the process exit initiator (Carol)', async () => { + const postBalanceCarol = new BN(await web3.eth.getBalance(carol)); + const expectedBalance = this.preBalanceCarol + .add(new BN(this.processExitBountySize)) + .sub(await spentOnGas(this.processTx.receipt)); + + expect(postBalanceCarol).to.be.bignumber.equal(expectedBalance); + }); + it('should publish an event', async () => { await expectEvent.inLogs( this.processTx.logs, @@ -267,7 +285,7 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma await this.exitGame.piggybackInFlightExitOnOutput( args, - { from: bob, value: this.piggybackBondSize }, + { from: bob, value: this.piggybackExitTxValue }, ); }); @@ -305,13 +323,17 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma }; await this.exitGame.piggybackInFlightExitOnInput( - args1, - { from: alice, value: this.piggybackBondSize }, + args1, { + from: alice, + value: this.piggybackBondSize.add(this.processExitBountySize), + }, ); await this.exitGame.piggybackInFlightExitOnInput( - args2, - { from: alice, value: this.piggybackBondSize }, + args2, { + from: alice, + value: this.piggybackBondSize.add(this.processExitBountySize), + }, ); }); @@ -347,8 +369,16 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma await time.increase(slightlyMoreThanTwoWeeks); const exitsToProcess = 1; - await this.framework.processExits( - config.registerKeys.vaultId.eth, ETH, 0, exitsToProcess, + this.preBalanceOtherAddress = new BN( + await web3.eth.getBalance(otherAddress), + ); + this.processTx = await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + exitsToProcess, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, ); }); @@ -365,8 +395,22 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma const postBalanceBob = new BN(await web3.eth.getBalance(bob)); const expectedBalance = preBalanceBob .add(new BN(this.piggybackBondSize)); + expect(expectedBalance).to.be.bignumber.equal(postBalanceBob); }); + + it('should award bounty for both piggybacks to the process exit initiator', async () => { + const postBalanceOtherAddress = new BN( + await web3.eth.getBalance(otherAddress), + ); + const expectedBalance = this.preBalanceOtherAddress + .add(new BN(this.processExitBountySize)) + .add(new BN(this.processExitBountySize)) + .sub(await spentOnGas(this.processTx.receipt)); + + expect(expectedBalance) + .to.be.bignumber.equal(postBalanceOtherAddress); + }); }); }); }); @@ -518,8 +562,10 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma inputIndex: 0, }; await this.exitGame.piggybackInFlightExitOnInput( - piggybackInputArgs, - { from: alice, value: this.piggybackBondSize }, + piggybackInputArgs, { + from: alice, + value: this.piggybackExitTxValue, + }, ); const piggybackOutputArgs = { @@ -527,8 +573,10 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma outputIndex: 0, }; await this.exitGame.piggybackInFlightExitOnOutput( - piggybackOutputArgs, - { from: bob, value: this.piggybackBondSize }, + piggybackOutputArgs, { + from: bob, + value: this.piggybackExitTxValue, + }, ); }); @@ -548,8 +596,16 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma await time.increase(slightlyMoreThanTwoWeeks); const exitsToProcess = 2; - await this.framework.processExits( - config.registerKeys.vaultId.eth, ETH, 0, exitsToProcess, + this.preBalanceOtherAddress = new BN( + await web3.eth.getBalance(otherAddress), + ); + this.processTx = await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + exitsToProcess, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, ); }); @@ -578,6 +634,19 @@ contract('PaymentExitGame - In-flight Exit - End to End Tests', ([_deployer, _ma expect(expectedBalance).to.be.bignumber.equal(postBalanceAlice); }); + + it('should award exit bounty to the process exit initiator', async () => { + const postBalanceOtherAddress = new BN( + await web3.eth.getBalance(otherAddress), + ); + const expectedBalance = this.preBalanceOtherAddress + .add(new BN(this.processExitBountySize)) + .add(new BN(this.processExitBountySize)) + .sub(await spentOnGas(this.processTx.receipt)); + + expect(expectedBalance) + .to.be.bignumber.equal(postBalanceOtherAddress); + }); }); }); }); diff --git a/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js b/plasma_framework/test/endToEndTests/PaymentStandardExit.e2e.test.js index 6ed972982..defeede7c 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,8 @@ 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.processExitBountySize = await this.exitGame.processStandardExitBountySize(); + this.startStandardExitTxValue = this.startStandardExitBondSize.add(this.processExitBountySize); }; const aliceDepositsETH = async () => { @@ -110,7 +112,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 +144,10 @@ 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, + }); }); it('should save the StandardExit data when successfully done', async () => { @@ -183,13 +186,22 @@ 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, + web3.utils.keccak256(bob), { + from: bob, + }, + ); }); it('should return the fund plus standard exit bond to Alice', async () => { @@ -200,6 +212,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 process exit initiator', 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 +239,10 @@ 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, + }); }); it('should start successully', async () => { @@ -238,14 +260,23 @@ 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, + web3.utils.keccak256(bob), + { 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 +298,10 @@ 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, + }); this.exitId = await this.exitGame.getStandardExitId( true, this.depositTx, this.depositUtxoPos, @@ -309,10 +341,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 +353,10 @@ 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, + }), 'Exit has already started.', ); }); @@ -337,6 +371,8 @@ contract('PaymentExitGame - Standard Exit - End to End Tests', ([_deployer, _mai ETH, 0, 1, + web3.utils.keccak256(bob), + { from: bob }, ); this.processExitsReceipt = receipt; }); @@ -398,9 +434,10 @@ 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, + }); }); it('should start successully', async () => { @@ -413,18 +450,21 @@ 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, + web3.utils.keccak256(bob), + { from: bob }, ); }); @@ -437,6 +477,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..e7390d897 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'); @@ -17,7 +15,7 @@ const { buildUtxoPos } = require('../helpers/positions.js'); const Testlang = require('../helpers/testlang.js'); const config = require('../../config.js'); -contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, authority, richFather]) => { +contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, authority, richFather, otherAddress]) => { const ETH = constants.ZERO_ADDRESS; const INITIAL_ERC20_SUPPLY = 10000000000; const DEPOSIT_VALUE = 1000000; @@ -61,6 +59,7 @@ contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, this.piggybackBondSize = await this.exitGame.piggybackBondSize(); this.framework.addExitQueue(config.registerKeys.vaultId.eth, ETH); + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(); }; const aliceDepositsETH = async () => { @@ -107,14 +106,17 @@ 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), + }); }); 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]; @@ -127,7 +129,14 @@ contract('PaymentExitGame - V2 Extension experiment', ([_deployer, _maintainer, this.bobBalanceBeforeProcessExit = new BN(await web3.eth.getBalance(alice)); - await this.framework.processExits(config.registerKeys.vaultId.eth, ETH, 0, 1); + await this.framework.processExits( + config.registerKeys.vaultId.eth, + ETH, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); }); it('should return the output amount plus standard exit bond to Alice', async () => { diff --git a/plasma_framework/test/endToEndTests/StandardExit.load.test.js b/plasma_framework/test/endToEndTests/StandardExit.load.test.js index db8cf3c3f..df369e8a5 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,7 @@ 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.processExitBountySize = await this.exitGame.processStandardExitBountySize(); }; const aliceDepositsETH = async () => { @@ -79,8 +77,12 @@ 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), + }), + ); } await Promise.all([startExits]); }); @@ -88,21 +90,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/PaymentInFlightExitModelUtils.test.js b/plasma_framework/test/src/exits/payment/PaymentInFlightExitModelUtils.test.js index 107f20b14..554052ade 100644 --- a/plasma_framework/test/src/exits/payment/PaymentInFlightExitModelUtils.test.js +++ b/plasma_framework/test/src/exits/payment/PaymentInFlightExitModelUtils.test.js @@ -21,6 +21,7 @@ contract('PaymentInFlightExitModelUtils', () => { token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; describe('isInputEmpty', () => { diff --git a/plasma_framework/test/src/exits/payment/PaymentUpdateExitBounty.test.js b/plasma_framework/test/src/exits/payment/PaymentUpdateExitBounty.test.js new file mode 100644 index 000000000..be3911e3f --- /dev/null +++ b/plasma_framework/test/src/exits/payment/PaymentUpdateExitBounty.test.js @@ -0,0 +1,141 @@ +const PaymentChallengeStandardExit = artifacts.require('PaymentChallengeStandardExit'); +const PaymentProcessStandardExit = artifacts.require('PaymentProcessStandardExit'); +const PaymentStandardExitRouter = artifacts.require('PaymentStandardExitRouterMock'); +const PaymentInFlightExitRouter = artifacts.require('PaymentInFlightExitRouterMock'); +const PaymentStartInFlightExit = artifacts.require('PaymentStartInFlightExit'); +const PaymentPiggybackInFlightExit = artifacts.require('PaymentPiggybackInFlightExit'); +const PaymentProcessInFlightExit = artifacts.require('PaymentProcessInFlightExit'); +const PaymentChallengeIFENotCanonical = artifacts.require('PaymentChallengeIFENotCanonical'); +const PaymentChallengeIFEInputSpent = artifacts.require('PaymentChallengeIFEInputSpent'); +const PaymentChallengeIFEOutputSpent = artifacts.require('PaymentChallengeIFEOutputSpent'); +const PaymentDeleteInFlightExit = artifacts.require('PaymentDeleteInFlightExit'); +const PaymentStartStandardExit = artifacts.require('PaymentStartStandardExit'); +const SpendingConditionRegistry = artifacts.require('SpendingConditionRegistry'); +const StateTransitionVerifierMock = artifacts.require('StateTransitionVerifierMock'); +const SpyPlasmaFramework = artifacts.require('SpyPlasmaFrameworkForExitGame'); +const SpyEthVault = artifacts.require('SpyEthVaultForExitGame'); +const SpyErc20Vault = artifacts.require('SpyErc20VaultForExitGame'); + +const { expect } = require('chai'); +const { expectEvent, time } = require('openzeppelin-test-helpers'); +const { TX_TYPE, VAULT_ID, SAFE_GAS_STIPEND } = require('../../../helpers/constants.js'); + +contract('PaymentExitGame - Update Exit Bounty', () => { + const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week + const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; + const INITIAL_IMMUNE_EXIT_GAME_NUM = 1; + const UPDATE_EXIT_BOUNTY_WAITING_PERIOD = time.duration.days(2); + + before('deploy and link with controller lib', async () => { + const startStandardExit = await PaymentStartStandardExit.new(); + const challengeStandardExit = await PaymentChallengeStandardExit.new(); + const processStandardExit = await PaymentProcessStandardExit.new(); + + await PaymentStandardExitRouter.link('PaymentStartStandardExit', startStandardExit.address); + await PaymentStandardExitRouter.link('PaymentChallengeStandardExit', challengeStandardExit.address); + await PaymentStandardExitRouter.link('PaymentProcessStandardExit', processStandardExit.address); + + const startInFlightExit = await PaymentStartInFlightExit.new(); + const piggybackInFlightExit = await PaymentPiggybackInFlightExit.new(); + const challengeInFlightExitNotCanonical = await PaymentChallengeIFENotCanonical.new(); + const challengeIFEInputSpent = await PaymentChallengeIFEInputSpent.new(); + const challengeIFEOutputSpent = await PaymentChallengeIFEOutputSpent.new(); + const processInFlightExit = await PaymentProcessInFlightExit.new(); + const deleteInFlightExit = await PaymentDeleteInFlightExit.new(); + + await PaymentInFlightExitRouter.link('PaymentStartInFlightExit', startInFlightExit.address); + await PaymentInFlightExitRouter.link('PaymentPiggybackInFlightExit', piggybackInFlightExit.address); + await PaymentInFlightExitRouter.link('PaymentChallengeIFENotCanonical', challengeInFlightExitNotCanonical.address); + await PaymentInFlightExitRouter.link('PaymentChallengeIFEInputSpent', challengeIFEInputSpent.address); + await PaymentInFlightExitRouter.link('PaymentChallengeIFEOutputSpent', challengeIFEOutputSpent.address); + await PaymentInFlightExitRouter.link('PaymentProcessInFlightExit', processInFlightExit.address); + await PaymentInFlightExitRouter.link('PaymentDeleteInFlightExit', deleteInFlightExit.address); + }); + + before('setup framework', async () => { + this.framework = await SpyPlasmaFramework.new( + MIN_EXIT_PERIOD, DUMMY_INITIAL_IMMUNE_VAULTS_NUM, INITIAL_IMMUNE_EXIT_GAME_NUM, + ); + + this.ethVault = await SpyEthVault.new(this.framework.address); + this.erc20Vault = await SpyErc20Vault.new(this.framework.address); + + await this.framework.registerVault(VAULT_ID.ETH, this.ethVault.address); + await this.framework.registerVault(VAULT_ID.ERC20, this.erc20Vault.address); + + this.spendingConditionRegistry = await SpendingConditionRegistry.new(); + + this.stateTransitionVerifier = await StateTransitionVerifierMock.new(); + await this.stateTransitionVerifier.mockResult(true); + }); + + describe('updateProcessStandardExitBountySize', () => { + beforeEach(async () => { + const exitGameArgs = [ + this.framework.address, + VAULT_ID.ETH, + VAULT_ID.ERC20, + this.spendingConditionRegistry.address, + this.stateTransitionVerifier.address, + TX_TYPE.PAYMENT, + SAFE_GAS_STIPEND, + ]; + this.exitGame = await PaymentStandardExitRouter.new(); + await this.exitGame.bootInternal(exitGameArgs); + this.processStandardExitBountySize = await this.exitGame.processStandardExitBountySize(); + this.newExitBountySize = this.processStandardExitBountySize.addn(20); + this.updateTx = await this.exitGame.updateProcessStandardExitBountySize(this.newExitBountySize); + }); + + it('should emit an event when the process standard exit bounty size is updated', async () => { + await expectEvent.inLogs( + this.updateTx.logs, + 'ProcessStandardExitBountyUpdated', + { + exitBountySize: this.newExitBountySize, + }, + ); + }); + + it('should update the exit bounty value after the waiting period has passed', async () => { + await time.increase(UPDATE_EXIT_BOUNTY_WAITING_PERIOD); + const exitBountySize = await this.exitGame.processStandardExitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(this.newExitBountySize); + }); + }); + + describe('updateProcessInFlightExitBountySize', () => { + beforeEach(async () => { + const exitGameArgs = [ + this.framework.address, + VAULT_ID.ETH, + VAULT_ID.ERC20, + this.spendingConditionRegistry.address, + this.stateTransitionVerifier.address, + TX_TYPE.PAYMENT, + SAFE_GAS_STIPEND, + ]; + this.exitGame = await PaymentInFlightExitRouter.new(); + await this.exitGame.bootInternal(exitGameArgs); + this.processInFlightExitBountySize = await this.exitGame.processInFlightExitBountySize(); + this.newExitBountySize = this.processInFlightExitBountySize.addn(20); + this.updateIFEBountyTx = await this.exitGame.updateProcessInFlightExitBountySize(this.newExitBountySize); + }); + + it('should emit an event when the in-flight exit bounty size is updated', async () => { + await expectEvent.inLogs( + this.updateIFEBountyTx.logs, + 'ProcessInFlightExitBountyUpdated', + { + exitBountySize: this.newExitBountySize, + }, + ); + }); + + it('should update the exit bounty value after the waiting period has passed', async () => { + await time.increase(UPDATE_EXIT_BOUNTY_WAITING_PERIOD); + const exitBountySize = await this.exitGame.processInFlightExitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(this.newExitBountySize); + }); + }); +}); diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.test.js index e526c5f8f..2e081e2d0 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEInputSpent.test.js @@ -31,6 +31,7 @@ const { spentOnGas, computeNormalOutputId, getOutputId } = require('../../../../ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, challenger, otherAddress]) => { const DUMMY_IFE_BOND_SIZE = 31415926535; const PIGGYBACK_BOND = 31415926535; + const PROCESS_EXIT_BOUNTY = 500000000000; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; const INITIAL_IMMUNE_EXIT_GAME_NUM = 1; @@ -117,6 +118,7 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const inFlightExitData = { @@ -133,6 +135,7 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c token: ETH, amount: 999, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, { outputId: getOutputId(inputTx.txBytes, inputTx.utxoPos), outputGuard: web3.utils.sha3('dummy output guard'), @@ -140,6 +143,7 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c token: ETH, amount: INPUT_TX_AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, emptyWithdrawData, emptyWithdrawData], outputs: [{ outputId: web3.utils.sha3('dummy output id'), @@ -148,6 +152,7 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c token: ETH, amount: outputAmount, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], }; @@ -200,16 +205,17 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c this.piggybackBondSize = await this.exitGame.piggybackBondSize(); + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + // Set up the piggyback data this.testData = await buildPiggybackInputData(this.inputTx); await this.exitGame.setInFlightExit(this.testData.exitId, this.testData.inFlightExitData); // Piggyback the second input - await this.exitGame.setInFlightExitInputPiggybacked( - this.testData.exitId, - 1, - { from: inputOwner, value: this.piggybackBondSize.toString() }, - ); + await this.exitGame.setInFlightExitInputPiggybacked(this.testData.exitId, 1, { + from: inputOwner, + value: this.piggybackBondSize.add(this.processExitBountySize), + }); // Create a transaction that spends the same input const challengingTx = createInputTransaction( @@ -269,10 +275,11 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c expect(new BN(exits[0].exitMap)).to.be.bignumber.equal(new BN(0)); }); - it('should pay the piggyback bond to the challenger', async () => { + it('should pay the piggyback bond plus exit bounty to the challenger', async () => { const actualPostBalance = new BN(await web3.eth.getBalance(challenger)); const expectedPostBalance = this.challengerPreBalance .add(new BN(PIGGYBACK_BOND)) + .add(new BN(PROCESS_EXIT_BOUNTY)) .sub(await spentOnGas(this.challengeTx.receipt)); expect(actualPostBalance).to.be.bignumber.equal(expectedPostBalance); @@ -282,11 +289,10 @@ contract('PaymentChallengeIFEInputSpent', ([_, alice, inputOwner, outputOwner, c describe('check exitMap before and after challenge', () => { beforeEach(async () => { // Piggyback input0 as well. - await this.exitGame.setInFlightExitInputPiggybacked( - this.testData.exitId, - 0, - { from: inputOwner, value: this.piggybackBondSize.toString() }, - ); + await this.exitGame.setInFlightExitInputPiggybacked(this.testData.exitId, 0, { + from: inputOwner, + value: this.piggybackBondSize.add(this.processExitBountySize), + }); }); it('should remove the input from piggybacked', async () => { diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.test.js index d6d676e89..4e3084ebd 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFENotCanonical.test.js @@ -33,6 +33,7 @@ const { createInclusionProof } = require('../../../../helpers/ife.js'); contract('PaymentChallengeIFENotCanonical', ([_, ifeOwner, inputOwner, outputOwner, competitorOwner, challenger]) => { const DUMMY_IFE_BOND_SIZE = 31415926535; // wei const PIGGYBACK_BOND = 31415926535; // wei + const PROCESS_EXIT_BOUNTY = 500000000000; const CHILD_BLOCK_INTERVAL = 1000; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week in second const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; @@ -137,6 +138,7 @@ contract('PaymentChallengeIFENotCanonical', ([_, ifeOwner, inputOwner, outputOwn token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const output = new PaymentTransactionOutput(outputType, TEST_IFE_OUTPUT_AMOUNT, outputOwner, ETH); @@ -169,6 +171,7 @@ contract('PaymentChallengeIFENotCanonical', ([_, ifeOwner, inputOwner, outputOwn token: ETH, amount: TEST_IFE_INPUT_AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], outputs: [{ outputId: DUMMY_OUTPUT_ID_FOR_OUTPUT, @@ -176,6 +179,7 @@ contract('PaymentChallengeIFENotCanonical', ([_, ifeOwner, inputOwner, outputOwn token: ETH, amount: TEST_IFE_OUTPUT_AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], bondSize: DUMMY_IFE_BOND_SIZE, }; diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.test.js index 8df95874a..cee13a656 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentChallengeIFEOutputSpent.test.js @@ -31,6 +31,7 @@ const { contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { const DUMMY_IFE_BOND_SIZE = 31415926535; // wei const PIGGYBACK_BOND = 31415926535; + const PROCESS_EXIT_BOUNTY = 500000000000; const MIN_EXIT_PERIOD = 60 * 60 * 24 * 7; // 1 week const DUMMY_INITIAL_IMMUNE_VAULTS_NUM = 0; const INITIAL_IMMUNE_EXIT_GAME_NUM = 1; @@ -47,6 +48,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const BLOCK_NUM = 1000; const MAX_NUM_OF_INPUTS = 4; @@ -109,6 +111,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { token: ETH, amount: AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, DUMMY_WITHDRAW_DATA, DUMMY_WITHDRAW_DATA, DUMMY_WITHDRAW_DATA], outputs: [{ outputId: filler, @@ -117,6 +120,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { token: ETH, amount: AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PROCESS_EXIT_BOUNTY, }, { outputId: filler, outputGuard: filler, @@ -124,6 +128,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { token: ETH, amount: AMOUNT, piggybackBondSize: PIGGYBACK_BOND, + bountySize: PIGGYBACK_BOND, }, DUMMY_WITHDRAW_DATA, DUMMY_WITHDRAW_DATA], }; @@ -174,7 +179,12 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { await this.framework.registerExitGame(OTHER_TX_TYPE, dummyExitGame.address, PROTOCOL.MORE_VP); this.piggybackBondSize = await this.exitGame.piggybackBondSize(); - this.exitGame.depositFundForTest({ from: alice, value: this.piggybackBondSize.toString() }); + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + + this.exitGame.depositFundForTest({ + from: alice, + value: this.piggybackBondSize.add(this.processExitBountySize), + }); const args = await buildValidChallengeOutputArgs(); @@ -207,7 +217,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { ); }); - it('should pay out bond to the challenger when challenged successfully', async () => { + it('should pay out bond plus bounty to the challenger when challenged successfully', async () => { const challengerPreBalance = new BN(await web3.eth.getBalance(bob)); const { receipt } = await this.exitGame.challengeInFlightExitOutputSpent( @@ -215,6 +225,7 @@ contract('PaymentChallengeIFEOutputSpent', ([_, alice, bob, otherAddress]) => { ); const expectedPostBalance = challengerPreBalance .add(new BN(PIGGYBACK_BOND)) + .add(new BN(PROCESS_EXIT_BOUNTY)) .sub(await spentOnGas(receipt)); const challengerPostBalance = new BN(await web3.eth.getBalance(bob)); 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..9196af89f 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,46 @@ 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.processExitBountySize = await this.exitGame.processStandardExitBountySize(); }); 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 +164,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 +181,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 +196,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 +209,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 +245,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 +273,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 +297,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/PaymentDeleteInFlightExit.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentDeleteInFlightExit.test.js index 353abf37d..b42907bd2 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentDeleteInFlightExit.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentDeleteInFlightExit.test.js @@ -109,6 +109,7 @@ contract('PaymentDeleteInFlightExit', ([_, bondOwner, inputOwner, outputOwner]) token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const inFlightExitData = { @@ -123,6 +124,7 @@ contract('PaymentDeleteInFlightExit', ([_, bondOwner, inputOwner, outputOwner]) token: ETH, amount: 999, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], outputs: [{ outputId: web3.utils.sha3('dummy output id'), @@ -130,6 +132,7 @@ contract('PaymentDeleteInFlightExit', ([_, bondOwner, inputOwner, outputOwner]) token: ETH, amount: outputAmount, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], bondSize: this.startIFEBondSize.toString(), }; diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnInput.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnInput.test.js index 2671bbc83..d756d0f8e 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnInput.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnInput.test.js @@ -91,6 +91,9 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput this.startIFEBondSize = await this.exitGame.startIFEBondSize(); this.piggybackBondSize = await this.exitGame.piggybackBondSize(); + + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + this.piggybackExitTxValue = this.piggybackBondSize.add(this.processExitBountySize); }); describe('piggybackOnInput', () => { @@ -113,6 +116,7 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const inFlightExitData = { @@ -127,12 +131,14 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput token: firstInputToken, amount: 999, piggybackBondSize: 0, + bountySize: 0, }, { outputId: web3.utils.sha3('dummy output id'), exitTarget: inputOwner, token: ETH, amount: 998, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData], outputs: [{ outputId: web3.utils.sha3('dummy output id'), @@ -140,6 +146,7 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput token: ETH, amount: outputAmount, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], bondSize: this.startIFEBondSize.toString(), }; @@ -164,10 +171,22 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput }; }; - it('should fail when not send with the bond value', async () => { + it('should fail when not sent with the bond value', async () => { const { argsInputOne } = await buildPiggybackInputData(); await expectRevert( - this.exitGame.piggybackInFlightExitOnInput(argsInputOne), + this.exitGame.piggybackInFlightExitOnInput( + argsInputOne, { value: this.processExitBountySize }, + ), + 'Input value must match msg.value', + ); + }); + + it('should fail when not sent with the correct bounty', async () => { + const { argsInputOne } = await buildPiggybackInputData(); + await expectRevert( + this.exitGame.piggybackInFlightExitOnInput( + argsInputOne, { value: this.piggybackBondSize }, + ), 'Input value must match msg.value', ); }); @@ -180,7 +199,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput data.argsInputOne.inFlightTx = nonExistingTx; await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'No in-flight exit to piggyback on', ); @@ -194,7 +216,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'Piggyback is possible only in the first phase of the exit period', ); @@ -208,7 +233,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput data.argsInputOne.inputIndex = inputIndexExceedSize; await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'Invalid input index', ); @@ -222,7 +250,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput data.argsInputOne.inputIndex = indexOfEmptyInput; await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'Indexed input is empty', ); @@ -236,7 +267,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'Indexed input already piggybacked', ); @@ -247,7 +281,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await this.exitGame.setInFlightExit(data.exitId, data.inFlightExitData); await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: nonInputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: nonInputOwner, + value: this.piggybackExitTxValue, + }, ), 'Can be called only by the exit target', ); @@ -258,7 +295,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await this.exitGame.setInFlightExit(data.exitId, data.inFlightExitData); await expectRevert( this.exitGame.piggybackInFlightExitOnInput( - data.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + data.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ), 'There is no block for the exit position to enqueue', ); @@ -277,7 +317,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await this.framework.addExitQueue(VAULT_ID.ETH, ETH); this.piggybackTx = await this.exitGame.piggybackInFlightExitOnInput( - this.testData.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + this.testData.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ); }); @@ -304,7 +347,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput it('should not enqueue when it is not first piggyback of the exit on the same token', async () => { const enqueuedCountBeforePiggyback = (await this.framework.enqueuedCount()).toNumber(); await this.exitGame.piggybackInFlightExitOnInput( - this.testData.argsInputTwo, { from: inputOwner, value: this.piggybackBondSize.toString() }, + this.testData.argsInputTwo, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ); expect((await this.framework.enqueuedCount()).toNumber()).to.equal(enqueuedCountBeforePiggyback); }); @@ -322,6 +368,12 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput expect(new BN(exits[0].inputs[0].piggybackBondSize)).to.be.bignumber.equal(this.piggybackBondSize); }); + it('should set the proper bounty size', async () => { + const exits = await this.exitGame.inFlightExits([this.testData.exitId]); + + expect(new BN(exits[0].inputs[0].bountySize)).to.be.bignumber.equal(this.processExitBountySize); + }); + it('should emit InFlightExitInputPiggybacked event', async () => { await expectEvent.inLogs( this.piggybackTx.logs, @@ -348,7 +400,10 @@ contract('PaymentPiggybackInFlightExitOnInput', ([_, alice, inputOwner, nonInput await this.framework.addExitQueue(VAULT_ID.ERC20, ERC20_TOKEN); this.piggybackTx = await this.exitGame.piggybackInFlightExitOnInput( - this.testData.argsInputOne, { from: inputOwner, value: this.piggybackBondSize.toString() }, + this.testData.argsInputOne, { + from: inputOwner, + value: this.piggybackExitTxValue, + }, ); }); diff --git a/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnOutput.test.js b/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnOutput.test.js index 8521ae06d..f4063c886 100644 --- a/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnOutput.test.js +++ b/plasma_framework/test/src/exits/payment/controllers/PaymentPiggybackInFlightExitOnOutput.test.js @@ -92,6 +92,9 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO this.startIFEBondSize = await this.exitGame.startIFEBondSize(); this.piggybackBondSize = await this.exitGame.piggybackBondSize(); + + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + this.piggybackExitTxValue = this.piggybackBondSize.add(this.processExitBountySize); }); describe('piggybackOnOutput', () => { @@ -114,6 +117,7 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const inFlightExitData = { @@ -128,6 +132,7 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO token: ETH, amount: 999, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData, emptyWithdrawData], outputs: [{ outputId: web3.utils.sha3('dummy output id'), @@ -135,12 +140,14 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO token: ETH, amount: outputAmount1, piggybackBondSize: 0, + bountySize: 0, }, { outputId: web3.utils.sha3('dummy output id'), exitTarget: outputOwner, token: ETH, amount: outputAmount2, piggybackBondSize: 0, + bountySize: 0, }, emptyWithdrawData, emptyWithdrawData], bondSize: this.startIFEBondSize.toString(), }; @@ -174,7 +181,19 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO it('should fail when not send with the bond value', async () => { const data = await buildPiggybackOutputData(); await expectRevert( - this.exitGame.piggybackInFlightExitOnOutput(data.outputOneCase.args), + this.exitGame.piggybackInFlightExitOnOutput( + data.outputOneCase.args, { value: this.processExitBountySize }, + ), + 'Input value must match msg.value', + ); + }); + + it('should fail when not sent with the correct bounty', async () => { + const data = await buildPiggybackOutputData(); + await expectRevert( + this.exitGame.piggybackInFlightExitOnOutput( + data.outputOneCase.args, { value: this.piggybackBondSize }, + ), 'Input value must match msg.value', ); }); @@ -183,7 +202,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO const data = await buildPiggybackOutputData(); await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'No in-flight exit to piggyback on', ); @@ -197,7 +219,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'Piggyback is possible only in the first phase of the exit period', ); @@ -211,7 +236,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO data.outputOneCase.args.outputIndex = MAX_OUTPUT_SIZE + 1; await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'Invalid output index', ); @@ -226,7 +254,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO data.outputOneCase.args.outputIndex = indexOfEmptyOutput; await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'Indexed output is empty', ); @@ -243,7 +274,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'Indexed output already piggybacked', ); @@ -256,7 +290,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ), 'There is no block for the exit position to enqueue', ); @@ -267,7 +304,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO await this.exitGame.setInFlightExit(data.exitId, data.inFlightExitData); await expectRevert( this.exitGame.piggybackInFlightExitOnOutput( - data.outputOneCase.args, { from: nonOutputOwner, value: this.piggybackBondSize.toString() }, + data.outputOneCase.args, { + from: nonOutputOwner, + value: this.piggybackExitTxValue, + }, ), 'Can be called only by the exit target', ); @@ -285,7 +325,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO ); this.piggybackTx = await this.exitGame.piggybackInFlightExitOnOutput( - this.testData.outputOneCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + this.testData.outputOneCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ); }); @@ -312,7 +355,10 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO it('should NOT enqueue with correct data when it is not the first piggyback of the exit on the token', async () => { const originalEnqueuedCount = await this.framework.enqueuedCount(); await this.exitGame.piggybackInFlightExitOnOutput( - this.testData.outputTwoCase.args, { from: outputOwner, value: this.piggybackBondSize.toString() }, + this.testData.outputTwoCase.args, { + from: outputOwner, + value: this.piggybackExitTxValue, + }, ); expect(await this.framework.enqueuedCount()).to.be.bignumber.equal(originalEnqueuedCount); @@ -332,6 +378,12 @@ contract('PaymentPiggybackInFlightExitOnOutput', ([_, alice, inputOwner, outputO expect(new BN(exits[0].outputs[0].piggybackBondSize)).to.be.bignumber.equal(this.piggybackBondSize); }); + it('should set the proper bounty size', async () => { + const exits = await this.exitGame.inFlightExits([this.testData.exitId]); + + expect(new BN(exits[0].outputs[0].bountySize)).to.be.bignumber.equal(this.processExitBountySize); + }); + it('should set the correct exit target to withdraw data on the output of exit data', async () => { const exitData = await this.exitGame.getInFlightExitOutput( this.testData.exitId, this.testData.outputOneCase.args.outputIndex, 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..1cf6521fa 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; @@ -86,6 +86,7 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne token: constants.ZERO_ADDRESS, amount: 0, piggybackBondSize: 0, + bountySize: 0, }; const inFlightExitData = { @@ -101,18 +102,21 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne token: ETH, amount: TEST_INPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, { outputId: TEST_OUTPUT_ID_FOR_INPUT_2, exitTarget: inputOwner2, token: ETH, amount: TEST_INPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, { outputId: TEST_OUTPUT_ID_FOR_INPUT_3, exitTarget: inputOwner3, token: erc20, amount: TEST_INPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, emptyWithdrawData], outputs: [{ outputId: TEST_OUTPUT_ID_FOR_OUTPUT_1, @@ -120,18 +124,21 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne token: ETH, amount: TEST_OUTPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, { outputId: TEST_OUTPUT_ID_FOR_OUTPUT_2, exitTarget: outputOwner2, token: ETH, amount: TEST_OUTPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, { outputId: TEST_OUTPUT_ID_FOR_OUTPUT_3, exitTarget: outputOwner3, token: erc20, amount: TEST_OUTPUT_AMOUNT, piggybackBondSize: this.piggybackBondSize.toString(), + bountySize: this.processExitBountySize.toString(), }, emptyWithdrawData], }; @@ -168,14 +175,17 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne this.startIFEBondSize = await this.exitGame.startIFEBondSize(); this.piggybackBondSize = await this.exitGame.piggybackBondSize(); + this.processExitBountySize = await this.exitGame.processInFlightExitBountySize(); + const maxNeededBond = this.startIFEBondSize.add(this.piggybackBondSize).muln(4); - await this.exitGame.depositFundForTest({ value: maxNeededBond.toString() }); + const totalAmount = maxNeededBond.add(this.processExitBountySize).muln(2); + await this.exitGame.depositFundForTest({ value: totalAmount.toString() }); }); 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 +205,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 +236,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; }; @@ -237,8 +247,10 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should not return piggyback bond', async () => { const postAttackBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); - // only start ife bond was returned - const expectedBalance = this.preAttackBalance.sub(new BN(this.startIFEBondSize)); + // only start ife bond and exit bounty was returned + const expectedBalance = this.preAttackBalance + .sub(new BN(this.startIFEBondSize)) + .sub(new BN(this.processExitBountySize)); expect(postAttackBalance).to.be.bignumber.equal(expectedBalance); }); @@ -262,8 +274,10 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne it('should not return piggyback bond', async () => { const postAttackBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); - // only start ife bond was returned - const expectedBalance = this.preAttackBalance.sub(new BN(this.startIFEBondSize)); + // only start ife bond and exit bounty was returned + const expectedBalance = this.preAttackBalance + .sub(new BN(this.startIFEBondSize)) + .sub(new BN(this.processExitBountySize)); expect(postAttackBalance).to.be.bignumber.equal(expectedBalance); }); @@ -282,6 +296,86 @@ contract('PaymentProcessInFlightExit', ([_, ifeBondOwner, inputOwner1, inputOwne }); }); + describe('When bounty award call failed', () => { + beforeEach(async () => { + this.attacker = await Attacker.new(); + + this.preAttackBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); + }); + describe('on awarding exit bounty', () => { + beforeEach(async () => { + this.exit = await buildInFlightExitData(); + }); + + const setExitAndStartProcessing = async (exitMap, isCanonical) => { + 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, + this.attacker.address, + ); + return receipt; + }; + + describe('when transaction is non-canonical', () => { + beforeEach(async () => { + this.receipt = await setExitAndStartProcessing(2 ** 0, false); + }); + + it('should not return exit bounty', async () => { + const postAttackBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); + // only start ife bond and piggyback bond was returned + const expectedBalance = this.preAttackBalance + .sub(new BN(this.startIFEBondSize)) + .sub(new BN(this.piggybackBondSize)); + expect(postAttackBalance).to.be.bignumber.equal(expectedBalance); + }); + + it('should publish an event that bounty award failed', async () => { + await expectEvent.inTransaction( + this.receipt.transactionHash, + PaymentProcessInFlightExit, + 'InFlightBountyReturnFailed', + { + receiver: this.attacker.address, + amount: new BN(this.processExitBountySize), + }, + ); + }); + }); + + describe('when transaction is canonical', () => { + beforeEach(async () => { + this.receipt = await setExitAndStartProcessing(2 ** MAX_INPUT_NUM, true); + }); + + it('should not return exit bounty', async () => { + const postAttackBalance = new BN(await web3.eth.getBalance(this.exitGame.address)); + // only start ife bond and piggyback bond was returned + const expectedBalance = this.preAttackBalance + .sub(new BN(this.startIFEBondSize)) + .sub(new BN(this.piggybackBondSize)); + expect(postAttackBalance).to.be.bignumber.equal(expectedBalance); + }); + + it('should publish an event that bounty award failed', async () => { + await expectEvent.inTransaction( + this.receipt.transactionHash, + PaymentProcessInFlightExit, + 'InFlightBountyReturnFailed', + { + receiver: this.attacker.address, + amount: new BN(this.processExitBountySize), + }, + ); + }); + }); + }); + }); + describe('When any in-flight exit is processed successfully', () => { beforeEach(async () => { this.ifeBondOwnerPreBalance = new BN(await web3.eth.getBalance(ifeBondOwner)); @@ -296,11 +390,21 @@ 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); + this.preBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ETH, ETH, otherAddress); + }); + + it('should transfer exit bounty to the process exit initiator of all piggybacked inputs/outputs that are cleaned up', async () => { + const postBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + const expectedBalance = this.preBalanceOtherAddress + .add(this.processExitBountySize) + .add(this.processExitBountySize); + + expect(postBalanceOtherAddress).to.be.bignumber.equal(expectedBalance); }); 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 +435,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 +464,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 +475,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 +505,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 +519,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 +539,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 +559,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 +573,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,15 +581,26 @@ 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); expect(postBalance).to.be.bignumber.equal(expectedBalance); }); + it('should return bounty to the process exit initiator for both input and output', async () => { + const preBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); + const postBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + const expectedBalance = preBalanceOtherAddress + .add(this.processExitBountySize) + .add(this.processExitBountySize); + + expect(postBalanceOtherAddress).to.be.bignumber.equal(expectedBalance); + }); + 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 +610,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 +618,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 +648,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 +662,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 +682,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 +702,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 +716,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,15 +724,26 @@ 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); expect(postBalance).to.be.bignumber.equal(expectedBalance); }); + it('should return bounty to the process exit initiator for both input and output', async () => { + const preBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + await this.exitGame.processExit(DUMMY_EXIT_ID, VAULT_ID.ERC20, erc20, otherAddress); + const postBalanceOtherAddress = new BN(await web3.eth.getBalance(otherAddress)); + const expectedBalance = preBalanceOtherAddress + .add(this.processExitBountySize) + .add(this.processExitBountySize); + + expect(postBalanceOtherAddress).to.be.bignumber.equal(expectedBalance); + }); + 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 +752,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 +763,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..146b6c50b 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,12 @@ 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.processExitBountySize = await this.exitGame.processStandardExitBountySize(); + + await this.exitGame.depositFundForTest({ + value: this.startStandardExitBondSize.add(this.processExitBountySize), + }); }); const getTestExitData = (exitable, token, exitTarget = alice) => ({ @@ -79,6 +85,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 +97,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 +119,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 bounty 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, + 'BountyReturnFailed', + { + 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 +175,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 +189,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 +202,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 +213,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 the process exit initiator 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 +243,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 the process exit initiator 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 +284,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 +298,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 +309,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 9ec66f7d4..5f9489e7c 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,9 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { await this.framework.registerExitGame(TX_TYPE.PAYMENT, this.exitGame.address, PROTOCOL.MORE_VP); this.startStandardExitBondSize = await this.exitGame.startStandardExitBondSize(); + + this.processExitBountySize = await this.exitGame.processStandardExitBountySize(); + this.startStandardExitTxValue = this.startStandardExitBondSize.add(this.processExitBountySize); }); it('should fail when the transaction is not standard finalized', async () => { @@ -139,9 +140,10 @@ 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, + }), 'The transaction must be standard finalized', ); }); @@ -153,9 +155,10 @@ 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, + }), 'Output amount must not be 0', ); }); @@ -164,15 +167,19 @@ 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, + }), 'Unsupported transaction type of the exit game', ); }); @@ -181,9 +188,10 @@ 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, + }), 'There is no block for the position', ); }); @@ -195,9 +203,25 @@ 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), + }), + '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), + }), 'Input value must match msg.value', ); }); @@ -208,9 +232,10 @@ 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, + }), 'Only output owner can start an exit', ); }); @@ -220,14 +245,16 @@ 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, + }); await expectRevert( - this.exitGame.startStandardExit( - args, { from: outputOwner, value: this.startStandardExitBondSize }, - ), + this.exitGame.startStandardExit(args, { + from: outputOwner, + value: this.startStandardExitTxValue, + }), 'Exit has already started', ); }); @@ -240,25 +267,28 @@ 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, + }), '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, + }); 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 +300,10 @@ 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, + }); const isTxDeposit = true; const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -293,9 +324,10 @@ 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, + }); const isTxDeposit = false; const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -315,9 +347,10 @@ 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, + }); const isTxDeposit = await this.framework.isDeposit(this.dummyBlockNum); const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); @@ -325,18 +358,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 () => { @@ -348,7 +376,7 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const exitId = await this.exitIdHelper.getStandardExitId(isTxDeposit, args.rlpOutputTx, args.utxoPos); const { receipt } = await this.exitGame.startStandardExit(args, { from: outputOwner, - value: this.startStandardExitBondSize, + value: this.startStandardExitTxValue, }); await expectEvent.inTransaction( @@ -379,7 +407,7 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const { receipt: receipt1 } = await this.exitGame.startStandardExit(args[0], { from: outputOwner, - value: this.startStandardExitBondSize, + value: this.startStandardExitTxValue, }); const exitId1 = await this.exitIdHelper.getStandardExitId( isTxDeposit, args[0].rlpOutputTx, args[0].utxoPos, @@ -398,7 +426,7 @@ contract('PaymentStartStandardExit', ([_, outputOwner, nonOutputOwner]) => { const { receipt: receipt2 } = await this.exitGame.startStandardExit(args[1], { from: outputOwner, - value: this.startStandardExitBondSize, + value: this.startStandardExitTxValue, }); const exitId2 = await this.exitIdHelper.getStandardExitId( isTxDeposit, args[1].rlpOutputTx, args[1].utxoPos, 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..61372ef8d --- /dev/null +++ b/plasma_framework/test/src/exits/utils/BountySize.test.js @@ -0,0 +1,170 @@ +const ExitBountyMock = artifacts.require('ExitBountyMock'); +const { BN, expectRevert, time } = require('openzeppelin-test-helpers'); +const { expect } = require('chai'); + +contract('ExitBounty', () => { + const WAITING_PERIOD = time.duration.days(2); + const HALF_WAITING_PERIOD = WAITING_PERIOD.divn(2); + + describe('with normal cases', () => { + beforeEach(async () => { + this.initialExitBountySize = new BN(20000000000); + this.lowerBoundDivisor = 2; + this.upperBoundMultiplier = 2; + this.contract = await ExitBountyMock.new( + this.initialExitBountySize, + this.lowerBoundDivisor, + this.upperBoundMultiplier, + ); + }); + + it('should return the initial exit bounty size', async () => { + const exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(this.initialExitBountySize); + }); + + it('should be able to update the exit bounty to upperBoundMultiplier times its current value', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).muln(this.upperBoundMultiplier); + await this.contract.updateExitBountySize(newExitBountySize); + }); + + it('should fail to update exit bounty to more than upperBoundMultiplier times its current value', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).muln(this.upperBoundMultiplier).addn(1); + await expectRevert( + this.contract.updateExitBountySize(newExitBountySize), + 'Bounty size is too high', + ); + }); + + it('should be able to update the exit bounty to lowerBoundDivisor of its current value', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).divn(this.lowerBoundDivisor); + await this.contract.updateExitBountySize(newExitBountySize); + }); + + it('should fail to update exit bounty to less than lowerBoundDivisor of its current value', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).divn(this.lowerBoundDivisor).subn(1); + await expectRevert( + this.contract.updateExitBountySize(newExitBountySize), + 'Bounty size is too low', + ); + }); + + it('should not update the actual exit bounty value until after the waiting period', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).muln(2); + await this.contract.updateExitBountySize(newExitBountySize); + + await time.increase(WAITING_PERIOD.sub(time.duration.seconds(5))); + const exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(this.initialExitBountySize); + }); + + it('should update the actual exit bounty value after the waiting period', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).muln(2); + await this.contract.updateExitBountySize(newExitBountySize); + + await time.increase(WAITING_PERIOD); + const exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(newExitBountySize); + }); + + it('should update the actual exit bounty value only after the waiting period', async () => { + const newExitBountySize = new BN(this.initialExitBountySize).muln(2); + await this.contract.updateExitBountySize(newExitBountySize); + + // Wait half the waiting period + await time.increase(HALF_WAITING_PERIOD); + + // Update again while the first update is in progress. + const secondNewExitBountySize = new BN(this.initialExitBountySize).muln(1.5); + await this.contract.updateExitBountySize(secondNewExitBountySize); + + // Wait half the waiting period again. + await time.increase(HALF_WAITING_PERIOD); + // Even though the first update's waiting period is over, the second + // update is in progress so the exit bounty size should not have changed yet. + let exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(this.initialExitBountySize); + + // Wait the remaining waiting period + await time.increase(HALF_WAITING_PERIOD); + exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(secondNewExitBountySize); + }); + + it('should be able to update continuosly', async () => { + const newExitBountySize1 = new BN(this.initialExitBountySize).muln(2); + await this.contract.updateExitBountySize(newExitBountySize1); + await time.increase(WAITING_PERIOD); + + let exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(newExitBountySize1); + + const newExitBountySize2 = newExitBountySize1.muln(2); + await this.contract.updateExitBountySize(newExitBountySize2); + + // Before a full waiting period is finished, it would still not update to latest + await time.increase(HALF_WAITING_PERIOD); + exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize, 'should not update to latest yet').to.be.bignumber.equal(newExitBountySize1); + + // update to latest after full waiting period is done + await time.increase(HALF_WAITING_PERIOD); + exitBountySize = await this.contract.exitBountySize(); + expect(exitBountySize, 'should update to latest').to.be.bignumber.equal(newExitBountySize2); + }); + }); + + describe('with boundary size of numbers', () => { + it('should able to update to max number of uint128 without having overflow issue', async () => { + const maxSizeOfUint128 = (new BN(2)).pow(new BN(128)).sub(new BN(1)); // 2^128 - 1 + + const initialExitBountySize = maxSizeOfUint128.sub(new BN(10000)); + const lowerBoundDivisor = 2; + const upperBoundMultiplier = 2; + const contract = await ExitBountyMock.new( + initialExitBountySize, + lowerBoundDivisor, + upperBoundMultiplier, + ); + + await contract.updateExitBountySize(maxSizeOfUint128); + await time.increase(WAITING_PERIOD); + const exitBountySize = await contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(maxSizeOfUint128); + }); + + it('should be able to update to 1 from 2', async () => { + const initialExitBountySize = new BN(2); + const lowerBoundDivisor = 2; + const upperBoundMultiplier = 2; + const contract = await ExitBountyMock.new( + initialExitBountySize, + lowerBoundDivisor, + upperBoundMultiplier, + ); + const exitBountySizeOne = new BN(1); + + await contract.updateExitBountySize(exitBountySizeOne); + await time.increase(WAITING_PERIOD); + const exitBountySize = await contract.exitBountySize(); + expect(exitBountySize).to.be.bignumber.equal(exitBountySizeOne); + }); + + it('should NOT be able to update to 0', async () => { + const initialExitBountySize = new BN(2); + const lowerBoundDivisor = 2; + const upperBoundMultiplier = 2; + const contract = await ExitBountyMock.new( + initialExitBountySize, + lowerBoundDivisor, + upperBoundMultiplier, + ); + const exitBountySizeZero = new BN(0); + + await expectRevert( + contract.updateExitBountySize(exitBountySizeZero), + 'Bounty size cannot be zero', + ); + }); + }); +}); diff --git a/plasma_framework/test/src/framework/ExitGameController.test.js b/plasma_framework/test/src/framework/ExitGameController.test.js index 24bd4340a..62c933077 100644 --- a/plasma_framework/test/src/framework/ExitGameController.test.js +++ b/plasma_framework/test/src/framework/ExitGameController.test.js @@ -12,7 +12,7 @@ const { buildTxPos } = require('../../helpers/positions.js'); const { PROTOCOL, EMPTY_BYTES_32 } = require('../../helpers/constants.js'); const { exitQueueKey } = require('../../helpers/utils.js'); -contract('ExitGameController', () => { +contract('ExitGameController', ([otherAddress]) => { const MIN_EXIT_PERIOD = 10; const INITIAL_IMMUNE_EXIT_GAMES = 1; const VAULT_ID = 1; @@ -315,18 +315,46 @@ contract('ExitGameController', () => { it('rejects when such token has not been added yet', async () => { const fakeNonAddedTokenAddress = (await DummyExitGame.new()).address; await expectRevert( - this.controller.processExits(VAULT_ID, fakeNonAddedTokenAddress, 0, 1), + this.controller.processExits( + VAULT_ID, + fakeNonAddedTokenAddress, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ), 'The token is not yet added to the Plasma framework', ); }); it('rejects when the exit queue is empty', async () => { await expectRevert( - this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1), + this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ), 'Exit queue is empty', ); }); + it('rejects when senderData is incorrect', async () => { + await expectRevert( + this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(this.controller.address), + { from: otherAddress }, + ), + 'Incorrect SenderData', + ); + }); + it('rejects when the top exit id mismatches with the specified one', async () => { await this.dummyExitGame.enqueue( VAULT_ID, @@ -339,7 +367,14 @@ contract('ExitGameController', () => { const nonExistingExitId = this.dummyExit.exitId - 1; await expectRevert( - this.controller.processExits(VAULT_ID, this.dummyToken, nonExistingExitId, 1), + this.controller.processExits( + VAULT_ID, + this.dummyToken, + nonExistingExitId, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ), 'Top exit ID of the queue is different to the one specified', ); }); @@ -361,7 +396,14 @@ contract('ExitGameController', () => { notAbleToExitYetExit.exitProcessor, ); - const tx = await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + const tx = await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(0), @@ -382,7 +424,14 @@ contract('ExitGameController', () => { }); it('should process the exit when the exitId is set to 0', async () => { - const tx = await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + const tx = await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(1), token: this.dummyToken, @@ -402,9 +451,23 @@ contract('ExitGameController', () => { this.dummyExit.exitProcessor, ); - await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); - const tx = await this.controller.processExits(otherVaultId, this.dummyToken, 0, 1); + const tx = await this.controller.processExits( + otherVaultId, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(1), token: this.dummyToken, @@ -413,7 +476,8 @@ contract('ExitGameController', () => { it('should process the exit when the exitId is set to the exact top of the queue', async () => { const tx = await this.controller.processExits( - VAULT_ID, this.dummyToken, this.dummyExit.exitId, 1, + VAULT_ID, this.dummyToken, this.dummyExit.exitId, 1, web3.utils.keccak256(otherAddress), + { from: otherAddress }, ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(1), @@ -422,7 +486,14 @@ contract('ExitGameController', () => { }); it('should call the "processExit" function of the exit processor when processes', async () => { - const { receipt } = await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + const { receipt } = await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inTransaction( receipt.transactionHash, @@ -435,7 +506,14 @@ contract('ExitGameController', () => { it('should delete the exit data after processed', async () => { const priority = await this.dummyExitGame.priorityFromEnqueue(); - await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); const delegationKey = web3.utils.soliditySha3(priority, VAULT_ID, this.dummyExit.token); const exitProcessor = await this.controller.delegations(delegationKey); @@ -445,7 +523,14 @@ contract('ExitGameController', () => { it('should stop to process when queue becomes empty', async () => { const queueSize = 1; const maxExitsToProcess = 2; - const tx = await this.controller.processExits(VAULT_ID, this.dummyToken, 0, maxExitsToProcess); + const tx = await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + maxExitsToProcess, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(queueSize), @@ -486,7 +571,14 @@ contract('ExitGameController', () => { }); it('should process with the order of priority and delete the processed exit from queue', async () => { - await this.controller.processExits(VAULT_ID, this.dummyToken, 0, 1); + await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); const key = exitQueueKey(VAULT_ID, this.dummyToken); const priorityQueueAddress = await this.controller.exitsQueues(key); @@ -497,7 +589,14 @@ contract('ExitGameController', () => { it('should process no more than the "maxExitsToProcess" limit', async () => { const maxExitsToProcess = 1; - const tx = await this.controller.processExits(VAULT_ID, this.dummyToken, 0, maxExitsToProcess); + const tx = await this.controller.processExits( + VAULT_ID, + this.dummyToken, + 0, + maxExitsToProcess, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ); await expectEvent.inLogs(tx.logs, 'ProcessedExitsNum', { processedNum: new BN(maxExitsToProcess), @@ -635,7 +734,14 @@ contract('ExitGameController', () => { ); await expectRevert( - this.controller.processExits(VAULT_ID, this.dummyExit.token, 0, 1), + this.controller.processExits( + VAULT_ID, + this.dummyExit.token, + 0, + 1, + web3.utils.keccak256(otherAddress), + { from: otherAddress }, + ), 'Reentrant call', ); });