Merge pull request #223 from gnosis/bug/218-non-18-decimals-tokens-transfer

BUG: Hotfix #218 Non-18 decimals tokens transfer
This commit is contained in:
Mikhail Mikheev 2019-10-14 19:15:26 +04:00 committed by GitHub
commit c3f4732aa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 105 additions and 16 deletions

View File

@ -2,5 +2,6 @@ pragma solidity ^0.5.2;
import "../src/test/contracts/TokenOMG.sol"; import "../src/test/contracts/TokenOMG.sol";
import "../src/test/contracts/TokenRDN.sol"; import "../src/test/contracts/TokenRDN.sol";
import "../src/test/contracts/Token6Decimals.sol";
contract DevDependenciesGetter {} contract DevDependenciesGetter {}

View File

@ -1,5 +1,6 @@
// @flow // @flow
import React from 'react' import React from 'react'
import { BigNumber } from 'bignumber.js'
import OpenInNew from '@material-ui/icons/OpenInNew' import OpenInNew from '@material-ui/icons/OpenInNew'
import { withStyles } from '@material-ui/core/styles' import { withStyles } from '@material-ui/core/styles'
import Close from '@material-ui/icons/Close' import Close from '@material-ui/icons/Close'
@ -17,7 +18,7 @@ import { copyToClipboard } from '~/utils/clipboard'
import Hairline from '~/components/layout/Hairline' import Hairline from '~/components/layout/Hairline'
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo' import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils' import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
import { getStandardTokenContract } from '~/logic/tokens/store/actions/fetchTokens' import { getHumanFriendlyToken } from '~/logic/tokens/store/actions/fetchTokens'
import { EMPTY_DATA } from '~/logic/wallets/ethTransactions' import { EMPTY_DATA } from '~/logic/wallets/ethTransactions'
import { getWeb3 } from '~/logic/wallets/getWeb3' import { getWeb3 } from '~/logic/wallets/getWeb3'
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions' import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
@ -66,8 +67,10 @@ const ReviewTx = ({
let txAmount = web3.utils.toWei(tx.amount, 'ether') let txAmount = web3.utils.toWei(tx.amount, 'ether')
if (!isSendingETH) { if (!isSendingETH) {
const StandardToken = await getStandardTokenContract() const HumanFriendlyToken = await getHumanFriendlyToken()
const tokenInstance = await StandardToken.at(tx.token.address) const tokenInstance = await HumanFriendlyToken.at(tx.token.address)
const decimals = await tokenInstance.decimals()
txAmount = new BigNumber(tx.amount).times(10 ** decimals.toNumber()).toString()
txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI() txData = tokenInstance.contract.methods.transfer(tx.recipientAddress, txAmount).encodeABI()
// txAmount should be 0 if we send tokens // txAmount should be 0 if we send tokens

View File

@ -46,7 +46,7 @@ type CustomDescProps = {
value: string, value: string,
recipient: string, recipient: string,
data: String, data: String,
classes: Obeject, classes: Object,
} }
const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => ( const TransferDescription = ({ value = '', symbol, recipient }: TransferDescProps) => (

View File

@ -0,0 +1,17 @@
pragma solidity ^0.5.2;
import "@gnosis.pm/util-contracts/contracts/GnosisStandardToken.sol";
contract Token6Decimals is GnosisStandardToken {
string public constant symbol = "6DEC";
string public constant name = "6 Decimals";
uint8 public constant decimals = 6;
constructor(
uint amount
)
public
{
balances[msg.sender] = amount;
}
}

View File

@ -1,17 +1,19 @@
// @flow // @flow
import { fireEvent } from '@testing-library/react' import { fireEvent } from '@testing-library/react'
import { Map, Set } from 'immutable' import { Map, Set, List } from 'immutable'
import { aNewStore } from '~/store' import { aNewStore } from '~/store'
import { aMinedSafe } from '~/test/builder/safe.redux.builder' import { aMinedSafe } from '~/test/builder/safe.redux.builder'
import { sendTokenTo, sendEtherTo } from '~/test/utils/tokenMovements' import { sendTokenTo, sendEtherTo, get6DecimalsTokenContract } from '~/test/utils/tokenMovements'
import { renderSafeView } from '~/test/builder/safe.dom.utils' import { renderSafeView } from '~/test/builder/safe.dom.utils'
import { getWeb3, getBalanceInEtherOf } from '~/logic/wallets/getWeb3' import { getWeb3, getBalanceInEtherOf } from '~/logic/wallets/getWeb3'
import { dispatchAddTokenToList } from '~/test/utils/transactions/moveTokens.helper' import { dispatchAddTokenToList } from '~/test/utils/transactions/moveTokens.helper'
import { sleep } from '~/utils/timer' import { sleep } from '~/utils/timer'
import saveTokens from '~/logic/tokens/store/actions/saveTokens'
import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances' import { calculateBalanceOf } from '~/routes/safe/store/actions/fetchTokenBalances'
import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens' import updateActiveTokens from '~/routes/safe/store/actions/updateActiveTokens'
import '@testing-library/jest-dom/extend-expect' import '@testing-library/jest-dom/extend-expect'
import updateSafe from '~/routes/safe/store/actions/updateSafe' import updateSafe from '~/routes/safe/store/actions/updateSafe'
import { makeToken } from '~/logic/tokens/store/model/token'
import { checkRegisteredTxSend, fillAndSubmitSendFundsForm } from './utils/transactions' import { checkRegisteredTxSend, fillAndSubmitSendFundsForm } from './utils/transactions'
import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances' import { BALANCE_ROW_TEST_ID } from '~/routes/safe/components/Balances'
@ -21,7 +23,6 @@ describe('DOM > Feature > Sending Funds', () => {
let accounts let accounts
beforeEach(async () => { beforeEach(async () => {
store = aNewStore() store = aNewStore()
// using 4th account because other accounts were used in other tests and paid gas
safeAddress = await aMinedSafe(store) safeAddress = await aMinedSafe(store)
accounts = await getWeb3().eth.getAccounts() accounts = await getWeb3().eth.getAccounts()
}) })
@ -62,7 +63,7 @@ describe('DOM > Feature > Sending Funds', () => {
await checkRegisteredTxSend(SafeDom, ethAmount, 'ETH', accounts[9]) await checkRegisteredTxSend(SafeDom, ethAmount, 'ETH', accounts[9])
}) })
it('Sends Tokens with threshold = 1', async () => { it('Sends Tokens with 18 decimals with threshold = 1', async () => {
// GIVEN // GIVEN
const tokensAmount = '100' const tokensAmount = '100'
const tokenReceiver = accounts[1] const tokenReceiver = accounts[1]
@ -102,4 +103,54 @@ describe('DOM > Feature > Sending Funds', () => {
// Check that the transaction was registered // Check that the transaction was registered
await checkRegisteredTxSend(SafeDom, tokensAmount, 'OMG', tokenReceiver) await checkRegisteredTxSend(SafeDom, tokensAmount, 'OMG', tokenReceiver)
}) })
it('Sends Tokens with decimals other than 18 with threshold = 1', async () => {
// GIVEN
const tokensAmount = '1000000'
const tokenReceiver = accounts[1]
const web3 = await getWeb3()
const SixDecimalsToken = await get6DecimalsTokenContract(web3, accounts[0])
const tokenList = List([
makeToken({
address: SixDecimalsToken.address,
name: '6 Decimals',
symbol: '6DEC',
decimals: 6,
logoUri: 'https://upload.wikimedia.org/wikipedia/commons/c/c0/Earth_simple_icon.png',
}),
])
await store.dispatch(saveTokens(tokenList))
await SixDecimalsToken.contract.methods.transfer(safeAddress, tokensAmount).send({ from: accounts[0] })
// WHEN
const SafeDom = await renderSafeView(store, safeAddress)
await sleep(1300)
// Activate token
const safeTokenBalance = await calculateBalanceOf(SixDecimalsToken.address, safeAddress, 6)
expect(safeTokenBalance).toBe('1')
const balances = Map({
[SixDecimalsToken.address]: safeTokenBalance,
})
store.dispatch(updateActiveTokens(safeAddress, Set([SixDecimalsToken.address])))
store.dispatch(updateSafe({ address: safeAddress, balances }))
await sleep(1000)
// Open send funds modal
const balanceRows = SafeDom.getAllByTestId(BALANCE_ROW_TEST_ID)
expect(balanceRows.length).toBe(2)
const sendButtons = SafeDom.getAllByTestId('balance-send-btn')
expect(sendButtons.length).toBe(2)
await fillAndSubmitSendFundsForm(SafeDom, sendButtons[1], '1', tokenReceiver)
// THEN
const safeFunds = await calculateBalanceOf(SixDecimalsToken.address, safeAddress, 6)
expect(Number(safeFunds)).toBe(0)
const receiverFunds = await calculateBalanceOf(SixDecimalsToken.address, tokenReceiver, 6)
expect(receiverFunds).toBe('1')
})
}) })

View File

@ -5,6 +5,7 @@ import { ensureOnce } from '~/utils/singleton'
import { toNative } from '~/logic/wallets/tokens' import { toNative } from '~/logic/wallets/tokens'
import TokenOMG from '../../../build/contracts/TokenOMG' import TokenOMG from '../../../build/contracts/TokenOMG'
import TokenRDN from '../../../build/contracts/TokenRDN' import TokenRDN from '../../../build/contracts/TokenRDN'
import Token6Decimals from '../../../build/contracts/Token6Decimals.json'
export const sendEtherTo = async (address: string, eth: string, fromAccountIndex: number = 0) => { export const sendEtherTo = async (address: string, eth: string, fromAccountIndex: number = 0) => {
const web3 = getWeb3() const web3 = getWeb3()
@ -41,8 +42,20 @@ const createTokenRDNContract = async (web3: any, creator: string) => {
return token.new(amount, { from: creator }) return token.new(amount, { from: creator })
} }
const create6DecimalsTokenContract = async (web3: any, creator: string) => {
const token = contract(Token6Decimals)
const { toBN } = web3.utils
const amount = toBN(50000)
.mul(toBN(10).pow(toBN(6)))
.toString()
token.setProvider(web3.currentProvider)
return token.new(amount, { from: creator })
}
export const getFirstTokenContract = ensureOnce(createTokenOMGContract) export const getFirstTokenContract = ensureOnce(createTokenOMGContract)
export const getSecondTokenContract = ensureOnce(createTokenRDNContract) export const getSecondTokenContract = ensureOnce(createTokenRDNContract)
export const get6DecimalsTokenContract = ensureOnce(create6DecimalsTokenContract)
export const sendTokenTo = async (safe: string, value: string, tokenContract?: any) => { export const sendTokenTo = async (safe: string, value: string, tokenContract?: any) => {
const web3 = getWeb3() const web3 = getWeb3()

View File

@ -1,6 +1,6 @@
// @flow // @flow
import * as React from 'react' import * as React from 'react'
import { fireEvent, waitForElement } from '@testing-library/react' import { fireEvent, waitForElement, act } from '@testing-library/react'
import { sleep } from '~/utils/timer' import { sleep } from '~/utils/timer'
export const fillAndSubmitSendFundsForm = async ( export const fillAndSubmitSendFundsForm = async (
@ -9,8 +9,9 @@ export const fillAndSubmitSendFundsForm = async (
value: string, value: string,
recipient: string, recipient: string,
) => { ) => {
// load add multisig form component await act(async () => {
fireEvent.click(sendButton) fireEvent.click(sendButton)
})
// give time to re-render it // give time to re-render it
await sleep(400) await sleep(400)
@ -18,13 +19,16 @@ export const fillAndSubmitSendFundsForm = async (
const recipientInput = SafeDom.getByPlaceholderText('Recipient*') const recipientInput = SafeDom.getByPlaceholderText('Recipient*')
const amountInput = SafeDom.getByPlaceholderText('Amount*') const amountInput = SafeDom.getByPlaceholderText('Amount*')
const reviewBtn = SafeDom.getByTestId('review-tx-btn') const reviewBtn = SafeDom.getByTestId('review-tx-btn')
await act(async () => {
fireEvent.change(recipientInput, { target: { value: recipient } }) fireEvent.change(recipientInput, { target: { value: recipient } })
fireEvent.change(amountInput, { target: { value } }) fireEvent.change(amountInput, { target: { value } })
await sleep(200)
fireEvent.click(reviewBtn) fireEvent.click(reviewBtn)
})
// Submit the tx (Review Tx screen) // Submit the tx (Review Tx screen)
const submitBtn = await waitForElement(() => SafeDom.getByTestId('submit-tx-btn')) const submitBtn = await waitForElement(() => SafeDom.getByTestId('submit-tx-btn'))
await act(async () => {
fireEvent.click(submitBtn) fireEvent.click(submitBtn)
})
await sleep(1000) await sleep(1000)
} }