Skip to content

Commit

Permalink
Backend: make sure fee is less than token value
Browse files Browse the repository at this point in the history
This commit adds tx parsing to extract the amount of
token being transfered, this allows for estimation of
token's value which then gets compared with miner fee value
to prevent unresonable miner fees when transferring tokens.

This commit also updates NEthereum to a new version because
the previous version was parsing token value incorrectly.

It's also noteworthy that Web3 object no longer accepts
configuring the timeout.

Co-authored-by: Mehrshad <[email protected]>
  • Loading branch information
aarani and Mersho committed Oct 2, 2023
1 parent aaa8c2e commit 746a662
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 69 deletions.
107 changes: 77 additions & 30 deletions src/GWallet.Backend/Ether/EtherAccount.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ open Nethereum.Signer
open Nethereum.KeyStore
open Nethereum.Util
open Nethereum.KeyStore.Crypto
open Nethereum.Model

open GWallet.Backend
open GWallet.Backend.FSharpUtil.UwpHacks


module internal Account =

let private addressUtil = AddressUtil()
let private signer = TransactionSigner()
let private signer = LegacyTransactionSigner()

let private KeyStoreService = KeyStoreService()

Expand Down Expand Up @@ -218,30 +220,67 @@ module internal Account =
return failwith <| SPrintF1 "Assertion failed: currency %A should be Ether or Ether token" account.Currency
}

let private ValidateMinerFee (trans: string) =
let intDecoder = IntTypeDecoder()

let tx = TransactionFactory.CreateTransaction trans

let amountInWei = intDecoder.DecodeBigInteger tx.Value

// TODO: handle validating miner fee in token transfer (where amount is zero)
if amountInWei <> BigInteger.Zero then
let gasLimitInWei = intDecoder.DecodeBigInteger tx.GasLimit
let gasPriceInWei = intDecoder.DecodeBigInteger tx.GasPrice
let minerFeeInWei = gasLimitInWei * gasPriceInWei

if minerFeeInWei > amountInWei then
raise MinerFeeHigherThanOutputs

let private BroadcastRawTransaction (currency: Currency) trans (ignoreHigherMinerFeeThanAmount: bool) =
if not ignoreHigherMinerFeeThanAmount then
ValidateMinerFee trans
Ether.Server.BroadcastTransaction currency ("0x" + trans)
let private ValidateMinerFee currency feeCurrency (trans: string) =
async {
let intDecoder = IntTypeDecoder()
let tx = TransactionFactory.CreateTransaction trans :?> SignedLegacyTransaction

let amountInEther =
let amountInWei = intDecoder.DecodeBigInteger tx.Value
UnitConversion.Convert.FromWei(amountInWei, UnitConversion.EthUnit.Ether)

let minerFeeInEther =
let gasLimitInWei = intDecoder.DecodeBigInteger tx.GasLimit
let gasPriceInWei = intDecoder.DecodeBigInteger tx.GasPrice
let minerFeeInWei = gasLimitInWei * gasPriceInWei
UnitConversion.Convert.FromWei(minerFeeInWei, UnitConversion.EthUnit.Ether)

if amountInEther <> 0m then
if minerFeeInEther > amountInEther then
return raise MinerFeeHigherThanOutputs
else
let tokenAmountInEther =
let _destination, tokenAmountInWei =
(TokenManager.OfflineTokenServiceWrapper currency)
.DecodeInputDataForTransferTransaction tx.Data

UnitConversion.Convert.FromWei(tokenAmountInWei, UnitConversion.EthUnit.Ether)

let estimateValueInUsd currency amount =
async {
let! usdValue = FiatValueEstimation.UsdValue currency
match usdValue with
| Fresh usdValue ->
return usdValue * amount
| NotFresh(Cached(usdValue, _time)) ->
return usdValue * amount
| NotFresh NotAvailable ->
return failwith "Can't estimate value in USD"
}

let! estimatedTokenValueInUsd =
estimateValueInUsd currency tokenAmountInEther

let! estimatedFeeValueInUsd =
estimateValueInUsd
feeCurrency
minerFeeInEther

if estimatedFeeValueInUsd > estimatedTokenValueInUsd then
return raise MinerFeeHigherThanOutputs
}

let private BroadcastRawTransaction (currency: Currency) (feeCurrency: Currency) trans (ignoreHigherMinerFeeThanAmount: bool) =
async {
if not ignoreHigherMinerFeeThanAmount then
do! ValidateMinerFee currency feeCurrency trans
return! Ether.Server.BroadcastTransaction currency ("0x" + trans)
}

