Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
May 11, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions accounts/abi/bind/backends/simulated.go
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,21 @@ 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 := new(big.Int)
balance.Set(b.pendingState.GetBalance(call.From)) // from can't be nil
if call.Value != nil {
if call.Value.Cmp(balance) >= 0 {
return 0, errors.New("insufficient funds for transfer")
}
balance.Sub(balance, call.Value)
}
allowance := new(big.Int).Div(balance, call.GasPrice)
if hi > allowance.Uint64() {
hi = allowance.Uint64()
}
}
cap = hi

// Create a helper to check if a gas allowance results in an executable transaction
Expand Down
67 changes: 67 additions & 0 deletions accounts/abi/bind/backends/simulated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
29 changes: 25 additions & 4 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -916,16 +921,32 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
}
hi = block.GasLimit()
}
// Recap the highest gas limit with account's avaliable balance.
if args.GasPrice != nil && args.GasPrice.ToInt().Uint64() != 0 {
state, _, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}
balance := new(big.Int)
balance.Set(state.GetBalance(*args.From)) // from can't be nil
if args.Value != nil {
if args.Value.ToInt().Cmp(balance) >= 0 {
return 0, errors.New("insufficient funds for transfer")
}
balance.Sub(balance, args.Value.ToInt())
}
allowance := new(big.Int).Div(balance, args.GasPrice.ToInt())
if hi > allowance.Uint64() {
hi = allowance.Uint64()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add a log here:

log.Warn("Gas estimation capped by limited funds", "original", hi, "fundable", allowance)

This way if someone hits this then they at least see some meaningful warning too, not just an estimation failure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe also add it to the simulated backend, just for completion's sake

}
}
// 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)
Expand Down