Unverified Commit 79b727bc authored by Marius van der Wijden's avatar Marius van der Wijden Committed by GitHub

accounts/abi/bind: refactor transact method (#23719)

This fixes a bug where gas-related fields of the TransactOpts passed
to transaction methods would be modified, skipping gas estimation for
subsequent transactions.
Co-authored-by: 's avatarYondon Fu <yondon.fu@gmail.com>
Co-authored-by: 's avatarFelix Lange <fjl@twurst.com>
parent 778ff947
...@@ -231,108 +231,158 @@ func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error) ...@@ -231,108 +231,158 @@ func (c *BoundContract) Transfer(opts *TransactOpts) (*types.Transaction, error)
return c.transact(opts, &c.address, nil) return c.transact(opts, &c.address, nil)
} }
// transact executes an actual transaction invocation, first deriving any missing func (c *BoundContract) createDynamicTx(opts *TransactOpts, contract *common.Address, input []byte, head *types.Header) (*types.Transaction, error) {
// authorization fields, and then scheduling the transaction for execution. // Normalize value
func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
var err error
// Ensure a valid value field and resolve the account nonce
value := opts.Value value := opts.Value
if value == nil { if value == nil {
value = new(big.Int) value = new(big.Int)
} }
var nonce uint64 // Estimate TipCap
if opts.Nonce == nil { gasTipCap := opts.GasTipCap
nonce, err = c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From) if gasTipCap == nil {
tip, err := c.transactor.SuggestGasTipCap(ensureContext(opts.Context))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve account nonce: %v", err) return nil, err
} }
} else { gasTipCap = tip
nonce = opts.Nonce.Uint64()
} }
// Figure out reasonable gas price values // Estimate FeeCap
if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) { gasFeeCap := opts.GasFeeCap
return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified") if gasFeeCap == nil {
gasFeeCap = new(big.Int).Add(
gasTipCap,
new(big.Int).Mul(head.BaseFee, big.NewInt(2)),
)
} }
head, err := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil) if gasFeeCap.Cmp(gasTipCap) < 0 {
if err != nil { return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", gasFeeCap, gasTipCap)
return nil, err
} }
if head.BaseFee != nil && opts.GasPrice == nil { // Estimate GasLimit
if opts.GasTipCap == nil { gasLimit := opts.GasLimit
tip, err := c.transactor.SuggestGasTipCap(ensureContext(opts.Context)) if opts.GasLimit == 0 {
var err error
gasLimit, err = c.estimateGasLimit(opts, contract, input, nil, gasTipCap, gasFeeCap, value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
opts.GasTipCap = tip
} }
if opts.GasFeeCap == nil { // create the transaction
gasFeeCap := new(big.Int).Add( nonce, err := c.getNonce(opts)
opts.GasTipCap, if err != nil {
new(big.Int).Mul(head.BaseFee, big.NewInt(2)), return nil, err
)
opts.GasFeeCap = gasFeeCap
} }
if opts.GasFeeCap.Cmp(opts.GasTipCap) < 0 { baseTx := &types.DynamicFeeTx{
return nil, fmt.Errorf("maxFeePerGas (%v) < maxPriorityFeePerGas (%v)", opts.GasFeeCap, opts.GasTipCap) To: contract,
Nonce: nonce,
GasFeeCap: gasFeeCap,
GasTipCap: gasTipCap,
Gas: gasLimit,
Value: value,
Data: input,
} }
} else { return types.NewTx(baseTx), nil
}
func (c *BoundContract) createLegacyTx(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
if opts.GasFeeCap != nil || opts.GasTipCap != nil { if opts.GasFeeCap != nil || opts.GasTipCap != nil {
return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet") return nil, errors.New("maxFeePerGas or maxPriorityFeePerGas specified but london is not active yet")
} }
if opts.GasPrice == nil { // Normalize value
value := opts.Value
if value == nil {
value = new(big.Int)
}
// Estimate GasPrice
gasPrice := opts.GasPrice
if gasPrice == nil {
price, err := c.transactor.SuggestGasPrice(ensureContext(opts.Context)) price, err := c.transactor.SuggestGasPrice(ensureContext(opts.Context))
if err != nil { if err != nil {
return nil, err return nil, err
} }
opts.GasPrice = price gasPrice = price
}
} }
// Estimate GasLimit
gasLimit := opts.GasLimit gasLimit := opts.GasLimit
if gasLimit == 0 { if opts.GasLimit == 0 {
// Gas estimation cannot succeed without code for method invocations var err error
if contract != nil { gasLimit, err = c.estimateGasLimit(opts, contract, input, gasPrice, nil, nil, value)
if code, err := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.address); err != nil { if err != nil {
return nil, err return nil, err
} else if len(code) == 0 {
return nil, ErrNoCode
} }
} }
// If the contract surely has code (or code is not needed), estimate the transaction // create the transaction
msg := ethereum.CallMsg{From: opts.From, To: contract, GasPrice: opts.GasPrice, GasTipCap: opts.GasTipCap, GasFeeCap: opts.GasFeeCap, Value: value, Data: input} nonce, err := c.getNonce(opts)
gasLimit, err = c.transactor.EstimateGas(ensureContext(opts.Context), msg)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to estimate gas needed: %v", err) return nil, err
}
} }
// Create the transaction, sign it and schedule it for execution
var rawTx *types.Transaction
if opts.GasFeeCap == nil {
baseTx := &types.LegacyTx{ baseTx := &types.LegacyTx{
To: contract,
Nonce: nonce, Nonce: nonce,
GasPrice: opts.GasPrice, GasPrice: gasPrice,
Gas: gasLimit, Gas: gasLimit,
Value: value, Value: value,
Data: input, Data: input,
} }
return types.NewTx(baseTx), nil
}
func (c *BoundContract) estimateGasLimit(opts *TransactOpts, contract *common.Address, input []byte, gasPrice, gasTipCap, gasFeeCap, value *big.Int) (uint64, error) {
if contract != nil { if contract != nil {
baseTx.To = &c.address // Gas estimation cannot succeed without code for method invocations.
if code, err := c.transactor.PendingCodeAt(ensureContext(opts.Context), c.address); err != nil {
return 0, err
} else if len(code) == 0 {
return 0, ErrNoCode
} }
rawTx = types.NewTx(baseTx) }
} else { msg := ethereum.CallMsg{
baseTx := &types.DynamicFeeTx{ From: opts.From,
Nonce: nonce, To: contract,
GasFeeCap: opts.GasFeeCap, GasPrice: gasPrice,
GasTipCap: opts.GasTipCap, GasTipCap: gasTipCap,
Gas: gasLimit, GasFeeCap: gasFeeCap,
Value: value, Value: value,
Data: input, Data: input,
} }
if contract != nil { return c.transactor.EstimateGas(ensureContext(opts.Context), msg)
baseTx.To = &c.address }
func (c *BoundContract) getNonce(opts *TransactOpts) (uint64, error) {
if opts.Nonce == nil {
return c.transactor.PendingNonceAt(ensureContext(opts.Context), opts.From)
} else {
return opts.Nonce.Uint64(), nil
}
}
// transact executes an actual transaction invocation, first deriving any missing
// authorization fields, and then scheduling the transaction for execution.
func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, input []byte) (*types.Transaction, error) {
if opts.GasPrice != nil && (opts.GasFeeCap != nil || opts.GasTipCap != nil) {
return nil, errors.New("both gasPrice and (maxFeePerGas or maxPriorityFeePerGas) specified")
} }
rawTx = types.NewTx(baseTx) // Create the transaction
var (
rawTx *types.Transaction
err error
)
if opts.GasPrice != nil {
rawTx, err = c.createLegacyTx(opts, contract, input)
} else {
// Only query for basefee if gasPrice not specified
if head, errHead := c.transactor.HeaderByNumber(ensureContext(opts.Context), nil); err != nil {
return nil, errHead
} else if head.BaseFee != nil {
rawTx, err = c.createDynamicTx(opts, contract, input, head)
} else {
// Chain is not London ready -> use legacy transaction
rawTx, err = c.createLegacyTx(opts, contract, input)
}
}
if err != nil {
return nil, err
} }
// Sign the transaction and schedule it for execution
if opts.Signer == nil { if opts.Signer == nil {
return nil, errors.New("no signer to authorize the transaction with") return nil, errors.New("no signer to authorize the transaction with")
} }
......
...@@ -31,8 +31,49 @@ import ( ...@@ -31,8 +31,49 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/stretchr/testify/assert"
) )
func mockSign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { return tx, nil }
type mockTransactor struct {
baseFee *big.Int
gasTipCap *big.Int
gasPrice *big.Int
suggestGasTipCapCalled bool
suggestGasPriceCalled bool
}
func (mt *mockTransactor) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
return &types.Header{BaseFee: mt.baseFee}, nil
}
func (mt *mockTransactor) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) {
return []byte{1}, nil
}
func (mt *mockTransactor) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
return 0, nil
}
func (mt *mockTransactor) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
mt.suggestGasPriceCalled = true
return mt.gasPrice, nil
}
func (mt *mockTransactor) SuggestGasTipCap(ctx context.Context) (*big.Int, error) {
mt.suggestGasTipCapCalled = true
return mt.gasTipCap, nil
}
func (mt *mockTransactor) EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error) {
return 0, nil
}
func (mt *mockTransactor) SendTransaction(ctx context.Context, tx *types.Transaction) error {
return nil
}
type mockCaller struct { type mockCaller struct {
codeAtBlockNumber *big.Int codeAtBlockNumber *big.Int
callContractBlockNumber *big.Int callContractBlockNumber *big.Int
...@@ -226,6 +267,51 @@ func TestUnpackIndexedBytesTyLogIntoMap(t *testing.T) { ...@@ -226,6 +267,51 @@ func TestUnpackIndexedBytesTyLogIntoMap(t *testing.T) {
unpackAndCheck(t, bc, expectedReceivedMap, mockLog) unpackAndCheck(t, bc, expectedReceivedMap, mockLog)
} }
func TestTransactGasFee(t *testing.T) {
assert := assert.New(t)
// GasTipCap and GasFeeCap
// When opts.GasTipCap and opts.GasFeeCap are nil
mt := &mockTransactor{baseFee: big.NewInt(100), gasTipCap: big.NewInt(5)}
bc := bind.NewBoundContract(common.Address{}, abi.ABI{}, nil, mt, nil)
opts := &bind.TransactOpts{Signer: mockSign}
tx, err := bc.Transact(opts, "")
assert.Nil(err)
assert.Equal(big.NewInt(5), tx.GasTipCap())
assert.Equal(big.NewInt(205), tx.GasFeeCap())
assert.Nil(opts.GasTipCap)
assert.Nil(opts.GasFeeCap)
assert.True(mt.suggestGasTipCapCalled)
// Second call to Transact should use latest suggested GasTipCap
mt.gasTipCap = big.NewInt(6)
mt.suggestGasTipCapCalled = false
tx, err = bc.Transact(opts, "")
assert.Nil(err)
assert.Equal(big.NewInt(6), tx.GasTipCap())
assert.Equal(big.NewInt(206), tx.GasFeeCap())
assert.True(mt.suggestGasTipCapCalled)
// GasPrice
// When opts.GasPrice is nil
mt = &mockTransactor{gasPrice: big.NewInt(5)}
bc = bind.NewBoundContract(common.Address{}, abi.ABI{}, nil, mt, nil)
opts = &bind.TransactOpts{Signer: mockSign}
tx, err = bc.Transact(opts, "")
assert.Nil(err)
assert.Equal(big.NewInt(5), tx.GasPrice())
assert.Nil(opts.GasPrice)
assert.True(mt.suggestGasPriceCalled)
// Second call to Transact should use latest suggested GasPrice
mt.gasPrice = big.NewInt(6)
mt.suggestGasPriceCalled = false
tx, err = bc.Transact(opts, "")
assert.Nil(err)
assert.Equal(big.NewInt(6), tx.GasPrice())
assert.True(mt.suggestGasPriceCalled)
}
func unpackAndCheck(t *testing.T, bc *bind.BoundContract, expected map[string]interface{}, mockLog types.Log) { func unpackAndCheck(t *testing.T, bc *bind.BoundContract, expected map[string]interface{}, mockLog types.Log) {
received := make(map[string]interface{}) received := make(map[string]interface{})
if err := bc.UnpackLogIntoMap(received, "received", mockLog); err != nil { if err := bc.UnpackLogIntoMap(received, "received", mockLog); err != nil {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment