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:
commit
c3f4732aa5
|
@ -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 {}
|
|
@ -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
|
||||||
|
|
|
@ -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) => (
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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')
|
||||||
fireEvent.change(recipientInput, { target: { value: recipient } })
|
await act(async () => {
|
||||||
fireEvent.change(amountInput, { target: { value } })
|
fireEvent.change(recipientInput, { target: { value: recipient } })
|
||||||
await sleep(200)
|
fireEvent.change(amountInput, { target: { value } })
|
||||||
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'))
|
||||||
fireEvent.click(submitBtn)
|
await act(async () => {
|
||||||
|
fireEvent.click(submitBtn)
|
||||||
|
})
|
||||||
await sleep(1000)
|
await sleep(1000)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue