From 263622f44f45eb3e8be6dde4ed030f7f5717066b Mon Sep 17 00:00:00 2001
From: gary rong <garyrong0905@gmail.com>
Date: Mon, 11 May 2020 16:08:20 +0800
Subject: [PATCH] accounts/abi/bind/backend, internal/ethapi: recap gas limit
 with balance (#21043)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* accounts/abi/bind/backend, internal/ethapi: recap gas limit with balance

* accounts, internal: address comment and fix lint

* accounts, internal: extend log message

* tiny nits to format hexutil.Big and nil properly

Co-authored-by: Péter Szilágyi <peterke@gmail.com>
---
 accounts/abi/bind/backends/simulated.go      | 22 +++++++
 accounts/abi/bind/backends/simulated_test.go | 67 ++++++++++++++++++++
 internal/ethapi/api.go                       | 35 ++++++++--
 3 files changed, 120 insertions(+), 4 deletions(-)

diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go
index 83d0db05b..cfbe2914d 100644
--- a/accounts/abi/bind/backends/simulated.go
+++ b/accounts/abi/bind/backends/simulated.go
@@ -39,6 +39,7 @@ import (
 	"github.com/ethereum/go-ethereum/eth/filters"
 	"github.com/ethereum/go-ethereum/ethdb"
 	"github.com/ethereum/go-ethereum/event"
+	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/params"
 	"github.com/ethereum/go-ethereum/rpc"
 )
@@ -401,6 +402,27 @@ func (b *SimulatedBackend) EstimateGas(ctx context.Context, call ethereum.CallMs
 	} else {
 		hi = b.pendingBlock.GasLimit()
 	}
+	// Recap the highest gas allowance with account's balance.
+	if call.GasPrice != nil && call.GasPrice.Uint64() != 0 {
+		balance := b.pendingState.GetBalance(call.From) // from can't be nil
+		available := new(big.Int).Set(balance)
+		if call.Value != nil {
+			if call.Value.Cmp(available) >= 0 {
+				return 0, errors.New("insufficient funds for transfer")
+			}
+			available.Sub(available, call.Value)
+		}
+		allowance := new(big.Int).Div(available, call.GasPrice)
+		if hi > allowance.Uint64() {
+			transfer := call.Value
+			if transfer == nil {
+				transfer = new(big.Int)
+			}
+			log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
+				"sent", transfer, "gasprice", call.GasPrice, "fundable", allowance)
+			hi = allowance.Uint64()
+		}
+	}
 	cap = hi
 
 	// Create a helper to check if a gas allowance results in an executable transaction
diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go
index a28f99aea..a55b4460a 100644
--- a/accounts/abi/bind/backends/simulated_test.go
+++ b/accounts/abi/bind/backends/simulated_test.go
@@ -466,6 +466,73 @@ func TestSimulatedBackend_EstimateGas(t *testing.T) {
 	}
 }
 