let BroadcastTransaction (trans: SignedTransaction<_>) =
BroadcastRawTransaction
trans.TransactionInfo.Proposal.Amount.Currency
(trans.TransactionInfo.Metadata :> IBlockchainFeeInfo).Currency
trans.RawTransaction

let internal GetPrivateKey (account: NormalAccount) password =
Expand Down Expand Up @@ -350,8 +389,8 @@ module internal Account =
else
failwith <| SPrintF1 "Assertion failed: Ether currency %A not supported?" account.Currency

let chain = GetNetwork account.Currency
if not (signer.VerifyTransaction(trans, chain)) then
let parsedTx = TransactionFactory.CreateTransaction trans :?> SignedLegacyTransaction
if parsedTx.VerifyTransaction() |> not then
failwith "Transaction could not be verified?"
trans

Expand All @@ -378,7 +417,11 @@ module internal Account =
let ecPrivKey = EthECKey(account.GetUnencryptedPrivateKey())
let signedTrans = SignTransactionWithPrivateKey
account txMetadata destination.PublicAddress amount ecPrivKey
BroadcastRawTransaction accountFrom.Currency signedTrans ignoreHigherMinerFeeThanAmount
BroadcastRawTransaction
accountFrom.Currency
txMetadata.Fee.Currency
signedTrans
ignoreHigherMinerFeeThanAmount

let SendPayment (account: NormalAccount)
(txMetadata: TransactionMetadata)
Expand All @@ -394,7 +437,11 @@ module internal Account =

let trans = SignTransaction account txMetadata destination amount password

BroadcastRawTransaction currency trans ignoreHigherMinerFeeThanAmount
BroadcastRawTransaction
currency
txMetadata.Fee.Currency
trans
ignoreHigherMinerFeeThanAmount

let private CreateInternal (password: string) (seed: array<byte>): FileRepresentation =
let privateKey = EthECKey(seed, true)
Expand Down Expand Up @@ -446,7 +493,7 @@ module internal Account =
signedTransaction.TransactionInfo.Proposal :> ITransactionDetails

| _ ->
let getTransactionChainId (tx: TransactionBase) =
let getTransactionChainId (tx: SignedLegacyTransaction) =
// the chain id can be deconstructed like so -
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
// into one of the following -
Expand All @@ -457,22 +504,22 @@ module internal Account =
let chainId = (v - BigInteger 35) / BigInteger 2
chainId

let getTransactionCurrency (tx: TransactionBase) =
let getTransactionCurrency (tx: SignedLegacyTransaction) =
match int (getTransactionChainId tx) with
| chainId when chainId = int Config.EthNet -> ETH
| chainId when chainId = int Config.EtcNet -> ETC
| other -> failwith <| SPrintF1 "Could not infer currency from transaction where chainId = %i." other

let tx = TransactionFactory.CreateTransaction signedTransaction.RawTransaction
let tx = TransactionFactory.CreateTransaction signedTransaction.RawTransaction :?> SignedLegacyTransaction

// HACK: I prefix 12 elements to the address due to AddressTypeDecoder expecting some sort of header...
let address = AddressTypeDecoder().Decode (Array.append (Array.zeroCreate 12) tx.ReceiveAddress)

let destAddress = addressUtil.ConvertToChecksumAddress address
let destAddress = address.ConvertToEthereumChecksumAddress()

