diff --git a/contracts/gateway/EIP20CoGateway.sol b/contracts/gateway/EIP20CoGateway.sol index 8910f67a..872b8ecf 100644 --- a/contracts/gateway/EIP20CoGateway.sol +++ b/contracts/gateway/EIP20CoGateway.sol @@ -546,8 +546,9 @@ contract EIP20CoGateway is GatewayBase { ); MessageBus.Message storage message = messages[_messageHash]; + MessageBus.MessageStatus outboxMessageStatus = + messageBox.outbox[_messageHash]; - redeemer_ = message.sender; redeemAmount_ = redeems[_messageHash].amount; MessageBus.progressOutboxWithProof( @@ -559,9 +560,50 @@ contract EIP20CoGateway is GatewayBase { MessageBus.MessageStatus(_messageStatus) ); + uint256 bountyAmount = redeems[_messageHash].bounty; (redeemer_, redeemAmount_) = progressRedeemInternal(_messageHash, message, true, bytes32(0)); + // Return revert penalty to redeemer if message is already progressed + // and can't be reverted anymore. + tryReturnPenaltyToRedeemer( + address(uint160(redeemer_)), // cast to address payable + outboxMessageStatus, + MessageBus.MessageStatus(_messageStatus), + bountyAmount + ); + } + + /** + * @notice Return the revert penalty to the redeemer. Only valid for + * a message transition from DeclaredRevocation -> Progressed. + * + * @dev Should only be called from progressRedeemWithProof. This function + * exists to avoid a stack too deep error. + * + * @param _redeemer Redeemer address. + * @param _outboxMessageStatus Message status before progressing. + * @param _inboxMessageStatus Message status after progressing. + * @param _bountyAmount Bounty amount to use for calculating penalty. + */ + function tryReturnPenaltyToRedeemer( + address payable _redeemer, + MessageBus.MessageStatus _outboxMessageStatus, + MessageBus.MessageStatus _inboxMessageStatus, + uint256 _bountyAmount + ) + private + { + if (_outboxMessageStatus != MessageBus.MessageStatus.DeclaredRevocation) { + return; + } + if (_inboxMessageStatus != MessageBus.MessageStatus.Progressed) { + return; + } + + // Penalty charged to redeemer for revert redeem. + uint256 penalty = penaltyFromBounty(_bountyAmount); + _redeemer.transfer(penalty); } /** diff --git a/contracts/gateway/EIP20Gateway.sol b/contracts/gateway/EIP20Gateway.sol index d745fd31..d565ab5f 100644 --- a/contracts/gateway/EIP20Gateway.sol +++ b/contracts/gateway/EIP20Gateway.sol @@ -475,6 +475,8 @@ contract EIP20Gateway is GatewayBase { // Get the message object MessageBus.Message storage message = messages[_messageHash]; + MessageBus.MessageStatus outboxMessageStatus = + messageBox.outbox[_messageHash]; MessageBus.progressOutboxWithProof( messageBox, @@ -485,6 +487,7 @@ contract EIP20Gateway is GatewayBase { MessageBus.MessageStatus(_messageStatus) ); + uint256 bountyAmount = stakes[_messageHash].bounty; (staker_, stakeAmount_) = progressStakeInternal( _messageHash, message, @@ -492,6 +495,50 @@ contract EIP20Gateway is GatewayBase { true ); + // Return revert penalty to staker if message is already progressed + // and can't be reverted anymore. + tryReturnPenaltyToStaker( + staker_, + outboxMessageStatus, + MessageBus.MessageStatus(_messageStatus), + bountyAmount + ); + } + + /** + * @notice Return the revert penalty to the staker. Only valid for + * a message transition from DeclaredRevocation -> Progressed. + * + * @dev Should only be called from progressStakeWithProof. This function + * exists to avoid a stack too deep error. + * + * @param _staker Staker address. + * @param _outboxMessageStatus Message status before progressing. + * @param _inboxMessageStatus Message status after progressing. + * @param _bountyAmount Bounty amount to use for calculating penalty. + */ + function tryReturnPenaltyToStaker( + address _staker, + MessageBus.MessageStatus _outboxMessageStatus, + MessageBus.MessageStatus _inboxMessageStatus, + uint256 _bountyAmount + ) + private + { + if (_outboxMessageStatus != MessageBus.MessageStatus.DeclaredRevocation) { + return; + } + if (_inboxMessageStatus != MessageBus.MessageStatus.Progressed) { + return; + } + + // Penalty charged to staker for revert stake. + uint256 penalty = penaltyFromBounty(_bountyAmount); + // transfer the penalty amount + require( + baseToken.transfer(_staker, penalty), + "Penalty amount transfer to staker failed" + ); } /** @@ -1087,7 +1134,7 @@ contract EIP20Gateway is GatewayBase { // Get the staker address staker_ = _message.sender; - //Get the stake amount. + // Get the stake amount. stakeAmount_ = stakes[_messageHash].amount; require( diff --git a/test/gateway/eip20_cogateway/progress_redeem_with_proof.js b/test/gateway/eip20_cogateway/progress_redeem_with_proof.js index 951354a1..166ac69c 100644 --- a/test/gateway/eip20_cogateway/progress_redeem_with_proof.js +++ b/test/gateway/eip20_cogateway/progress_redeem_with_proof.js @@ -34,7 +34,8 @@ const MessageStatusEnum = messageBus.MessageStatusEnum; contract('EIP20CoGateway.progressRedeemWithProof() ', function (accounts) { - let utilityToken, eip20CoGateway, redeemParams, bountyAmount, owner, facilitator; + let utilityToken, eip20CoGateway, redeemParams, bountyAmount, penaltyAmount, owner, facilitator; + const PENALTY_MULTIPLIER = 1.5; let setStorageRoot = async function() { @@ -81,6 +82,7 @@ contract('EIP20CoGateway.progressRedeemWithProof() ', function (accounts) { ); bountyAmount = new BN(proofData.co_gateway.constructor.bounty); + penaltyAmount = bountyAmount.muln(PENALTY_MULTIPLIER); eip20CoGateway = await EIP20CoGateway.new( proofData.co_gateway.constructor.valueToken, @@ -380,6 +382,14 @@ contract('EIP20CoGateway.progressRedeemWithProof() ', function (accounts) { it('should pass when message inbox status at target is progressed and outbox' + ' status at source is revocation declared', async function () { + await web3.eth.sendTransaction( + { + to: eip20CoGateway.address, + from: facilitator, + value: penaltyAmount, + } + ); + await eip20CoGateway.setOutboxStatus( redeemParams.messageHash, MessageStatusEnum.DeclaredRevocation, @@ -521,6 +531,69 @@ contract('EIP20CoGateway.progressRedeemWithProof() ', function (accounts) { }); + it('should return penalty to redeemer when the message status in source is ' + + 'declared revocation and in the target is progressed', async function () { + const facilitator = accounts[8]; + const redeemer = redeemParams.redeemer; + + await web3.eth.sendTransaction( + { + to: eip20CoGateway.address, + from: facilitator, + value: penaltyAmount, + } + ); + + await eip20CoGateway.setOutboxStatus( + redeemParams.messageHash, + MessageStatusEnum.DeclaredRevocation, + ); + + const initialFacilitatorEthBalance = await Utils.getBalance(facilitator); + const initialRedeemerEthBalance = await Utils.getBalance(redeemer); + const initialCoGatewayEthBalance = await Utils.getBalance(eip20CoGateway.address); + + await setStorageRoot(); + + const tx = await eip20CoGateway.progressRedeemWithProof( + redeemParams.messageHash, + proofData.gateway.progress_unstake.proof_data.storageProof[0].serializedProof, + new BN(proofData.gateway.progress_unstake.proof_data.block_number, 16), + MessageStatusEnum.Progressed, + { from: facilitator }, + ); + + const finalFacilitatorEthBalance = await Utils.getBalance(facilitator); + const finalRedeemerEthBalance = await Utils.getBalance(redeemer); + const finalCoGatewayEthBalance = await Utils.getBalance(eip20CoGateway.address); + + const expectedFinalFacilitatorETHBalance = initialFacilitatorEthBalance + .add(bountyAmount) + .subn(tx.receipt.gasUsed); + + const expectedFinalRedeemerETHBalance = initialRedeemerEthBalance + .add(penaltyAmount); + + assert.strictEqual( + finalFacilitatorEthBalance.eq(expectedFinalFacilitatorETHBalance), + true, + `Facilitator's base token balance ${finalFacilitatorEthBalance.toString(10)} should be equal to ${expectedFinalFacilitatorETHBalance.toString(10)}`, + ); + + assert.strictEqual( + finalRedeemerEthBalance.eq(expectedFinalRedeemerETHBalance), + true, + `Redeemer's base token balance ${finalRedeemerEthBalance.toString(10)} should be equal to ${expectedFinalRedeemerETHBalance.toString(10)}`, + ); + + assert.strictEqual( + finalCoGatewayEthBalance.eq(initialCoGatewayEthBalance.sub(bountyAmount).sub(penaltyAmount)), + true, + `CoGateway's base token balance ${finalCoGatewayEthBalance.toString(10)} should be equal to ${initialCoGatewayEthBalance.sub(bountyAmount).sub(penaltyAmount)}.`, + ); + + }); + it('should decrease token supply for utility token', async function () { let initialTotalSupply = await utilityToken.totalSupply.call(); diff --git a/test/gateway/eip20_gateway/progress_stake_with_proof.js b/test/gateway/eip20_gateway/progress_stake_with_proof.js index 6a79f06d..4f62a0bb 100644 --- a/test/gateway/eip20_gateway/progress_stake_with_proof.js +++ b/test/gateway/eip20_gateway/progress_stake_with_proof.js @@ -37,7 +37,8 @@ const MessageStatusEnum = messageBus.MessageStatusEnum; contract('EIP20Gateway.progressStakeWithProof()', function (accounts) { - let gateway, mockToken, baseToken, stakeData, progressStakeParams, bountyAmount; + let gateway, mockToken, baseToken, stakeData, progressStakeParams, bountyAmount, penaltyAmount; + const PENALTY_MULTIPLIER = 1.5; let setStorageRoot = async function() { @@ -123,6 +124,7 @@ contract('EIP20Gateway.progressStakeWithProof()', function (accounts) { let burner = NullAddress; bountyAmount = new BN(proofData.gateway.constructor.bounty); + penaltyAmount = bountyAmount.muln(PENALTY_MULTIPLIER); gateway = await Gateway.new( mockToken.address, @@ -312,6 +314,9 @@ contract('EIP20Gateway.progressStakeWithProof()', function (accounts) { MessageStatusEnum.DeclaredRevocation, ); + // Fund Gateway with enough tokens that it can return penalty. + await baseToken.transfer(gateway.address, penaltyAmount, { from: accounts[0] }); + await setStorageRoot(); let result = await gateway.progressStakeWithProof.call( @@ -542,4 +547,75 @@ contract('EIP20Gateway.progressStakeWithProof()', function (accounts) { }); + it('should return penalty to staker when the message status in source is ' + + 'declared revocation and in the target is progressed', async function () { + + let stakeVault = await gateway.stakeVault.call(); + let caller = accounts[0]; + let staker = stakeData.staker; + + await gateway.setOutboxStatus( + stakeData.messageHash, + MessageStatusEnum.DeclaredRevocation, + ); + + // Fund Gateway with enough tokens that it can return penalty. + await baseToken.transfer(gateway.address, penaltyAmount, { from: accounts[0] }); + + let callerInitialBaseTokenBalance = await baseToken.balanceOf(caller); + let stakerInitialBaseTokenBalance = await baseToken.balanceOf(staker); + let gatewayInitialTokenBalance = await mockToken.balanceOf(gateway.address); + let gatewayInitialBaseTokenBalance = await baseToken.balanceOf(gateway.address); + let stakeVaultInitialTokenBalance = await mockToken.balanceOf(stakeVault); + + await setStorageRoot(); + + await gateway.progressStakeWithProof( + progressStakeParams.messageHash, + proofData.co_gateway.progress_mint.proof_data.storageProof[0].serializedProof, + new BN(proofData.co_gateway.progress_mint.proof_data.block_number), + MessageStatusEnum.Progressed, + ); + + let callerFinalBaseTokenBalance = await baseToken.balanceOf(caller); + let stakerFinalBaseTokenBalance = await baseToken.balanceOf(staker); + let gatewayFinalTokenBalance = await mockToken.balanceOf(gateway.address); + let gatewayFinalBaseTokenBalance = await baseToken.balanceOf(gateway.address); + let stakeVaultFinalTokenBalance = await mockToken.balanceOf(stakeVault); + + assert.strictEqual( + callerFinalBaseTokenBalance.eq(callerInitialBaseTokenBalance.add(bountyAmount)), + true, + "Bounty should be returned to caller.", + ); + + assert.strictEqual( + stakerFinalBaseTokenBalance.eq(stakerInitialBaseTokenBalance.add(penaltyAmount)), + true, + `Staker's base token balance ${stakerFinalBaseTokenBalance.toString(10)} must be equal to ${stakerInitialBaseTokenBalance.add(penaltyAmount).toString(10)}`, + ); + + assert.strictEqual( + gatewayFinalTokenBalance.eq(gatewayInitialTokenBalance.sub(stakeData.amount)), + true, + "Gateway token balance should reduced by stake amount on successful " + + "progress stake.", + ); + + assert.strictEqual( + gatewayFinalBaseTokenBalance.eq( + gatewayInitialBaseTokenBalance.sub(bountyAmount).sub(penaltyAmount) + ), + true, + `Gateway's base token balance ${gatewayFinalBaseTokenBalance.toString(10)} must be equal to ${gatewayInitialBaseTokenBalance.sub(bountyAmount).sub(penaltyAmount).toString(10)}.`, + ); + + assert.strictEqual( + stakeVaultFinalTokenBalance.eq(stakeVaultInitialTokenBalance.add(stakeData.amount)), + true, + "Stake vault token balance should increase by stake amount on " + + "successful progress stake.", + ); + + }); });