diff --git a/contracts/gateway/RedeemPool.sol b/contracts/gateway/RedeemPool.sol index 42ec45e1..0e68217a 100644 --- a/contracts/gateway/RedeemPool.sol +++ b/contracts/gateway/RedeemPool.sol @@ -361,7 +361,7 @@ contract RedeemPool is Organized, Mutex { RedeemerProxy redeemerProxy = redeemerProxies[msg.sender]; require( address(redeemerProxy) != address(0), - "Redeem proxy does not exist for the caller." + "Redeemer proxy does not exist for the caller." ); // Resetting the proxy address of the redeemer. delete redeemerProxies[msg.sender]; diff --git a/contracts/gateway/RedeemerProxy.sol b/contracts/gateway/RedeemerProxy.sol index 8652807a..4be106ff 100644 --- a/contracts/gateway/RedeemerProxy.sol +++ b/contracts/gateway/RedeemerProxy.sol @@ -162,7 +162,7 @@ contract RedeemerProxy is Mutex { { require( address(_token) != address(0), - "The token address may not be address zero." + "The token address must not be address zero." ); require( _token.transfer(_to, _value), diff --git a/contracts/test/gateway/MockRedeemerProxy.sol b/contracts/test/gateway/MockRedeemerProxy.sol new file mode 100644 index 00000000..ff8ffdeb --- /dev/null +++ b/contracts/test/gateway/MockRedeemerProxy.sol @@ -0,0 +1,48 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +/** + * @title MockRedeemerProxy is a contract used for unit testing of RedeemPool + * method `destructStakerProxy`. + */ +contract MockRedeemerProxy { + + /* Storage */ + + /** Flag to assert if self destruct is called. */ + bool public selfDestruted; + + + /* Special Functions */ + + constructor () public { + selfDestruted = false; + } + + /** + * @notice Mock method called from redeem pool during unit testing of + * `destructRedeemerProxy`. + */ + function selfDestruct() external { + selfDestruted = true; + } +} diff --git a/contracts/test/gateway/SpyEIP20CoGateway.sol b/contracts/test/gateway/SpyEIP20CoGateway.sol new file mode 100644 index 00000000..a34e933f --- /dev/null +++ b/contracts/test/gateway/SpyEIP20CoGateway.sol @@ -0,0 +1,90 @@ +pragma solidity ^0.5.0; + +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +import "./SpyToken.sol"; + +/** + * @title A test double co-gateway where you can check sent values. + * + * @notice Use this spy if you need to investigate which values were sent to + * the co-gateway. + */ +contract SpyEIP20CoGateway { + + SpyToken public utilityToken; + uint256 public expectedNonce = 1; + uint256 public bounty = 100; + uint256 public amount; + address public beneficiary; + uint256 public gasPrice; + uint256 public gasLimit; + uint256 public nonce; + bytes32 public hashLock; + + constructor() public { + utilityToken = new SpyToken(); + } + + /** + * This method is used for testing. It returns fix nonce. + */ + function getNonce(address) external view returns(uint256) { + return expectedNonce; + } + + /** + * @notice Used for testing of redeem feature. This method spy on co-gateway redeem. + * + * + * @param _amount Redeem amount that will be transferred from redeemer + * account. + * @param _beneficiary The address in the origin chain where the value + * tok ens will be released. + * @param _gasPrice Gas price that redeemer is ready to pay to get the + * redeem process done. + * @param _gasLimit Gas limit that redeemer is ready to pay. + * @param _nonce Nonce of the redeemer address. + * @param _hashLock Hash Lock provided by the facilitator. + * + * @return messageHash_ Hash of message. + */ + function redeem( + uint256 _amount, + address _beneficiary, + uint256 _gasPrice, + uint256 _gasLimit, + uint256 _nonce, + bytes32 _hashLock + ) + external + payable + returns(bytes32) + { + amount = _amount; + beneficiary = _beneficiary; + gasPrice = _gasPrice; + gasLimit = _gasLimit; + nonce = _nonce; + hashLock = _hashLock; + return bytes32('1'); + } +} diff --git a/test/gateway/redeem_pool/accept_redeem_request.js b/test/gateway/redeem_pool/accept_redeem_request.js new file mode 100644 index 00000000..9dfc2aad --- /dev/null +++ b/test/gateway/redeem_pool/accept_redeem_request.js @@ -0,0 +1,276 @@ +const RedeemPool = artifacts.require('RedeemPool'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const MockOrganization = artifacts.require('MockOrganization'); +const SpyToken = artifacts.require('SpyToken'); +const BN = require('bn.js'); +const web3 = require('../../test_lib/web3'); +const Utils = require('../../test_lib/utils'); + +contract('RedeemPool.acceptRedeem()', (accounts) => { + let eip20CoGateway; + let redeemPool; + let worker; + let redeemRequest; + let utilityToken; + let bounty; + + beforeEach(async () => { + worker = accounts[2]; + + eip20CoGateway = await EIP20CoGateway.new(); + bounty = await eip20CoGateway.bounty.call(); + const organization = await MockOrganization.new(accounts[1], accounts[2]); + redeemPool = await RedeemPool.new(organization.address); + + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + hashLock: web3.utils.sha3('1'), + }; + + const token = await eip20CoGateway.utilityToken.call(); + utilityToken = await SpyToken.at(token); + }); + + it('should be able to accept redeem', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ); + }); + + it('should transfer token to redeemer proxy', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + await redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ); + const redeemerProxy = await redeemPool.redeemerProxies.call(redeemRequest.redeemer); + const toAddress = await utilityToken.toAddress.call(); + const transferAmount = await utilityToken.transferAmount.call(); + + assert.strictEqual( + toAddress, + redeemerProxy, + 'to address for transfer from must match', + ); + assert.strictEqual( + new BN(transferAmount).eq(redeemRequest.amount), + true, + 'Transfer from amount must match', + ); + }); + + it('should transfer bounty to redeemer proxy', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + const initialBalance = await web3.eth.getBalance(redeemPool.address); + await redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ); + + const finalBalance = await web3.eth.getBalance(redeemPool.address); + + assert.strictEqual( + initialBalance, + finalBalance, + 'Initial base token balance must be equal to final balance as bounty is' + + ' transferred', + ); + }); + + it('should be able to request redeem again after successful accept redeem', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + const oldRedeemerProxy = await redeemPool.redeemerProxies.call(redeemRequest.redeemer); + await redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ); + + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + const newRedeemerProxy = await redeemPool.redeemerProxies.call(redeemRequest.redeemer); + + assert.strictEqual( + oldRedeemerProxy, + newRedeemerProxy, + 'Redeemer proxy should be deployed once', + ); + }); + + it('should fail if redeemer proxy does not exists', async () => { + await Utils.expectRevert(redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ), + 'RedeemerProxy address is null.'); + }); + + it('should fail if there is no open request redeem', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + await redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ); + await Utils.expectRevert(redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ), + 'Redeem request must exists.'); + }); + + it('should fail if the token transfer fails from redeem pool to redeemer' + + ' proxy', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await utilityToken.setTransferFakeResponse(false); + + await Utils.expectRevert(redeemPool.acceptRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + redeemRequest.hashLock, + { + from: worker, + value: bounty, + }, + ), + 'Redeem amount must be transferred to the redeem proxy.'); + }); +}); diff --git a/test/gateway/redeem_pool/destruct_redeemer_proxy.js b/test/gateway/redeem_pool/destruct_redeemer_proxy.js new file mode 100644 index 00000000..1e27d661 --- /dev/null +++ b/test/gateway/redeem_pool/destruct_redeemer_proxy.js @@ -0,0 +1,91 @@ +// Copyright 2019 OpenST Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// ---------------------------------------------------------------------------- +// +// http://www.simpletoken.org/ +// +// ---------------------------------------------------------------------------- + +const RedeemPool = artifacts.require('RedeemPool'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const MockOrganization = artifacts.require('MockOrganization'); +const BN = require('bn.js'); +const Utils = require('../../test_lib/utils'); +const web3 = require('../../test_lib/web3'); + +contract('RedeemPool.destructRedeemerProxy()', (accounts) => { + let eip20CoGateway; + let redeemPool; + let worker; + let redeemRequest; + beforeEach(async () => { + worker = accounts[2]; + eip20CoGateway = await EIP20CoGateway.new(); + const organization = await MockOrganization.new(accounts[1], accounts[2]); + redeemPool = await RedeemPool.new(organization.address); + + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + }; + + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + }); + + it('should be able to successfully destruct redeemer proxy', async () => { + const response = await redeemPool.destructRedeemerProxy( + { from: redeemRequest.redeemer }, + ); + + assert.strictEqual(response.receipt.status, true, 'Receipt status is unsuccessful'); + + const redeemerProxyAddress = await redeemPool.redeemerProxies.call(redeemRequest.redeemer); + assert.strictEqual( + redeemerProxyAddress, + Utils.NULL_ADDRESS, + 'Redeemer proxy contract shouldnot exists for redeemer', + ); + + const code = await web3.eth.getCode(redeemerProxyAddress); + assert.strictEqual( + code, + '0x', + 'Redeemer proxy contract should be self distructed', + ); + }); + + it('should fail when owner doesn\'t have any deployed staker proxy', async () => { + const nonProxy = accounts[8]; + await Utils.expectRevert( + redeemPool.destructRedeemerProxy( + { from: nonProxy }, + ), + 'Redeemer proxy does not exist for the caller.', + ); + }); +}); diff --git a/test/gateway/redeem_pool/reject_redeem_request.js b/test/gateway/redeem_pool/reject_redeem_request.js new file mode 100644 index 00000000..10ac28c4 --- /dev/null +++ b/test/gateway/redeem_pool/reject_redeem_request.js @@ -0,0 +1,190 @@ +const RedeemPool = artifacts.require('RedeemPool'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const MockOrganization = artifacts.require('MockOrganization'); +const SpyToken = artifacts.require('SpyToken'); +const BN = require('bn.js'); +const web3 = require('../../test_lib/web3'); +const Utils = require('../../test_lib/utils'); +const EventDecoder = require('../../test_lib/event_decoder.js'); + +contract('RedeemPool.rejectRedeemRequest()', (accounts) => { + let eip20CoGateway; + let redeemPool; + let worker; + let redeemRequest; + let utilityToken; + let bounty; + + beforeEach(async () => { + worker = accounts[2]; + + eip20CoGateway = await EIP20CoGateway.new(); + bounty = await eip20CoGateway.bounty.call(); + const organization = await MockOrganization.new(accounts[1], accounts[2]); + redeemPool = await RedeemPool.new(organization.address); + + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + hashLock: web3.utils.sha3('1'), + }; + + const token = await eip20CoGateway.utilityToken.call(); + utilityToken = await SpyToken.at(token); + }); + + it('should be able to reject redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.rejectRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + { from: worker }, + ); + }); + + it('should emit event', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const expectedHash = await redeemPool.redeemRequestHashes.call( + redeemRequest.redeemer, + redeemRequest.cogateway, + ); + + const result = await redeemPool.rejectRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + { from: worker }, + ); + + const events = EventDecoder.getEvents(result, redeemPool); + + const eventData = events.RedeemRejected; + + assert.strictEqual( + eventData.redeemer, + redeemRequest.redeemer, + 'Redeemer address must match', + ); + + assert.strictEqual( + eventData.redeemRequestHash, + expectedHash, + 'Redeem request hash must match', + ); + }); + it('should transfer token to redeemer', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.rejectRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + { from: worker }, + ); + + const toAddress = await utilityToken.toAddress.call(); + const transferAmount = await utilityToken.transferAmount.call(); + + assert.strictEqual( + toAddress, + redeemRequest.redeemer, + 'to address for transfer from must match', + ); + assert.strictEqual( + new BN(transferAmount).eq(redeemRequest.amount), + true, + 'Transfer from amount must match', + ); + }); + + it('should allow new redeem request after reject redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.rejectRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + { from: worker }, + ); + + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + }); + + it('should fail reject redeem request if no open redeem request', async () => { + await Utils.expectRevert(redeemPool.rejectRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.redeemer, + redeemRequest.cogateway, + { from: worker }, + ), + 'Redeem request must exists.'); + }); +}); diff --git a/test/gateway/redeem_pool/request_redeem.js b/test/gateway/redeem_pool/request_redeem.js new file mode 100644 index 00000000..512fb1a3 --- /dev/null +++ b/test/gateway/redeem_pool/request_redeem.js @@ -0,0 +1,212 @@ +const RedeemPool = artifacts.require('RedeemPool'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const MockOrganization = artifacts.require('MockOrganization'); +const SpyToken = artifacts.require('SpyToken'); +const BN = require('bn.js'); +const Utils = require('../../test_lib/utils'); +const EventDecoder = require('../../test_lib/event_decoder.js'); + +contract('RedeemPool.requestRedeem()', (accounts) => { + let eip20CoGateway; + let redeemPool; + let worker; + let redeemRequest; + let utilityToken; + + beforeEach(async () => { + worker = accounts[2]; + eip20CoGateway = await EIP20CoGateway.new(); + const organization = await MockOrganization.new(accounts[1], accounts[2]); + redeemPool = await RedeemPool.new(organization.address); + + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + }; + + const token = await eip20CoGateway.utilityToken.call(); + utilityToken = await SpyToken.at(token); + }); + + it('should be able to request redeem', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + }); + + it('should emit event of redeem request', async () => { + const result = await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const expectedHash = await redeemPool.redeemRequestHashes.call( + redeemRequest.redeemer, + redeemRequest.cogateway, + ); + const events = EventDecoder.getEvents(result, redeemPool); + + const eventData = events.RedeemRequested; + + assert.strictEqual( + eventData.redeemer, + redeemRequest.redeemer, + 'Invalid redeemer address', + ); + assert.strictEqual( + new BN(eventData.amount).eq(redeemRequest.amount), + true, + `Expected redeem amount is ${redeemRequest.amount} but got ${eventData.amount}`, + ); + assert.strictEqual( + eventData.gasLimit.eq(redeemRequest.gasLimit), + true, + `Expected gasLimit amount is ${redeemRequest.gasLimit} but got ${eventData.gasLimit}`, + ); + assert.strictEqual( + eventData.gasPrice.eq(redeemRequest.gasPrice), + true, + `Expected gasPrice amount is ${redeemRequest.gasPrice} but got ${eventData.gasPrice}`, + ); + assert.strictEqual( + eventData.cogateway, + redeemRequest.cogateway, + 'Invalid cogateway address', + ); + assert.strictEqual( + eventData.beneficiary, + redeemRequest.beneficiary, + 'Invalid beneficiary address', + ); + assert.strictEqual( + eventData.nonce.eq(redeemRequest.nonce), + true, + `Expected nonce amount is ${redeemRequest.nonce.toString(10)} but got ${eventData.nonce.toString(10)}`, + ); + const redeemerProxy = await redeemPool.redeemerProxies.call(redeemRequest.redeemer); + assert.strictEqual( + eventData.redeemerProxy, + redeemerProxy, + 'Invalid reedemer proxy address', + ); + + assert.strictEqual( + eventData.redeemRequestHash, + expectedHash, + 'Redeem request hash must match', + ); + }); + + it('should transfer token to redeem pool on successful redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const fromAddress = await utilityToken.fromAddress.call(); + const toAddress = await utilityToken.toAddress.call(); + const transferAmount = await utilityToken.transferAmount.call(); + + assert.strictEqual( + fromAddress, + redeemRequest.redeemer, + 'From address for transfer from must match', + ); + assert.strictEqual( + toAddress, + redeemPool.address, + 'to address for transfer from must match', + ); + assert.strictEqual( + new BN(transferAmount).eq(redeemRequest.amount), + true, + 'Transfer from amount must match', + ); + }); + + it('should fail if tokens cannot be transferred from redeemer to redeem' + + ' pool', async () => { + await utilityToken.setTransferFromFakeResponse(false); + + await Utils.expectRevert(redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ), + 'Utility token transfer returned false.'); + }); + + it('should fail if there is existing redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await Utils.expectRevert(redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ), + 'Request for this redeemer at this co-gateway is already in process.'); + }); + + it('should fail for zero redeem amount', async () => { + await Utils.expectRevert(redeemPool.requestRedeem( + 0, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ), + 'Redeem amount must not be zero.'); + }); + + it('should fail if cogateway nonce does not match', async () => { + await Utils.expectRevert(redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + '100', + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ), + 'Incorrect redeemer nonce.'); + }); +}); diff --git a/test/gateway/redeem_pool/revoke_redeem_request.js b/test/gateway/redeem_pool/revoke_redeem_request.js new file mode 100644 index 00000000..e8162da6 --- /dev/null +++ b/test/gateway/redeem_pool/revoke_redeem_request.js @@ -0,0 +1,185 @@ +const RedeemPool = artifacts.require('RedeemPool'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const MockOrganization = artifacts.require('MockOrganization'); +const SpyToken = artifacts.require('SpyToken'); +const BN = require('bn.js'); +const web3 = require('../../test_lib/web3'); +const Utils = require('../../test_lib/utils'); +const EventDecoder = require('../../test_lib/event_decoder.js'); + +contract('RedeemPool.revokeRedeemRequest()', (accounts) => { + let eip20CoGateway; + let redeemPool; + let worker; + let redeemRequest; + let utilityToken; + let bounty; + + beforeEach(async () => { + worker = accounts[2]; + + eip20CoGateway = await EIP20CoGateway.new(); + bounty = await eip20CoGateway.bounty.call(); + const organization = await MockOrganization.new(accounts[1], accounts[2]); + redeemPool = await RedeemPool.new(organization.address); + + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + hashLock: web3.utils.sha3('1'), + }; + + const token = await eip20CoGateway.utilityToken.call(); + utilityToken = await SpyToken.at(token); + }); + + it('should be able to revoke redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.revokeRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + }); + + it('should emit event', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const expectedHash = await redeemPool.redeemRequestHashes.call( + redeemRequest.redeemer, + redeemRequest.cogateway, + ); + + const result = await redeemPool.revokeRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const events = EventDecoder.getEvents(result, redeemPool); + + const eventData = events.RedeemRevoked; + + assert.strictEqual( + eventData.redeemer, + redeemRequest.redeemer, + 'Redeemer address must match', + ); + + assert.strictEqual( + eventData.redeemRequestHash, + expectedHash, + 'Redeem request hash must match', + ); + }); + it('should transfer token to redeemer', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.revokeRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + const toAddress = await utilityToken.toAddress.call(); + const transferAmount = await utilityToken.transferAmount.call(); + + assert.strictEqual( + toAddress, + redeemRequest.redeemer, + 'to address for transfer from must match', + ); + assert.strictEqual( + new BN(transferAmount).eq(redeemRequest.amount), + true, + 'Transfer from amount must match', + ); + }); + + it('should allow new redeem request after revoke redeem request', async () => { + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.revokeRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + + await redeemPool.requestRedeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ); + }); + + it('should fail revoke redeem request if no open redeem request', async () => { + await Utils.expectRevert(redeemPool.revokeRedeemRequest( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.cogateway, + { from: redeemRequest.redeemer }, + ), + 'Redeem request must exists.'); + }); +}); diff --git a/test/gateway/redeem_proxy/constructor.js b/test/gateway/redeem_proxy/constructor.js new file mode 100644 index 00000000..fd3f716f --- /dev/null +++ b/test/gateway/redeem_proxy/constructor.js @@ -0,0 +1,24 @@ +const RedeemerProxy = artifacts.require('RedeemerProxy'); + +contract('RedeemerProxy.constructor()', (accounts) => { + it('should construct successfully', async () => { + const redeemPool = accounts[1]; + const owner = accounts[2]; + const proxy = await RedeemerProxy.new(owner, { from: redeemPool }); + + const actualComposer = await proxy.redeemPool.call(); + const actualOwner = await proxy.owner.call(); + + assert.strictEqual( + actualComposer, + redeemPool, + 'Redeem Pool address must match', + ); + + assert.strictEqual( + actualOwner, + owner, + 'Owner address must match', + ); + }); +}); diff --git a/test/gateway/redeem_proxy/redeem.js b/test/gateway/redeem_proxy/redeem.js new file mode 100644 index 00000000..63a1eccf --- /dev/null +++ b/test/gateway/redeem_proxy/redeem.js @@ -0,0 +1,175 @@ +const RedeemerProxy = artifacts.require('RedeemerProxy'); +const EIP20CoGateway = artifacts.require('SpyEIP20CoGateway'); +const SpyToken = artifacts.require('SpyToken'); + +const BN = require('bn.js'); +const web3 = require('../../test_lib/web3'); +const Utils = require('../../test_lib/utils'); + +contract('RedeemerProxy.redeem()', (accounts) => { + let redeemPool; + let owner; + let proxy; + let eip20CoGateway; + let redeemRequest; + let bounty; + let utilityToken; + + beforeEach(async () => { + redeemPool = accounts[1]; + owner = accounts[2]; + eip20CoGateway = await EIP20CoGateway.new(); + proxy = await RedeemerProxy.new(owner, { from: redeemPool }); + bounty = await eip20CoGateway.bounty.call(); + redeemRequest = { + amount: new BN('100'), + beneficiary: accounts[3], + gasPrice: new BN('1'), + gasLimit: new BN('2'), + nonce: new BN('1'), + cogateway: eip20CoGateway.address, + redeemer: accounts[4], + hashLock: web3.utils.sha3('1'), + }; + const token = await eip20CoGateway.utilityToken.call(); + utilityToken = await SpyToken.at(token); + }); + + it('should successfully redeem', async () => { + await proxy.redeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.hashLock, + redeemRequest.cogateway, + { from: redeemPool, value: bounty }, + ); + }); + + it('should approve co-gateway for redeem amount', async () => { + await proxy.redeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.hashLock, + redeemRequest.cogateway, + { from: redeemPool, value: bounty }, + ); + + const approveTo = await utilityToken.approveTo.call(); + const approveFrom = await utilityToken.approveFrom.call(); + const amount = await utilityToken.approveAmount.call(); + + assert.strictEqual( + approveTo, + redeemRequest.cogateway, + 'Spender address must be co-gateway', + ); + + assert.strictEqual( + approveFrom, + proxy.address, + 'Approve from address must be redeemer proxy', + ); + + assert.strictEqual( + new BN(amount).eq(redeemRequest.amount), + true, + `Spend amount ${amount} must be equal to redeem amount ${redeemRequest.amount}`, + ); + }); + + it('should transfer bounty to co-gateway', async () => { + const cogatewayInitialBalance = await web3.eth.getBalance(redeemRequest.cogateway); + await proxy.redeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.hashLock, + redeemRequest.cogateway, + { from: redeemPool, value: bounty }, + ); + const cogatewayFinalBalance = await web3.eth.getBalance(redeemRequest.cogateway); + + const expectedCogatewaybalance = new BN(cogatewayInitialBalance).add(new BN(bounty)); + assert.strictEqual( + new BN(cogatewayFinalBalance).eq(expectedCogatewaybalance), + true, + `Cogateway final balance must be ${expectedCogatewaybalance} insteead of ${cogatewayFinalBalance} `, + ); + }); + + it('should call cogateway redeem with correct param', async () => { + await proxy.redeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.hashLock, + redeemRequest.cogateway, + { from: redeemPool, value: bounty }, + ); + + const amount = await eip20CoGateway.amount.call(); + const beneficiary = await eip20CoGateway.beneficiary.call(); + const gasPrice = await eip20CoGateway.gasPrice.call(); + const gasLimit = await eip20CoGateway.gasLimit.call(); + const nonce = await eip20CoGateway.nonce.call(); + const hashLock = await eip20CoGateway.hashLock.call(); + + assert.strictEqual( + new BN(amount).eq(redeemRequest.amount), + true, + `Redeem amount should be ${redeemRequest.amount.toString(10)} instead of ${amount.toString(10)}`, + ); + assert.strictEqual( + new BN(gasPrice).eq(redeemRequest.gasPrice), + true, + `Gas price should be ${redeemRequest.gasPrice.toString(10)} instead of ${gasPrice.toString(10)}`, + ); + assert.strictEqual( + new BN(gasLimit).eq(redeemRequest.gasLimit), + true, + `Gas limit should be ${redeemRequest.gasLimit.toString(10)} instead of ${gasLimit.toString(10)}`, + ); + assert.strictEqual( + new BN(nonce).eq(redeemRequest.nonce), + true, + `Nonc should be ${redeemRequest.nonce.toString(10)} instead of ${nonce.toString(10)}`, + ); + assert.strictEqual( + hashLock, + redeemRequest.hashLock, + `Hashlock should be ${redeemRequest.hashLock} instead of ${hashLock}`, + ); + assert.strictEqual( + beneficiary, + redeemRequest.beneficiary, + `Beneficiary should be ${redeemRequest.beneficiary} instead of ${beneficiary}`, + ); + }); + + it('should fail if non redeemPool address request redeems', async () => { + const nonRedeemPoolAddress = accounts[6]; + await Utils.expectRevert( + proxy.redeem( + redeemRequest.amount, + redeemRequest.beneficiary, + redeemRequest.gasPrice, + redeemRequest.gasLimit, + redeemRequest.nonce, + redeemRequest.hashLock, + redeemRequest.cogateway, + { from: nonRedeemPoolAddress, value: bounty }, + ), + 'This function can only be called by the Redeem Pool.', + ); + }); +}); diff --git a/test/gateway/redeem_proxy/self_destruct.js b/test/gateway/redeem_proxy/self_destruct.js new file mode 100644 index 00000000..8eaafbce --- /dev/null +++ b/test/gateway/redeem_proxy/self_destruct.js @@ -0,0 +1,44 @@ +const RedeemerProxy = artifacts.require('RedeemerProxy'); + +const web3 = require('../../test_lib/web3'); +const Utils = require('../../test_lib/utils'); + +contract('RedeemerProxy.selfDestruct()', (accounts) => { + let redeemPool; + let owner; + let proxy; + + beforeEach(async () => { + redeemPool = accounts[1]; + owner = accounts[2]; + proxy = await RedeemerProxy.new(owner, { from: redeemPool }); + }); + + it('should successfully self destruct', async () => { + const codeBeforeSelfDestruct = await web3.eth.getCode(proxy.address); + + await proxy.selfDestruct({ from: redeemPool }); + + const codeAfterSelfDestruct = await web3.eth.getCode(proxy.address); + + assert.strictEqual( + codeAfterSelfDestruct, + '0x', + 'Contract must be self destructed', + ); + + assert.strictEqual( + codeBeforeSelfDestruct.length > 2, + true, + `Contract must have deployed byte code but found ${codeBeforeSelfDestruct}`, + ); + }); + + it('should fail to self destruct for non redeemPool address', async () => { + const nonComposerAddress = accounts[6]; + await Utils.expectRevert( + proxy.selfDestruct({ from: nonComposerAddress }), + 'This function can only be called by the Redeem Pool.', + ); + }); +}); diff --git a/test/gateway/redeem_proxy/transfer_token.js b/test/gateway/redeem_proxy/transfer_token.js new file mode 100644 index 00000000..9a92e64b --- /dev/null +++ b/test/gateway/redeem_proxy/transfer_token.js @@ -0,0 +1,63 @@ +const RedeemerProxy = artifacts.require('RedeemerProxy'); +const SpyToken = artifacts.require('SpyToken'); + +const BN = require('bn.js'); +const Utils = require('../../test_lib/utils'); + +contract('RedeemerProxy.transferToken()', (accounts) => { + let composer; + let owner; + let proxy; + let token; + const beneficiary = accounts[3]; + const amount = new BN('100'); + + beforeEach(async () => { + composer = accounts[1]; + owner = accounts[2]; + proxy = await RedeemerProxy.new(owner, { from: composer }); + token = await SpyToken.new(); + }); + + it('should successfully transfer token', async () => { + await proxy.transferToken(token.address, beneficiary, amount, { from: owner }); + + const toAddress = await token.toAddress.call(); + const transferAmount = await token.transferAmount.call(); + + assert.strictEqual( + toAddress, + beneficiary, + 'To address must match', + ); + + assert.strictEqual( + amount.eq(new BN(transferAmount)), + true, + `Transfer amount should be ${amount} instead of ${transferAmount}`, + ); + }); + + it('should fail to transferToken for non owner address', async () => { + const nonOwnerAddress = accounts[6]; + await Utils.expectRevert( + proxy.transferToken(token.address, beneficiary, amount, { from: nonOwnerAddress }), + 'This function can only be called by the owner.', + ); + }); + + it('should fail if EIP20 transfer fails', async () => { + await token.setTransferFakeResponse(false); + await Utils.expectRevert( + proxy.transferToken(token.address, beneficiary, amount, { from: owner }), + 'EIP20Token transfer returned false.', + ); + }); + + it('should fail for zero token address', async () => { + await Utils.expectRevert( + proxy.transferToken(Utils.NULL_ADDRESS, beneficiary, amount, { from: owner }), + 'The token address must not be address zero.', + ); + }); +});