let txDetails =
{
OriginAddress = signer.GetSenderAddress signedTransaction.RawTransaction
OriginAddress = TransactionVerificationAndRecovery.GetSenderAddress signedTransaction.RawTransaction
Amount = UnitConversion.Convert.FromWei (IntTypeDecoder().DecodeBigInteger tx.Value)
Currency = getTransactionCurrency tx
DestinationAddress = destAddress
Expand Down
23 changes: 7 additions & 16 deletions src/GWallet.Backend/Ether/EtherServer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type BalanceType =
| Unconfirmed
| Confirmed

type SomeWeb3 (connectionTimeOut, url: string) =
inherit Web3 (connectionTimeOut, url)
type SomeWeb3 (url: string) =
inherit Web3 (url)

member val Url = url with get

Expand Down Expand Up @@ -66,7 +66,7 @@ module Web3ServerSeedList =

module Server =

let private Web3Server (connectionTimeOut, serverDetails: ServerDetails) =
let private Web3Server (serverDetails: ServerDetails) =
match serverDetails.ServerInfo.ConnectionType with
| { Protocol = Tcp _ ; Encrypted = _ } ->
failwith <| SPrintF1 "Ether server of TCP connection type?: %s" serverDetails.ServerInfo.NetworkPath
Expand All @@ -77,7 +77,7 @@ module Server =
else
"http"
let uri = SPrintF2 "%s://%s" protocol serverDetails.ServerInfo.NetworkPath
SomeWeb3 (connectionTimeOut, uri)
SomeWeb3 uri

let HttpRequestExceptionMatchesErrorCode (ex: Http.HttpRequestException) (errorCode: int): bool =
ex.Message.StartsWith(SPrintF1 "%i " errorCode) || ex.Message.Contains(SPrintF1 " %i " errorCode)
Expand Down Expand Up @@ -388,7 +388,6 @@ module Server =

let Web3ServerToRetrievalFunc (server: ServerDetails)
(web3ClientFunc: SomeWeb3->Async<'R>)
currency
: Async<'R> =

let HandlePossibleEtherFailures (job: Async<'R>): Async<'R> =
Expand All @@ -403,15 +402,8 @@ module Server =
return raise <| FSharpUtil.ReRaise ex
}

let connectionTimeout =
match currency with
| Currency.ETC when etcEcosystemIsMomentarilyCentralized ->
Config.DEFAULT_NETWORK_TIMEOUT + Config.DEFAULT_NETWORK_TIMEOUT
| _ ->
Config.DEFAULT_NETWORK_TIMEOUT

async {
let web3Server = Web3Server (connectionTimeout, server)
let web3Server = Web3Server (server)
try
return! HandlePossibleEtherFailures (web3ClientFunc web3Server)

Expand All @@ -429,14 +421,13 @@ module Server =
// and room for simplification to not pass a new ad-hoc delegate?
let GetServerFuncs<'R> (web3Func: SomeWeb3->Async<'R>)
(etherServers: seq<ServerDetails>)
(currency: Currency)
: seq<Server<ServerDetails,'R>> =
let Web3ServerToGenericServer (web3ClientFunc: SomeWeb3->Async<'R>)
(etherServer: ServerDetails)
: Server<ServerDetails,'R> =
{
Details = etherServer
Retrieval = Web3ServerToRetrievalFunc etherServer web3ClientFunc currency
Retrieval = Web3ServerToRetrievalFunc etherServer web3ClientFunc
}

let serverFuncs =
Expand All @@ -448,7 +439,7 @@ module Server =
(web3Func: SomeWeb3->Async<'R>)
: List<Server<ServerDetails,'R>> =
let etherServers = Web3ServerSeedList.Randomize currency
GetServerFuncs web3Func etherServers currency
GetServerFuncs web3Func etherServers
|> List.ofSeq

let GetTransactionCount (currency: Currency) (address: string)
Expand Down
23 changes: 22 additions & 1 deletion src/GWallet.Backend/Ether/TokenManager.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
namespace GWallet.Backend.Ether

open System
open System.Numerics

open Nethereum.Web3
open Nethereum.Hex.HexTypes
open Nethereum.Hex.HexConvertors.Extensions
open Nethereum.StandardTokenEIP20
open Nethereum.StandardTokenEIP20.ContractDefinition

Expand Down Expand Up @@ -45,8 +47,27 @@ module TokenManager =
failwith "Assertion failed: transactionInput's VALUE property should be equal to passed tokenAmountInWei parameter"
transactionInput.Data

member self.DecodeInputDataForTransferTransaction (data: byte[]) =
let transferFuncBuilder = self.ContractHandler.GetFunction<TransferFunction>()
let decodedInput = transferFuncBuilder.DecodeInput (data.ToHex true)

let expectedParamsCount = 2

let transferDestination, transferAmountInWei =
try
if decodedInput.Count = expectedParamsCount then
decodedInput.[0].Result :?> string,
decodedInput.[1].Result :?> BigInteger
else
failwith <| SPrintF2 "Invalid transfer function parameters count, expected %i got %i" expectedParamsCount decodedInput.Count
with
| :? System.InvalidCastException ->
failwith "Invalid transfer function parameters type"

transferDestination, transferAmountInWei

// this is a dummy instance we need in order to pass it to base class of StandardTokenService, but not
// really used online; FIXME: propose "Web3-less" overload to Nethereum
let private dummyOfflineWeb3 = Web3 Config.DEFAULT_NETWORK_TIMEOUT
let private dummyOfflineWeb3 = Web3()
type OfflineTokenServiceWrapper(currency: Currency) =
inherit TokenServiceWrapper(dummyOfflineWeb3, currency)
Loading

0 comments on commit 746a662

Please sign in to comment.