+func TestSimulatedBackend_EstimateGasWithPrice(t *testing.T) {
+	key, _ := crypto.GenerateKey()
+	addr := crypto.PubkeyToAddress(key.PublicKey)
+
+	sim := NewSimulatedBackend(core.GenesisAlloc{addr: {Balance: big.NewInt(params.Ether*2 + 2e17)}}, 10000000)
+	defer sim.Close()
+
+	receipant := common.HexToAddress("deadbeef")
+	var cases = []struct {
+		name        string
+		message     ethereum.CallMsg
+		expect      uint64
+		expectError error
+	}{
+		{"EstimateWithoutPrice", ethereum.CallMsg{
+			From:     addr,
+			To:       &receipant,
+			Gas:      0,
+			GasPrice: big.NewInt(0),
+			Value:    big.NewInt(1000),
+			Data:     nil,
+		}, 21000, nil},
+
+		{"EstimateWithPrice", ethereum.CallMsg{
+			From:     addr,
+			To:       &receipant,
+			Gas:      0,
+			GasPrice: big.NewInt(1000),
+			Value:    big.NewInt(1000),
+			Data:     nil,
+		}, 21000, nil},
+
+		{"EstimateWithVeryHighPrice", ethereum.CallMsg{
+			From:     addr,
+			To:       &receipant,
+			Gas:      0,
+			GasPrice: big.NewInt(1e14), // gascost = 2.1ether
+			Value:    big.NewInt(1e17), // the remaining balance for fee is 2.1ether
+			Data:     nil,
+		}, 21000, nil},
+
+		{"EstimateWithSuperhighPrice", ethereum.CallMsg{
+			From:     addr,
+			To:       &receipant,
+			Gas:      0,
+			GasPrice: big.NewInt(2e14), // gascost = 4.2ether
+			Value:    big.NewInt(1000),
+			Data:     nil,
+		}, 21000, errors.New("gas required exceeds allowance (10999)")}, // 10999=(2.2ether-1000wei)/(2e14)
+	}
+	for _, c := range cases {
+		got, err := sim.EstimateGas(context.Background(), c.message)
+		if c.expectError != nil {
+			if err == nil {
+				t.Fatalf("Expect error, got nil")
+			}
+			if c.expectError.Error() != err.Error() {
+				t.Fatalf("Expect error, want %v, got %v", c.expectError, err)
+			}
+			continue
+		}
+		if got != c.expect {
+			t.Fatalf("Gas estimation mismatch, want %d, got %d", c.expect, got)
+		}
+	}
+}
+
 func TestSimulatedBackend_HeaderByHash(t *testing.T) {
 	testAddr := crypto.PubkeyToAddress(testKey.PublicKey)
 
diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go
index febfcb224..45240aa2c 100644
--- a/internal/ethapi/api.go
+++ b/internal/ethapi/api.go
@@ -906,6 +906,11 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
 		hi  uint64
 		cap uint64
 	)
+	// Use zero address if sender unspecified.
+	if args.From == nil {
+		args.From = new(common.Address)
+	}
+	// Determine the highest gas limit can be used during the estimation.
 	if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
 		hi = uint64(*args.Gas)
 	} else {
@@ -916,16 +921,38 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
 		}
 		hi = block.GasLimit()
 	}
+	// Recap the highest gas limit with account's available balance.
+	if args.GasPrice != nil && args.GasPrice.ToInt().Uint64() != 0 {
+		state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
+		if err != nil {
+			return 0, err
+		}
+		balance := state.GetBalance(*args.From) // from can't be nil
+		available := new(big.Int).Set(balance)
+		if args.Value != nil {
+			if args.Value.ToInt().Cmp(available) >= 0 {
+				return 0, errors.New("insufficient funds for transfer")
+			}
+			available.Sub(available, args.Value.ToInt())
+		}
+		allowance := new(big.Int).Div(available, args.GasPrice.ToInt())
+		if hi > allowance.Uint64() {
+			transfer := args.Value
+			if transfer == nil {
+				transfer = new(hexutil.Big)
+			}
+			log.Warn("Gas estimation capped by limited funds", "original", hi, "balance", balance,
+				"sent", transfer.ToInt(), "gasprice", args.GasPrice.ToInt(), "fundable", allowance)
+			hi = allowance.Uint64()
+		}
+	}
+	// Recap the highest gas allowance with specified gascap.
 	if gasCap != nil && hi > gasCap.Uint64() {
 		log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
 		hi = gasCap.Uint64()
 	}
 	cap = hi
 
-	// Use zero address if sender unspecified.
-	if args.From == nil {
-		args.From = new(common.Address)
-	}
 	// Create a helper to check if a gas allowance results in an executable transaction
 	executable := func(gas uint64) (bool, *core.ExecutionResult, error) {
 		args.Gas = (*hexutil.Uint64)(&gas)
-- 
GitLab