From 42a3b6b9d58728433b7d7e98466819d56c5612ac Mon Sep 17 00:00:00 2001 From: Artem Pikulin Date: Fri, 28 Dec 2018 17:23:01 +0700 Subject: [PATCH] #17 Use symmetric swap protocol. --- contracts/Alice.sol | 121 ---------- contracts/Bob.sol | 243 ------------------- contracts/EtomicSwap.sol | 139 +++++++++++ test/Alice.js | 245 ------------------- test/Bob.js | 497 --------------------------------------- test/EtomicSwap.js | 276 ++++++++++++++++++++++ 6 files changed, 415 insertions(+), 1106 deletions(-) delete mode 100644 contracts/Alice.sol delete mode 100644 contracts/Bob.sol create mode 100644 contracts/EtomicSwap.sol delete mode 100644 test/Alice.js delete mode 100644 test/Bob.js create mode 100644 test/EtomicSwap.js diff --git a/contracts/Alice.sol b/contracts/Alice.sol deleted file mode 100644 index 323dcd6..0000000 --- a/contracts/Alice.sol +++ /dev/null @@ -1,121 +0,0 @@ -pragma solidity ^0.5.0; -import 'openzeppelin-solidity/contracts/token/ERC20/ERC20.sol'; - -contract Alice { - enum DealState { - Uninitialized, - Initialized, - PaymentSentToBob, - PaymentSentToAlice - } - - struct Deal { - bytes20 dealHash; - DealState state; - } - - mapping (bytes32 => Deal) public deals; - - constructor() public { } - - function initEthDeal( - bytes32 _dealId, - address _bob, - bytes20 _aliceHash, - bytes20 _bobHash - ) external payable { - require(_bob != address(0) && msg.value > 0 && deals[_dealId].state == DealState.Uninitialized); - bytes20 dealHash = ripemd160(abi.encodePacked( - msg.sender, - _aliceHash, - _bob, - _bobHash, - msg.value, - address(0) - )); - deals[_dealId] = Deal( - dealHash, - DealState.Initialized - ); - } - - function initErc20Deal( - bytes32 _dealId, - uint _amount, - address _bob, - bytes20 _aliceHash, - bytes20 _bobHash, - address _tokenAddress - ) external { - require(_bob != address(0) && _tokenAddress != address(0) && _amount > 0 && deals[_dealId].state == DealState.Uninitialized); - bytes20 dealHash = ripemd160(abi.encodePacked( - msg.sender, - _aliceHash, - _bob, - _bobHash, - _amount, - _tokenAddress - )); - deals[_dealId] = Deal( - dealHash, - DealState.Initialized - ); - ERC20 token = ERC20(_tokenAddress); - assert(token.transferFrom(msg.sender, address(this), _amount)); - } - - function aliceClaimsPayment( - bytes32 _dealId, - uint _amount, - address _tokenAddress, - address _bob, - bytes20 _aliceHash, - bytes calldata _bobSecret - ) external { - require(deals[_dealId].state == DealState.Initialized); - bytes20 dealHash = ripemd160(abi.encodePacked( - msg.sender, - _aliceHash, - _bob, - ripemd160(abi.encodePacked(sha256(abi.encodePacked(_bobSecret)))), - _amount, - _tokenAddress - )); - require(dealHash == deals[_dealId].dealHash); - - deals[_dealId].state = DealState.PaymentSentToAlice; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } - - function bobClaimsPayment( - bytes32 _dealId, - uint _amount, - address _tokenAddress, - address _alice, - bytes20 _bobHash, - bytes calldata _aliceSecret - ) external { - require(deals[_dealId].state == DealState.Initialized); - bytes20 dealHash = ripemd160(abi.encodePacked( - _alice, - ripemd160(abi.encodePacked(sha256(abi.encodePacked(_aliceSecret)))), - msg.sender, - _bobHash, - _amount, - _tokenAddress - )); - require(dealHash == deals[_dealId].dealHash); - deals[_dealId].state = DealState.PaymentSentToBob; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } -} diff --git a/contracts/Bob.sol b/contracts/Bob.sol deleted file mode 100644 index 39f7387..0000000 --- a/contracts/Bob.sol +++ /dev/null @@ -1,243 +0,0 @@ -pragma solidity ^0.5.0; -import 'openzeppelin-solidity/contracts/math/SafeMath.sol'; -import 'openzeppelin-solidity/contracts/token/ERC20/ERC20.sol'; - -contract Bob { - using SafeMath for uint; - - enum DepositState { - Uninitialized, - BobMadeDeposit, - AliceClaimedDeposit, - BobClaimedDeposit - } - - enum PaymentState { - Uninitialized, - BobMadePayment, - AliceClaimedPayment, - BobClaimedPayment - } - - struct BobDeposit { - bytes20 depositHash; - uint64 lockTime; - DepositState state; - } - - struct BobPayment { - bytes20 paymentHash; - uint64 lockTime; - PaymentState state; - } - - mapping (bytes32 => BobDeposit) public deposits; - - mapping (bytes32 => BobPayment) public payments; - - constructor() public { } - - function bobMakesEthDeposit( - bytes32 _txId, - address _alice, - bytes20 _bobHash, - bytes20 _aliceHash, - uint64 _lockTime - ) external payable { - require(_alice != address(0) && msg.value > 0 && deposits[_txId].state == DepositState.Uninitialized); - bytes20 depositHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - _bobHash, - _aliceHash, - address(0), - msg.value - )); - deposits[_txId] = BobDeposit( - depositHash, - _lockTime, - DepositState.BobMadeDeposit - ); - } - - function bobMakesErc20Deposit( - bytes32 _txId, - uint256 _amount, - address _alice, - bytes20 _bobHash, - bytes20 _aliceHash, - address _tokenAddress, - uint64 _lockTime - ) external { - bytes20 depositHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - _bobHash, - _aliceHash, - _tokenAddress, - _amount - )); - deposits[_txId] = BobDeposit( - depositHash, - _lockTime, - DepositState.BobMadeDeposit - ); - ERC20 token = ERC20(_tokenAddress); - assert(token.transferFrom(msg.sender, address(this), _amount)); - } - - function bobClaimsDeposit( - bytes32 _txId, - uint256 _amount, - bytes32 _bobSecret, - bytes20 _aliceHash, - address _alice, - address _tokenAddress - ) external { - require(deposits[_txId].state == DepositState.BobMadeDeposit); - bytes20 depositHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - ripemd160(abi.encodePacked(sha256(abi.encodePacked(_bobSecret)))), - _aliceHash, - _tokenAddress, - _amount - )); - require(depositHash == deposits[_txId].depositHash && now < deposits[_txId].lockTime); - deposits[_txId].state = DepositState.BobClaimedDeposit; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } - - function aliceClaimsDeposit( - bytes32 _txId, - uint256 _amount, - bytes32 _aliceSecret, - address _bob, - address _tokenAddress, - bytes20 _bobHash - ) external { - require(deposits[_txId].state == DepositState.BobMadeDeposit); - bytes20 depositHash = ripemd160(abi.encodePacked( - msg.sender, - _bob, - _bobHash, - ripemd160(abi.encodePacked(sha256(abi.encodePacked(_aliceSecret)))), - _tokenAddress, - _amount - )); - require(depositHash == deposits[_txId].depositHash && now >= deposits[_txId].lockTime); - deposits[_txId].state = DepositState.AliceClaimedDeposit; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } - - function bobMakesEthPayment( - bytes32 _txId, - address _alice, - bytes20 _secretHash, - uint64 _lockTime - ) external payable { - require(_alice != address(0) && msg.value > 0 && payments[_txId].state == PaymentState.Uninitialized); - bytes20 paymentHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - _secretHash, - address(0), - msg.value - )); - payments[_txId] = BobPayment( - paymentHash, - _lockTime, - PaymentState.BobMadePayment - ); - } - - function bobMakesErc20Payment( - bytes32 _txId, - uint256 _amount, - address _alice, - bytes20 _secretHash, - address _tokenAddress, - uint64 _lockTime - ) external { - require( - _alice != address(0) && - _amount > 0 && - payments[_txId].state == PaymentState.Uninitialized && - _tokenAddress != address(0) - ); - bytes20 paymentHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - _secretHash, - _tokenAddress, - _amount - )); - payments[_txId] = BobPayment( - paymentHash, - _lockTime, - PaymentState.BobMadePayment - ); - ERC20 token = ERC20(_tokenAddress); - assert(token.transferFrom(msg.sender, address(this), _amount)); - } - - function bobClaimsPayment( - bytes32 _txId, - uint256 _amount, - address _alice, - address _tokenAddress, - bytes20 _secretHash - ) external { - require(payments[_txId].state == PaymentState.BobMadePayment); - bytes20 paymentHash = ripemd160(abi.encodePacked( - _alice, - msg.sender, - _secretHash, - _tokenAddress, - _amount - )); - require(now >= payments[_txId].lockTime && paymentHash == payments[_txId].paymentHash); - payments[_txId].state = PaymentState.BobClaimedPayment; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } - - function aliceClaimsPayment( - bytes32 _txId, - uint256 _amount, - bytes32 _secret, - address _bob, - address _tokenAddress - ) external { - require(payments[_txId].state == PaymentState.BobMadePayment); - bytes20 paymentHash = ripemd160(abi.encodePacked( - msg.sender, - _bob, - ripemd160(abi.encodePacked(sha256(abi.encodePacked(_secret)))), - _tokenAddress, - _amount - )); - require(now < payments[_txId].lockTime && paymentHash == payments[_txId].paymentHash); - payments[_txId].state = PaymentState.AliceClaimedPayment; - if (_tokenAddress == address(0)) { - msg.sender.transfer(_amount); - } else { - ERC20 token = ERC20(_tokenAddress); - assert(token.transfer(msg.sender, _amount)); - } - } -} diff --git a/contracts/EtomicSwap.sol b/contracts/EtomicSwap.sol new file mode 100644 index 0000000..4884347 --- /dev/null +++ b/contracts/EtomicSwap.sol @@ -0,0 +1,139 @@ +pragma solidity ^0.5.0; +import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol"; + +contract EtomicSwap { + enum PaymentState { + Uninitialized, + PaymentSent, + ReceivedSpent, + SenderRefunded + } + + struct Payment { + bytes20 paymentHash; + uint64 lockTime; + PaymentState state; + } + + mapping (bytes32 => Payment) public payments; + + event PaymentSent(bytes32 id); + event ReceiverSpent(bytes32 id, bytes32 secret); + event SenderRefunded(bytes32 id); + + constructor() public { } + + function ethPayment( + bytes32 _id, + address _receiver, + bytes20 _secretHash, + uint64 _lockTime + ) external payable { + require(_receiver != address(0) && msg.value > 0 && payments[_id].state == PaymentState.Uninitialized); + + bytes20 paymentHash = ripemd160(abi.encodePacked( + _receiver, + msg.sender, + _secretHash, + address(0), + msg.value + )); + + payments[_id] = Payment( + paymentHash, + _lockTime, + PaymentState.PaymentSent + ); + + emit PaymentSent(_id); + } + + function erc20Payment( + bytes32 _id, + uint256 _amount, + address _tokenAddress, + address _receiver, + bytes20 _secretHash, + uint64 _lockTime + ) external payable { + require(_receiver != address(0) && _amount > 0 && payments[_id].state == PaymentState.Uninitialized); + + bytes20 paymentHash = ripemd160(abi.encodePacked( + _receiver, + msg.sender, + _secretHash, + _tokenAddress, + _amount + )); + + payments[_id] = Payment( + paymentHash, + _lockTime, + PaymentState.PaymentSent + ); + + IERC20 token = IERC20(_tokenAddress); + require(token.transferFrom(msg.sender, address(this), _amount)); + emit PaymentSent(_id); + } + + function receiverSpend( + bytes32 _id, + uint256 _amount, + bytes32 _secret, + address _tokenAddress, + address _sender + ) external { + require(payments[_id].state == PaymentState.PaymentSent); + + bytes20 paymentHash = ripemd160(abi.encodePacked( + msg.sender, + _sender, + ripemd160(abi.encodePacked(sha256(abi.encodePacked(_secret)))), + _tokenAddress, + _amount + )); + + require(paymentHash == payments[_id].paymentHash && now < payments[_id].lockTime); + payments[_id].state = PaymentState.ReceivedSpent; + if (_tokenAddress == address(0)) { + msg.sender.transfer(_amount); + } else { + IERC20 token = IERC20(_tokenAddress); + require(token.transfer(msg.sender, _amount)); + } + + emit ReceiverSpent(_id, _secret); + } + + function senderRefund( + bytes32 _id, + uint256 _amount, + bytes20 _paymentHash, + address _tokenAddress, + address _receiver + ) external { + require(payments[_id].state == PaymentState.PaymentSent); + + bytes20 paymentHash = ripemd160(abi.encodePacked( + _receiver, + msg.sender, + _paymentHash, + _tokenAddress, + _amount + )); + + require(paymentHash == payments[_id].paymentHash && now >= payments[_id].lockTime); + + payments[_id].state = PaymentState.SenderRefunded; + + if (_tokenAddress == address(0)) { + msg.sender.transfer(_amount); + } else { + IERC20 token = IERC20(_tokenAddress); + require(token.transfer(msg.sender, _amount)); + } + + emit SenderRefunded(_id); + } +} diff --git a/test/Alice.js b/test/Alice.js deleted file mode 100644 index f1cdbea..0000000 --- a/test/Alice.js +++ /dev/null @@ -1,245 +0,0 @@ -const Alice = artifacts.require('Alice'); -const Token = artifacts.require('Token'); -const crypto = require('crypto'); -const RIPEMD160 = require('ripemd160'); - -const EVMThrow = 'VM Exception while processing transaction'; - -require('chai') - .use(require('chai-as-promised')) - .should(); - -const [ DEAL_UNINITIALIZED, DEAL_INITIALIZED, DEAL_PAYMENT_SENT_TO_BOB, DEAL_PAYMENT_SENT_TO_ALICE] = [0, 1, 2, 3]; - -const aliceSecret = crypto.randomBytes(32); -const aliceHash = '0x' + new RIPEMD160().update(crypto.createHash('sha256').update(aliceSecret).digest()).digest('hex'); -const bobSecret = crypto.randomBytes(32); -const bobHash = '0x' + new RIPEMD160().update(crypto.createHash('sha256').update(bobSecret).digest()).digest('hex'); -const dealId = '0x' + crypto.randomBytes(32).toString('hex'); -const aliceSecretHex = '0x' + aliceSecret.toString('hex'); -const bobSecretHex = '0x' + bobSecret.toString('hex'); - -const zeroAddr = '0x0000000000000000000000000000000000000000'; - -contract('Alice', function(accounts) { - - beforeEach(async function () { - this.alice = await Alice.new(); - this.token = await Token.new(); - this.wrongToken = await Token.new(); - await this.token.transfer(accounts[1], web3.utils.toWei('100')); - }); - - it('Should create contract with not initialized deals', async function () { - const deal = await this.alice.deals(dealId); - assert.equal(deal[1].valueOf(), DEAL_UNINITIALIZED); - }); - - it('Should allow to init ETH deal', async function () { - const initParams = [ - dealId, - accounts[1], - aliceHash, - bobHash - ]; - await this.alice.initEthDeal(...initParams, { value: web3.utils.toWei('1') }).should.be.fulfilled; - const deal = await this.alice.deals(dealId); - - assert.equal(deal[1].valueOf(), DEAL_INITIALIZED); - - // should not allow to init again - await this.alice.initEthDeal(...initParams, { value: 1 }).should.be.rejectedWith(EVMThrow); - }); - - it('Should allow to init using ERC20 token', async function () { - const initParams = [ - dealId, - web3.utils.toWei('1'), - accounts[1], - aliceHash, - bobHash, - this.token.address - ]; - - await this.token.approve(this.alice.address, web3.utils.toWei('1'), { from: accounts[0] }); - await this.alice.initErc20Deal(...initParams).should.be.fulfilled; - - const deal = await this.alice.deals(dealId); - // initialized status - assert.equal(deal[1].valueOf(), DEAL_INITIALIZED); - - // check Alice contract token balance - const balance = await this.token.balanceOf(this.alice.address); - assert.equal(balance.toString(), web3.utils.toWei('1')); - - // should not allow to init again - await this.alice.initErc20Deal(...initParams).should.be.rejectedWith(EVMThrow); - }); - - it('Should allow Bob to claim and receive ETH payment using Alice secret', async function () { - const initParams = [ - dealId, - accounts[1], - aliceHash, - bobHash - ]; - - // should not allow to claim payment from uninitialized deal - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // init - await this.alice.initEthDeal(...initParams, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[0], bobHash, bobSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim wrong amount - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('2'), zeroAddr, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from address not equal to bob - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[0], bobHash, aliceSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[0], bobHash, aliceSecretHex, { from: accounts[1], gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check receiver balance - correct amount should be sent - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const deal = await this.alice.deals(dealId); - // check payment sent status - assert.equal(deal[1].valueOf(), DEAL_PAYMENT_SENT_TO_BOB); - - // should not allow to claim again - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow);; - }); - - it('Should allow Bob to claim and receive ERC20 payment using Alice secret', async function () { - const initParams = [ - dealId, - web3.utils.toWei('1'), - accounts[1], - aliceHash, - bobHash, - this.token.address - ]; - - // should not allow to claim payment from uninitialized deal - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // init - await this.token.approve(this.alice.address, web3.utils.toWei('1'), { from: accounts[0] }); - await this.alice.initErc20Deal(...initParams).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[0], bobHash, bobSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from address not equal to bob - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[0], bobHash, aliceSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim invalid amount - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('2'), this.token.address, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[1]); - - // check bob balance - correct amount should be sent - assert.equal(balanceAfter.toString(), web3.utils.toWei('101')); - - const deal = await this.alice.deals(dealId); - // check payment sent status - assert.equal(deal[1].valueOf(), DEAL_PAYMENT_SENT_TO_BOB); - - // should not allow to claim again - await this.alice.bobClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[0], bobHash, aliceSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow);; - }); - - it('Should allow Alice claim ETH payment using Bob secret', async function () { - const initParams = [ - dealId, - accounts[1], - aliceHash, - bobHash - ]; - - // should not allow to claim from uninitialized deal - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // init - await this.alice.initEthDeal(...initParams, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[1], aliceHash, aliceSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from non-Alice address - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[1], aliceHash, bobSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim wrong amount - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('2'), zeroAddr, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[1], aliceHash, bobSecretHex, { from: accounts[0], gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check initiator balance - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const deal = await this.alice.deals(dealId); - // check payment sent status - assert.equal(deal[1].valueOf(), DEAL_PAYMENT_SENT_TO_ALICE); - - // should not allow to claim again - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), zeroAddr, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - }); - - it('Should allow Alice to claim payment ERC20 using Bob secret', async function () { - const initParams = [ - dealId, - web3.utils.toWei('1'), - accounts[1], - aliceHash, - bobHash, - this.token.address - ]; - - // should not allow to claim from uninitialized deal - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // init - await this.token.approve(this.alice.address, web3.utils.toWei('1'), { from: accounts[0] }); - await this.alice.initErc20Deal(...initParams).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[1], aliceHash, aliceSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from non-Alice address - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim wrong amount - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('2'), this.token.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim wrong token - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.wrongToken.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[0]); - - const deal = await this.alice.deals(dealId); - // check confirmed status - assert.equal(deal[1].valueOf(), DEAL_PAYMENT_SENT_TO_ALICE); - - // check initiator balance - assert.equal(balanceAfter.toString(), web3.utils.toWei('900')); - - // should not allow to claim again - await this.alice.aliceClaimsPayment(dealId, web3.utils.toWei('1'), this.token.address, accounts[1], aliceHash, bobSecretHex, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - }); -}); diff --git a/test/Bob.js b/test/Bob.js deleted file mode 100644 index 1c7db5c..0000000 --- a/test/Bob.js +++ /dev/null @@ -1,497 +0,0 @@ -const Bob = artifacts.require('Bob'); -const Token = artifacts.require('Token'); -const crypto = require('crypto'); -const RIPEMD160 = require('ripemd160'); - -const EVMThrow = 'VM Exception while processing transaction'; - -require('chai') - .use(require('chai-as-promised')) - .should(); - -async function increaseTime (increaseAmount) { - await web3.currentProvider.send({ - jsonrpc: '2.0', - method: 'evm_increaseTime', - id: Date.now(), - params: [increaseAmount] - }, function () { - - }); -} - -async function currentEvmTime() { - const block = await web3.eth.getBlock("latest"); - return block.timestamp; -} - -const txId = '0x' + crypto.randomBytes(32).toString('hex'); -const [DEPOSIT_UNINITIALIZED, BOB_MADE_DEPOSIT, ALICE_CLAIMED_DEPOSIT, BOB_CLAIMED_DEPOSIT] = [0, 1, 2, 3]; -const [PAYMENT_UNINITIALIZED, BOB_MADE_PAYMENT, ALICE_CLAIMED_PAYMENT, BOB_CLAIMED_PAYMENT] = [0, 1, 2, 3]; - -const secret = crypto.randomBytes(32); -const secretHash = '0x' + new RIPEMD160().update(crypto.createHash('sha256').update(secret).digest()).digest('hex'); -const secretHex = '0x' + secret.toString('hex'); - -const zeroAddr = '0x0000000000000000000000000000000000000000'; - -contract('Bob', function(accounts) { - - beforeEach(async function () { - this.bob = await Bob.new(); - this.token = await Token.new(); - await this.token.transfer(accounts[1], web3.utils.toWei('100')); - }); - - it('should create contract with uninitialized deposits and payments', async function () { - const deposit = await this.bob.deposits(txId); - assert.equal(deposit[2].valueOf(), DEPOSIT_UNINITIALIZED); - const payment = await this.bob.payments(txId); - assert.equal(payment[2].valueOf(), PAYMENT_UNINITIALIZED); - }); - - it('should allow Bob to make ETH deposit', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - secretHash, - lockTime - ]; - await this.bob.bobMakesEthDeposit(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - const deposit = await this.bob.deposits(txId); - - // locktime - assert.equal(deposit[1].valueOf(), lockTime); - // status - assert.equal(deposit[2].valueOf(), BOB_MADE_DEPOSIT); - - // should not allow to deposit again - await this.bob.bobMakesEthDeposit(...params, { value: web3.utils.toWei('1') }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to make ERC20 deposit', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - secretHash, - this.token.address, - lockTime - ]; - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - await this.bob.bobMakesErc20Deposit(...params).should.be.fulfilled; - - //check contract token balance - const balance = await this.token.balanceOf(this.bob.address); - assert.equal(balance.toString(), web3.utils.toWei('1')); - - const deposit = await this.bob.deposits(txId); - - // locktime - assert.equal(deposit[1].valueOf(), lockTime); - // status - assert.equal(deposit[2].valueOf(), BOB_MADE_DEPOSIT); - - // should not allow to deposit again - await this.bob.bobMakesErc20Deposit(...params).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to claim ETH deposit by revealing the secret', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - secretHash, - lockTime - ]; - - // not allow to claim if deposit was not sent - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], zeroAddr).should.be.rejectedWith(EVMThrow); - - const depositTx = await this.bob.bobMakesEthDeposit(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // not allow to claim with invalid secret - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), txId, secretHash, accounts[1], zeroAddr).should.be.rejectedWith(EVMThrow); - // not allow to claim wrong value - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('2'), secretHex, secretHash, accounts[1], zeroAddr).should.be.rejectedWith(EVMThrow); - // not allow to claim from not Bob address - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // success claim - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], zeroAddr, { gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check bob balance - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const deposit = await this.bob.deposits(txId); - - // status - assert.equal(deposit[2].valueOf(), BOB_CLAIMED_DEPOSIT); - - // should not allow to claim again - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], zeroAddr).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to claim ERC20 deposit by revealing the secret', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - secretHash, - this.token.address, - lockTime - ]; - - // not allow to claim if deposit was not sent - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], this.token.address).should.be.rejectedWith(EVMThrow); - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - const depositTx = await this.bob.bobMakesErc20Deposit(...params).should.be.fulfilled; - - // not allow to claim with invalid secret - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), txId, secretHash, accounts[1], this.token.address).should.be.rejectedWith(EVMThrow); - // not allow to claim wrong value - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('2'), secretHex, secretHash, accounts[1], this.token.address).should.be.rejectedWith(EVMThrow); - // not allow to claim from not Bob address - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], this.token.address, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // success claim - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], this.token.address).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[0]); - - // check bob balance - assert.equal(balanceAfter.toString(), web3.utils.toWei('900')); - - const deposit = await this.bob.deposits(txId); - - // status - assert.equal(deposit[2].valueOf(), BOB_CLAIMED_DEPOSIT); - - // should not allow to claim again - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], this.token.address).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Alice to claim ETH deposit after locktime expires', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - secretHash, - lockTime - ]; - - // not allow to claim if deposit not sent - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await this.bob.bobMakesEthDeposit(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // not allow to claim before lock expires - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await await increaseTime(1000); - // not allow bob to claim even by revealing correct secret - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], zeroAddr, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // not allow to claim from incorrect address - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, secretHash, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // success claim - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, secretHash, { from: accounts[1], gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check alice balance - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const deposit = await this.bob.deposits(txId); - - // status - assert.equal(deposit[2].valueOf(), ALICE_CLAIMED_DEPOSIT); - - // should not allow to claim again - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Alice to claim ERC deposit after locktime expires', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - secretHash, - this.token.address, - lockTime - ]; - - // not allow to claim if deposit not sent - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - await this.bob.bobMakesErc20Deposit(...params).should.be.fulfilled; - - // not allow to claim before timelock expires - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await await increaseTime(1000); - // not allow bob to claim even by revealing correct secret - await this.bob.bobClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, secretHash, accounts[1], this.token.address,{ from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // not allow to claim from incorrect address - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, secretHash, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // success claim - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, secretHash, { from: accounts[1] }).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[1]); - - // check alice balance - assert.equal(balanceAfter.toString(), web3.utils.toWei('101')); - - const deposit = await this.bob.deposits(txId); - - // status - assert.equal(deposit[2].valueOf(), ALICE_CLAIMED_DEPOSIT); - - // should not allow to claim again - await this.bob.aliceClaimsDeposit(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, secretHash, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to make ETH payment', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - lockTime - ]; - await this.bob.bobMakesEthPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - const payment = await this.bob.payments(txId); - - // locktime - assert.equal(payment[1].valueOf(), lockTime); - // status - assert.equal(payment[2].valueOf(), BOB_MADE_PAYMENT); - - // should not allow to send payment again - await this.bob.bobMakesEthPayment(...params, { value: web3.utils.toWei('1') }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to make ERC20 payment', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - this.token.address, - lockTime - ]; - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - await this.bob.bobMakesErc20Payment(...params).should.be.fulfilled; - - //check contract token balance - const balance = await this.token.balanceOf(this.bob.address); - assert.equal(balance.toString(), web3.utils.toWei('1')); - - const payment = await this.bob.payments(txId); - - // locktime - assert.equal(payment[1].valueOf(), lockTime); - // status - assert.equal(payment[2].valueOf(), BOB_MADE_PAYMENT); - - // should not allow to send payment again - await this.bob.bobMakesErc20Payment(...params).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Alice to claim ETH payment by revealing a secret', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - lockTime - ]; - - // should not allow to claim from uninitialized payment - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await this.bob.bobMakesEthPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), txId, accounts[0], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - // should not allow to claim invalid amount - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('2'), secretHex, accounts[0], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from incorrect address - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // success claim - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, { from: accounts[1], gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check alice balance - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const payment = await this.bob.payments(txId); - - // status - assert.equal(payment[2].valueOf(), ALICE_CLAIMED_PAYMENT); - - // should not allow to claim again - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Alice to claim ERC20 payment by revealing a secret', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - this.token.address, - lockTime - ]; - - // should not allow to claim from uninitialized payment - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - await this.bob.bobMakesErc20Payment(...params).should.be.fulfilled; - - // should not allow to claim with invalid secret - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), txId, accounts[0], this.token.address, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim from incorrect address - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, { from: accounts[0] }).should.be.rejectedWith(EVMThrow); - - // success claim - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, { from: accounts[1] }).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[1]); - // check alice balance - assert.equal(balanceAfter.toString(), web3.utils.toWei('101')); - - const payment = await this.bob.payments(txId); - - // status - assert.equal(payment[2].valueOf(), ALICE_CLAIMED_PAYMENT); - - // should not allow to claim again - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to claim ETH payment after locktime expires', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - accounts[1], - secretHash, - lockTime - ]; - - // should not allow to claim from uninitialized payment - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], zeroAddr, secretHash).should.be.rejectedWith(EVMThrow); - - await this.bob.bobMakesEthPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; - - // should not allow to claim before locktime - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], zeroAddr, secretHash).should.be.rejectedWith(EVMThrow); - - await increaseTime(1000); - // should not allow alice to claim even with valid secret - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], zeroAddr, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim invalid amount - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('2'), accounts[1], zeroAddr, secretHash).should.be.rejectedWith(EVMThrow); - - // success claim - const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - // default ganache-cli gas price - const gasPrice = web3.utils.toWei('100', 'gwei'); - const tx = await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], zeroAddr, secretHash, { gasPrice }).should.be.fulfilled; - const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); - - const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); - // check alice balance - assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); - - const payment = await this.bob.payments(txId); - - // status - assert.equal(payment[2].valueOf(), BOB_CLAIMED_PAYMENT); - - // should not allow to claim again - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], zeroAddr, secretHash).should.be.rejectedWith(EVMThrow); - }); - - it('should allow Bob to claim ERC20 payment after locktime expires', async function () { - const lockTime = await currentEvmTime() + 1000; - const params = [ - txId, - web3.utils.toWei('1'), - accounts[1], - secretHash, - this.token.address, - lockTime - ]; - - // should not allow to claim from uninitialized payment - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], this.token.address, secretHash).should.be.rejectedWith(EVMThrow); - - await this.token.approve(this.bob.address, web3.utils.toWei('1')); - await this.bob.bobMakesErc20Payment(...params).should.be.fulfilled; - - // should not allow to claim before time lock expires - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], this.token.address, secretHash).should.be.rejectedWith(EVMThrow); - - await increaseTime(1000); - // should not allow alice to claim even with valid secret - await this.bob.aliceClaimsPayment(txId, web3.utils.toWei('1'), secretHex, accounts[0], this.token.address, { from: accounts[1] }).should.be.rejectedWith(EVMThrow); - - // should not allow to claim invalid amount - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('2'), accounts[1], this.token.address, secretHash).should.be.rejectedWith(EVMThrow); - - // success claim - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], this.token.address, secretHash).should.be.fulfilled; - const balanceAfter = await this.token.balanceOf(accounts[0]); - - // check bob balance - assert.equal(balanceAfter.toString(), web3.utils.toWei('900')); - - const payment = await this.bob.payments(txId); - - // status - assert.equal(payment[2].valueOf(), BOB_CLAIMED_PAYMENT); - - // should not allow to claim again - await this.bob.bobClaimsPayment(txId, web3.utils.toWei('1'), accounts[1], this.token.address, secretHash).should.be.rejectedWith(EVMThrow); - }); -}); diff --git a/test/EtomicSwap.js b/test/EtomicSwap.js new file mode 100644 index 0000000..8f8aef3 --- /dev/null +++ b/test/EtomicSwap.js @@ -0,0 +1,276 @@ +const Swap = artifacts.require('EtomicSwap'); +const Token = artifacts.require('Token'); +const crypto = require('crypto'); +const RIPEMD160 = require('ripemd160'); + +const EVMThrow = 'VM Exception while processing transaction'; + +require('chai') + .use(require('chai-as-promised')) + .should(); + +function increaseTime (increaseAmount) { + return new Promise((resolve, reject) => { + web3.currentProvider.send({ + jsonrpc: '2.0', + method: 'evm_increaseTime', + id: Date.now(), + params: [increaseAmount] + }, (err, res) => { + return err ? reject(err) : resolve(res); + }); + }); +} + +async function currentEvmTime() { + const block = await web3.eth.getBlock("latest"); + return block.timestamp; +} + +const id = '0x' + crypto.randomBytes(32).toString('hex'); +const [PAYMENT_UNINITIALIZED, PAYMENT_SENT, RECEIVER_SPENT, SENDER_REFUNDED] = [0, 1, 2, 3]; + +const secret = crypto.randomBytes(32); +const secretHash = '0x' + new RIPEMD160().update(crypto.createHash('sha256').update(secret).digest()).digest('hex'); +const secretHex = '0x' + secret.toString('hex'); + +const zeroAddr = '0x0000000000000000000000000000000000000000'; + +contract('EtomicSwap', function(accounts) { + + beforeEach(async function () { + this.swap = await Swap.new(); + this.token = await Token.new(); + await this.token.transfer(accounts[1], web3.utils.toWei('100')); + }); + + it('should create contract with uninitialized payments', async function () { + const payment = await this.swap.payments(id); + assert.equal(payment[2].valueOf(), PAYMENT_UNINITIALIZED); + }); + + it('should allow to send ETH payment', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + accounts[1], + secretHash, + lockTime + ]; + await this.swap.ethPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; + + const payment = await this.swap.payments(id); + + // locktime + assert.equal(payment[1].valueOf(), lockTime); + // status + assert.equal(payment[2].valueOf(), PAYMENT_SENT); + + // should not allow to send again + await this.swap.ethPayment(...params, { value: web3.utils.toWei('1') }).should.be.rejectedWith(EVMThrow); + }); + + it('should allow to send ERC20 payment', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + web3.utils.toWei('1'), + this.token.address, + accounts[1], + secretHash, + lockTime + ]; + + await this.token.approve(this.swap.address, web3.utils.toWei('1')); + await this.swap.erc20Payment(...params).should.be.fulfilled; + + //check contract token balance + const balance = await this.token.balanceOf(this.swap.address); + assert.equal(balance.toString(), web3.utils.toWei('1')); + + const payment = await this.swap.payments(id); + + // locktime + assert.equal(payment[1].valueOf(), lockTime); + // status + assert.equal(payment[2].valueOf(), PAYMENT_SENT); + + // should not allow to deposit again + await this.swap.erc20Payment(...params).should.be.rejectedWith(EVMThrow); + }); + + it('should allow sender to refund ETH payment after locktime', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + accounts[1], + secretHash, + lockTime + ]; + + // not allow to refund if payment was not sent + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, zeroAddr, accounts[1]).should.be.rejectedWith(EVMThrow); + + await this.swap.ethPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; + + // not allow to refund before locktime + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, zeroAddr, accounts[1]).should.be.rejectedWith(EVMThrow); + + await increaseTime(1000); + + // not allow to call refund from non-sender address + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, zeroAddr, accounts[1], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + // not allow to refund invalid amount + await this.swap.senderRefund(id, web3.utils.toWei('2'), secretHash, zeroAddr, accounts[1]).should.be.rejectedWith(EVMThrow); + + // success refund + const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); + const gasPrice = web3.utils.toWei('100', 'gwei'); + + const tx = await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, zeroAddr, accounts[1], { gasPrice }).should.be.fulfilled; + const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[0])); + + const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); + // check sender balance + assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); + + const payment = await this.swap.payments(id); + assert.equal(payment[2].valueOf(), SENDER_REFUNDED); + + // not allow to refund again + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, zeroAddr, accounts[1]).should.be.rejectedWith(EVMThrow); + }); + + it('should allow sender to refund ERC20 payment after locktime', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + web3.utils.toWei('1'), + this.token.address, + accounts[1], + secretHash, + lockTime + ]; + + await this.token.approve(this.swap.address, web3.utils.toWei('1')); + await this.swap.erc20Payment(...params).should.be.fulfilled; + + // not allow to refund if payment was not sent + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, this.token.address, accounts[1]).should.be.rejectedWith(EVMThrow); + + // not allow to refund before locktime + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, this.token.address, accounts[1]).should.be.rejectedWith(EVMThrow); + + await increaseTime(1000); + + // not allow to call refund from non-sender address + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, this.token.address, accounts[1], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + // not allow to refund invalid amount + await this.swap.senderRefund(id, web3.utils.toWei('2'), secretHash, this.token.address, accounts[1]).should.be.rejectedWith(EVMThrow); + + // success refund + const balanceBefore = web3.utils.toBN(await this.token.balanceOf(accounts[0])); + + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, this.token.address, accounts[1]).should.be.fulfilled; + + const balanceAfter = web3.utils.toBN(await this.token.balanceOf(accounts[0])); + + // check sender balance + assert.equal(balanceAfter.sub(balanceBefore).toString(), web3.utils.toWei('1')); + + const payment = await this.swap.payments(id); + assert.equal(payment[2].valueOf(), SENDER_REFUNDED); + + // not allow to refund again + await this.swap.senderRefund(id, web3.utils.toWei('1'), secretHash, this.token.address, accounts[1]).should.be.rejectedWith(EVMThrow); + }); + + it('should allow receiver to spend ETH payment by revealing a secret', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + accounts[1], + secretHash, + lockTime + ]; + + // should not allow to spend uninitialized payment + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, zeroAddr, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + await this.swap.ethPayment(...params, { value: web3.utils.toWei('1') }).should.be.fulfilled; + + // should not allow to spend with invalid secret + await this.swap.receiverSpend(id, web3.utils.toWei('1'), id, zeroAddr, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + // should not allow to spend invalid amount + await this.swap.receiverSpend(id, web3.utils.toWei('2'), secretHex, zeroAddr, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + // should not allow to claim from non-receiver address even with valid secret + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, zeroAddr, accounts[0], { from: accounts[0] }).should.be.rejectedWith(EVMThrow); + + // success spend + const balanceBefore = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); + const gasPrice = web3.utils.toWei('100', 'gwei'); + + const tx = await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, zeroAddr, accounts[0], { from: accounts[1], gasPrice }).should.be.fulfilled; + const txFee = web3.utils.toBN(gasPrice).mul(web3.utils.toBN(tx.receipt.gasUsed)); + + const balanceAfter = web3.utils.toBN(await web3.eth.getBalance(accounts[1])); + + // check receiver balance + assert.equal(balanceAfter.sub(balanceBefore).add(txFee).toString(), web3.utils.toWei('1')); + + const payment = await this.swap.payments(id); + + // status + assert.equal(payment[2].valueOf(), RECEIVER_SPENT); + + // should not allow to spend again + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, zeroAddr, accounts[0], { from: accounts[1], gasPrice }).should.be.rejectedWith(EVMThrow); + }); + + it('should allow receiver to spend ERC20 payment by revealing a secret', async function () { + const lockTime = await currentEvmTime() + 1000; + const params = [ + id, + web3.utils.toWei('1'), + this.token.address, + accounts[1], + secretHash, + lockTime + ]; + + // should not allow to spend uninitialized payment + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, this.token.address, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + await this.token.approve(this.swap.address, web3.utils.toWei('1')); + await this.swap.erc20Payment(...params).should.be.fulfilled; + + // should not allow to spend with invalid secret + await this.swap.receiverSpend(id, web3.utils.toWei('1'), id, this.token.address, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + // should not allow to spend invalid amount + await this.swap.receiverSpend(id, web3.utils.toWei('2'), secretHex, this.token.address, accounts[0], { from: accounts[1] }).should.be.rejectedWith(EVMThrow); + + // should not allow to claim from non-receiver address even with valid secret + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, this.token.address, accounts[0], { from: accounts[0] }).should.be.rejectedWith(EVMThrow); + + // success spend + const balanceBefore = web3.utils.toBN(await this.token.balanceOf(accounts[1])); + + const gasPrice = web3.utils.toWei('100', 'gwei'); + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, this.token.address, accounts[0], { from: accounts[1], gasPrice }).should.be.fulfilled; + const balanceAfter = web3.utils.toBN(await this.token.balanceOf(accounts[1])); + + // check receiver balance + assert.equal(balanceAfter.sub(balanceBefore).toString(), web3.utils.toWei('1')); + + const payment = await this.swap.payments(id); + + // status + assert.equal(payment[2].valueOf(), RECEIVER_SPENT); + + // should not allow to spend again + await this.swap.receiverSpend(id, web3.utils.toWei('1'), secretHex, this.token.address, accounts[0], { from: accounts[1], gasPrice }).should.be.rejectedWith(EVMThrow); + }); +});