Skip to content

Instantly share code, notes, and snippets.

@andreafspeziale
Created December 18, 2019 14:40
Show Gist options
  • Save andreafspeziale/de8443832629cd58c9723e7df91cc814 to your computer and use it in GitHub Desktop.
Save andreafspeziale/de8443832629cd58c9723e7df91cc814 to your computer and use it in GitHub Desktop.
Test case for the atomic proxy SC
/* eslint-env node, mocha */
/* global artifacts, contract, expect, web3 */
/* eslint no-underscore-dangle: 1 */
const Tx = require('ethereumjs-tx').Transaction
const ethereumUtil = require('ethereumjs-util')
const moment = require('moment')

const BigNumber = web3.utils.BN

const ERC20 = artifacts.require('./Mocks/MockToken.sol')
const ETHSender = artifacts.require('./ETHSender.sol')
const Proxy = artifacts.require('./Proxy.sol')

const sender = '0xdb1b9e1708aec862fee256821702fa1906ceff67'
const senderPrivateKey = Buffer.from('a8345d27c6d41e4816163fe133daddf38298bb74c16ea5f8245727d03a5f85f8', 'hex')

contract('Proxy', (accounts) => {
  const [ProxyOwner] = accounts
  const [, TokenOwner] = accounts
  const [, , issuer] = accounts
  const [, , , receiver] = accounts

  describe('forwardWithNoValue', () => {
    let ETHSenderInstance
    let MockTokenInstance
    let ProxyInstance

    beforeEach(async () => {
      ProxyInstance = await Proxy.new({ from: ProxyOwner })
      ETHSenderInstance = await ETHSender.new(ProxyInstance.address, { from: issuer, value: web3.utils.toWei('1') })

      MockTokenInstance = await ERC20.new(
        'MockToken',
        'ERC20',
        18,
        web3.utils.toWei('100'),
        { from: TokenOwner },
      )

      await MockTokenInstance.transfer(sender, web3.utils.toWei('100'), { from: TokenOwner })
    })

    it('Should complete successfully the entire flow where an etherless account pay a transaction fee in ERC20', async () => {
      // sender:
      //    - has no ETH
      //    - has 100 ERC0
      //    - want to send 10 ERC20 to another account
      //    - never approved issuer to move its funds

      // in order to send the 10 ERC0 to another account the flow needs:
      //    - create the raw transaction of the approveTx (approve(Proxy, ~ ∞))
      //    - create the raw transaction of the primaryTx (transfer(receiver, amount))
      //    - create a metaTx signed by issuer and sender for the approve where
      //        - 0 ERC20 are asked to the user by issuer
      //        - the equivalent of the approve cost is sent to the user by issuer in ETH
      //    - as soon the sender receive the ETH for the approve the approve raw transaction must be broadcasted
      //    - create a metaTx signed by issuer and sender for the original Tx where
      //        - the equivalent of the original Tx cost is withdraw from the sender by issuer in ERC20
      //        - the equivalent of the original Tx cost is sent to the user by issuer in ETH
      //    - as soon the sender receive the ETH for the original Tx it must be broadcasted

      const senderERC20InitialBalance = await MockTokenInstance.balanceOf(sender)
      const amountToReceiver = new BigNumber(web3.utils.toWei('10'))

      // prepare approve
      const approveTxData = MockTokenInstance.contract.methods.approve(ProxyInstance.address, web3.utils.toWei('1000000000000000000000')).encodeABI()
      const approveGasEstimation = new BigNumber(await MockTokenInstance.approve.estimateGas(ProxyInstance.address, web3.utils.toWei('1000000000000000000000'), { from: sender }))
      const approveTx = new Tx({
        from: sender,
        to: MockTokenInstance.address,
        data: approveTxData,
        nonce: web3.utils.toHex(await web3.eth.getTransactionCount(sender)),
        value: web3.utils.toHex('0'),
        gasPrice: web3.utils.toHex(web3.utils.toWei('0.00000001')),
        gas: web3.utils.toHex(approveGasEstimation),
      })

      // sign approve
      approveTx.sign(senderPrivateKey)
      const approveRawTx = ethereumUtil.bufferToHex(approveTx.serialize())
      const approveETHCost = approveTx.getUpfrontCost()

      // prepare transfer which is the primary sender intention
      const primaryTxData = MockTokenInstance.contract.methods.transfer(receiver, amountToReceiver.toString()).encodeABI()
      const primaryGasEstimation = new BigNumber(await MockTokenInstance.transfer.estimateGas(receiver, amountToReceiver.toString(), { from: sender }))

      const primaryTx = new Tx({
        to: MockTokenInstance.address,
        data: primaryTxData,
        nonce: web3.utils.toHex(await web3.eth.getTransactionCount(sender) + 1),
        value: web3.utils.toHex('0'),
        gasPrice: web3.utils.toHex(web3.utils.toWei('0.00000001')),
        gas: web3.utils.toHex(primaryGasEstimation), // web3.utils.toHex(primaryGasEstimation.add(new BigNumber('30000')))
      })

      // sign primary tx
      primaryTx.sign(senderPrivateKey)
      const primaryRawTx = ethereumUtil.bufferToHex(primaryTx.serialize())
      const primaryETHCost = primaryTx.getUpfrontCost()

      // prepare approve meta tx

      // transferFrom(sender, issuer, 0)
      let senderMetaTxData = MockTokenInstance
        .contract
        .methods
        .transferFrom(sender, issuer, '0').encodeABI()

      // sendETH(issuer, sender, approveETHCost)
      let issuerMetaTxData = ETHSenderInstance
        .contract
        .methods
        .sendETH(issuer, sender, approveETHCost.toString()).encodeABI()

      // commons
      let recipients = [MockTokenInstance.address, ETHSenderInstance.address]
      let metaTxsValue = [new BigNumber('0'), new BigNumber('0')]
      let packedTxsDataField = senderMetaTxData + issuerMetaTxData.substring(2)
      let txsDataSizes = [senderMetaTxData.substring(2).length / 2, issuerMetaTxData.substring(2).length / 2]
      let salt = new BigNumber(web3.utils.randomHex(32))
      let expiration = new BigNumber(moment().unix()).add(new BigNumber('72000'))

      // hash
      let hashedMetaTx = web3.utils.soliditySha3(
        { t: 'address[]', v: recipients },
        { t: 'uint256[]', v: metaTxsValue },
        { t: 'bytes', v: packedTxsDataField },
        { t: 'uint256', v: salt },
        { t: 'uint256', v: expiration },
      )

      // sign
      let senderSig = ethereumUtil.ecsign(
        ethereumUtil.hashPersonalMessage(Buffer.from(hashedMetaTx.substring(2), 'hex')),
        senderPrivateKey,
      )
      let senderSigHex = ethereumUtil.bufferToHex(Buffer.concat([senderSig.r, senderSig.s])) + web3.utils.toHex(senderSig.v).substring(2)

      let packedSignature = senderSigHex

      await ProxyInstance.forwardWithNoValue(
        packedSignature,
        recipients,
        packedTxsDataField,
        txsDataSizes,
        salt,
        expiration,
        { from: issuer },
      )

      expect(await web3.eth.getBalance(sender)).to.equal(approveETHCost.toString())
      // send the previously signed approve tx
      await web3.eth.sendSignedTransaction(approveRawTx)
      expect((await MockTokenInstance.allowance(sender, ProxyInstance.address)).toString()).to.equal(web3.utils.toWei('1000000000000000000000'))
      // the empty account ETH balance should be = to 0
      expect(await web3.eth.getBalance(sender)).to.equal('0')

      // prepare transfer meta tx

      const transferERC20FeeCost = new BigNumber(web3.utils.toWei('1'))

      // transferFrom(sender, issuer, 1)
      senderMetaTxData = MockTokenInstance
        .contract
        .methods
        .transferFrom(sender, issuer, transferERC20FeeCost.toString()).encodeABI()

      // sendETH(issuer, sender, primaryETHCost)
      issuerMetaTxData = ETHSenderInstance
        .contract
        .methods
        .sendETH(issuer, sender, primaryETHCost.toString()).encodeABI()

      // commons
      recipients = [MockTokenInstance.address, ETHSenderInstance.address]
      metaTxsValue = [new BigNumber('0'), new BigNumber('0')]
      packedTxsDataField = senderMetaTxData + issuerMetaTxData.substring(2)
      txsDataSizes = [senderMetaTxData.substring(2).length / 2, issuerMetaTxData.substring(2).length / 2]
      salt = new BigNumber(web3.utils.randomHex(32))
      expiration = new BigNumber(moment().unix()).add(new BigNumber('72000'))

      // hash
      hashedMetaTx = web3.utils.soliditySha3(
        { t: 'address[]', v: recipients },
        { t: 'uint256[]', v: metaTxsValue },
        { t: 'bytes', v: packedTxsDataField },
        { t: 'uint256', v: salt },
        { t: 'uint256', v: expiration },
      )

      // sign
      senderSig = ethereumUtil.ecsign(
        ethereumUtil.hashPersonalMessage(Buffer.from(hashedMetaTx.substring(2), 'hex')),
        senderPrivateKey,
      )
      senderSigHex = ethereumUtil.bufferToHex(Buffer.concat([senderSig.r, senderSig.s])) + web3.utils.toHex(senderSig.v).substring(2)

      packedSignature = senderSigHex

      await ProxyInstance.forwardWithNoValue(
        packedSignature,
        recipients,
        packedTxsDataField,
        txsDataSizes,
        salt,
        expiration,
        { from: issuer },
      )

      expect(await web3.eth.getBalance(sender)).to.equal(primaryETHCost.toString())

      const receiverBalanceBeforeTransfer = await MockTokenInstance.balanceOf(receiver)

      await web3.eth.sendSignedTransaction(primaryRawTx)

      // At the end of the flow
      // sender:
      //  - has 100 - 10 - 1 (transaction fee) ERC20s
      //  - has 0 ETH
      //
      // receiver:
      //  - has + 10 ERC20s
      //
      // issuer:
      //  - has + 1 ERC20s

      const senderERC20FinalBalance = await MockTokenInstance.balanceOf(sender)
      const issuerERC20Balance = await MockTokenInstance.balanceOf(issuer)
      const receiverBalanceAfterTransfer = await MockTokenInstance.balanceOf(receiver)
      const senderETHFinalBalance = await web3.eth.getBalance(sender)

      expect(
        receiverBalanceBeforeTransfer.add(amountToReceiver).toString(),
      ).to.equal(receiverBalanceAfterTransfer.toString())

      expect(senderETHFinalBalance).to.equal('0')

      expect(senderERC20FinalBalance.toString()).to.equal(senderERC20InitialBalance.sub(amountToReceiver).sub(transferERC20FeeCost).toString())

      expect(issuerERC20Balance.toString()).to.equal(transferERC20FeeCost.toString())
    })
  })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment