Skip to content

Commit

Permalink
perform check for erc-20 approve synchronously
Browse files Browse the repository at this point in the history
  • Loading branch information
mholtzman committed Feb 17, 2022
1 parent be6d942 commit cddb743
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 109 deletions.
62 changes: 62 additions & 0 deletions main/abi/erc20.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { decodeCallData as decodeContractData, DecodedCallData } from '.'
import { Interface } from '@ethersproject/abi'
import erc20 from '../externalData/balances/erc-20-abi'
import { Provider } from '../provider'

const erc20Abi = JSON.stringify(erc20)
const erc20Interface = new Interface(erc20)

function decodeCallData (contractAddress: Address, calldata: string) {
const decodedCall = decodeContractData(calldata, erc20Abi)

if (decodedCall) {
return {
contractAddress: contractAddress.toLowerCase(),
contractName: 'ERC-20',
source: 'erc-20 contract',
...decodedCall
}
}
}

function isApproval (data: DecodedCallData) {
return (
data.method === 'approve' &&
data.args.length === 2 &&
(data.args[0].name || '').toLowerCase().endsWith('spender') && data.args[0].type === 'address' &&
(data.args[1].name || '').toLowerCase().endsWith('value') && data.args[1].type === 'uint256'
)
}

async function getDecimals (provider: Provider, contractAddress: Address) {
const calldata = erc20Interface.encodeFunctionData('decimals')

return new Promise<number>(resolve => {
provider.send({
id: 1,
jsonrpc: '2.0',
_origin: 'frame.eth',
method: 'eth_call',
params: [
{
to: contractAddress,
data: calldata,
value: '0x0'
}, 'latest'
]
}, res => resolve(res.result ? parseInt(res.result, 16) : 0))
})

// if the contract doesnt provide decimals, try to get the data from Etherscan
}

function encodeFunctionData (fn: string, params: any[]) {
return erc20Interface.encodeFunctionData(fn, params)
}

export default {
encodeFunctionData,
decodeCallData,
getDecimals,
isApproval
}
73 changes: 24 additions & 49 deletions main/abi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import log from 'electron-log'
import fetch from 'node-fetch'
import { Interface } from '@ethersproject/abi'
import erc20 from '../externalData/balances/erc-20-abi'
import { Provider } from '../provider'

const erc20Abi = JSON.stringify(erc20)

Expand Down Expand Up @@ -46,27 +45,40 @@ function parseAbi (abiData: string): Interface {
}
}

async function fetchSourceCode (contractAddress: Address): Promise<ContractSourceCodeResult | undefined> {
async function fetchSourceCode (contractAddress: Address) {
const res = await fetch(`https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${contractAddress}&apikey=3SYU5MW5QK8RPCJV1XVICHWKT774993S24`)

if (res.status === 200 && (res.headers.get('content-type') || '').toLowerCase().includes('json')) {
const parsedResponse = await (res.json() as Promise<EtherscanSourceCodeResponse>)

if (parsedResponse.message === 'OK') return parsedResponse.result
}

return []
}


async function fetchAbi (contractAddress: Address): Promise<ContractSourceCodeResult | undefined> {
try {
const res = await fetch(`https://api.etherscan.io/api?module=contract&action=getsourcecode&address=${contractAddress}&apikey=3SYU5MW5QK8RPCJV1XVICHWKT774993S24`)
const data = await (res.json() as Promise<EtherscanSourceCodeResponse>)
const sources = await fetchSourceCode(contractAddress)

if (data && data.message === 'OK' && (data.result || []).length > 0) {
const implementation = data.result[0].Implementation
if (sources.length > 0) {
const source = sources[0]
const implementation = source.Implementation

if (implementation) {
// this is a proxy contract, return the ABI for the source
return fetchSourceCode(implementation)
return fetchAbi(implementation)
}

return data.result[0]
return source
}
} catch (e) {
log.warn(`could not fetch source code for contract ${contractAddress}`, e)
}
}

function decodeData (abi: string, calldata: string) {
export function decodeCallData (calldata: string, abi: string) {
const contractInterface = parseAbi(abi)

if (contractInterface) {
Expand All @@ -86,16 +98,16 @@ function decodeData (abi: string, calldata: string) {
}
}

export async function decodeCalldata (contractAddress: Address, calldata: string): Promise<DecodedCallData | undefined> {
export async function decodeContractCall (contractAddress: Address, calldata: string): Promise<DecodedCallData | undefined> {
const contractSources: ContractSource[] = [{ name: 'ERC-20', source: 'erc-20 contract', abi: erc20Abi }]
const contractSource = await fetchSourceCode(contractAddress)
const contractSource = await fetchAbi(contractAddress)

if (contractSource) {
contractSources.push({ name: contractSource.ContractName, source: 'etherscan', abi: contractSource.ABI })
}

for (const source of contractSources.reverse()) {
const decodedCall = decodeData(source.abi, calldata)
const decodedCall = decodeCallData(calldata, source.abi)

if (decodedCall) {
return {
Expand All @@ -109,40 +121,3 @@ export async function decodeCalldata (contractAddress: Address, calldata: string

log.warn(`Unable to decode data for contract ${contractAddress}`)
}

export function isErc20Approval (data: DecodedCallData) {
return (
data.method === 'approve' &&
data.args.length === 2 &&
data.args[0].name === 'spender' && data.args[0].type === 'address' &&
data.args[1].name === 'value' && data.args[1].type === 'uint256'
)
}

export async function getErc20Decimals (provider: Provider, contractAddress: Address) {
const erc20Interface = parseAbi(erc20Abi)
const calldata = erc20Interface.encodeFunctionData('decimals')

return new Promise<number>(resolve => {
provider.send({
id: 1,
jsonrpc: '2.0',
_origin: 'frame.eth',
method: 'eth_call',
params: [
{
to: contractAddress,
data: calldata,
value: '0x0'
}, 'latest'
]
}, res => resolve(parseInt(res.result, 16)))
})

// if the contract doesnt provide decimals, try to get the data from Etherscan
}

export function encodeErc20Call (fn: string, params: any[]) {
const erc20Interface = parseAbi(erc20Abi)
return erc20Interface.encodeFunctionData(fn, params)
}
109 changes: 50 additions & 59 deletions main/accounts/Account/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import log from 'electron-log'
import { isValidAddress, addHexPrefix } from 'ethereumjs-util'
import { Version } from 'eth-sig-util'
import BigNumber from 'bignumber.js'

import { AccessRequest, AccountRequest, Accounts, RequestMode, TransactionRequest } from '..'
import { decodeCalldata, encodeErc20Call, getErc20Decimals, isErc20Approval } from '../../abi'
import { decodeContractCall } from '../../abi'
import erc20 from '../../abi/erc20'
import nebulaApi from '../../nebula'
import signers from '../../signers'
import windows from '../../windows'
Expand All @@ -22,14 +22,6 @@ const nebula = nebulaApi('accounts')
const storeApi = {
getPermissions: function (address: Address) {
return (store('main.permissions', address) || {}) as Record<string, Permission>
},
findToken: function (accountAddress: Address, contractAddress: Address, chainId: number) {
const allTokens = [
...store('main.tokens.custom'),
...store('main.tokens.known', accountAddress)
] as Token[]

return allTokens.find(token => token.address === contractAddress && token.chainId === chainId)
}
}

Expand Down Expand Up @@ -245,56 +237,47 @@ class FrameAccount {
res({ id: payload.id, jsonrpc: payload.jsonrpc, error })
}

private async populateRequestCallData (req: TransactionRequest) {
const { to, data: calldata } = req.data || {}

if (calldata && calldata !== '0x' && parseInt(calldata, 16) !== 0) {
try {
const decodedData = await decodeCalldata(to || '', calldata)
const knownTxRequest = this.requests[req.handlerId] as TransactionRequest

if (decodedData && knownTxRequest) {
if (isErc20Approval(decodedData)) {
const spender = decodedData.args[0].value
const amount = decodedData.args[1].value

const targetChain = parseInt(req.data.chainId, 16)
let decimals = 0
private async checkForErc20Approve (req: TransactionRequest, calldata: string) {
const decodedData = erc20.decodeCallData(req.data.to || '', calldata)

if (decodedData && erc20.isApproval(decodedData)) {
const spender = decodedData.args[0].value
const amount = decodedData.args[1].value
const decimals = await erc20.getDecimals(provider, decodedData.contractAddress)

this.addRequiredApproval(
req,
ApprovalType.TokenSpendApproval,
{
decimals,
amount,
contract: decodedData.contractAddress
},
data => {
req.data.data = erc20.encodeFunctionData('approve', [spender, data.amount])

if (req.decodedData) {
req.decodedData.args[1].value = data.amount
}
}
)

// first check if we already have data about the token
const token = storeApi.findToken(this.address, decodedData.contractAddress, targetChain)
req.decodedData = decodedData
this.update()
}
}

if (token) {
decimals = token.decimals
} else {
// if not, try to get token data from the contract
decimals = await getErc20Decimals(provider, decodedData.contractAddress)
}
private async populateRequestCallData (req: TransactionRequest, calldata: string) {
try {
const decodedData = await decodeContractCall(req.data.to || '', calldata)
const knownTxRequest = this.requests[req.handlerId] as TransactionRequest

this.addRequiredApproval(
knownTxRequest,
ApprovalType.TokenSpendApproval,
{
decimals,
amount,
contract: decodedData.contractAddress
},
data => {
req.data.data = encodeErc20Call('approve', [spender, data.amount])

if (req.decodedData) {
req.decodedData.args[1].value = data.amount
}
}
)

knownTxRequest.decodedData = decodedData
this.update()
}
}
} catch (e) {
log.warn(e)
}
if (knownTxRequest && decodedData) {
knownTxRequest.decodedData = decodedData
this.update()
}
} catch (e) {
log.warn(e)
}
}

Expand Down Expand Up @@ -322,8 +305,16 @@ class FrameAccount {
this.requests[r.handlerId].res = res

if ((req || {}).type === 'transaction') {
this.populateRequestCallData(req as TransactionRequest)
this.populateRequestEnsName(req as TransactionRequest)
const txRequest = req as TransactionRequest
const calldata = txRequest.data.data

if (calldata && calldata !== '0x' && parseInt(calldata, 16) !== 0) {
await this.checkForErc20Approve(txRequest, calldata)

this.populateRequestCallData(txRequest, calldata)
}

this.populateRequestEnsName(txRequest)
}

this.update()
Expand Down
2 changes: 1 addition & 1 deletion main/externalData/balances/erc-20-abi.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = [
export default [
{
"constant": true,
"inputs": [],
Expand Down

0 comments on commit cddb743

Please sign in to comment.