diff --git a/Makefile b/Makefile index 80eee3ae14..4765c79baa 100644 --- a/Makefile +++ b/Makefile @@ -11,14 +11,21 @@ mod-tidy: contracts: cd contracts && \ - solc --bin --abi --overwrite -o . ./SimpleERC20.sol + solc --bin --abi --overwrite -o . ./SimpleERC20.sol && \ + solc --bin --abi --overwrite -o . ./Failure.sol .PHONY: contracts bindings: contracts abigen \ - --abi ./contracts/SimpleERC20.abi \ - --bin ./contracts/SimpleERC20.bin \ - --pkg bindings \ - --type "SimpleERC20" \ - --out ./optimism/bindings/simple_erc20.go + --abi ./contracts/SimpleERC20.abi \ + --bin ./contracts/SimpleERC20.bin \ + --pkg bindings \ + --type "SimpleERC20" \ + --out ./optimism/bindings/simple_erc20.go + abigen \ + --abi ./contracts/Failure.abi \ + --bin ./contracts/Failure.bin \ + --pkg bindings \ + --type "Failure" \ + --out ./optimism/bindings/failure.go .PHONY: bindings \ No newline at end of file diff --git a/contracts/Failure.abi b/contracts/Failure.abi new file mode 100644 index 0000000000..868236489b --- /dev/null +++ b/contracts/Failure.abi @@ -0,0 +1 @@ +[{"inputs":[],"name":"fail","outputs":[],"stateMutability":"payable","type":"function"}] \ No newline at end of file diff --git a/contracts/Failure.bin b/contracts/Failure.bin new file mode 100644 index 0000000000..c6f9cdbb77 --- /dev/null +++ b/contracts/Failure.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b5061010f806100206000396000f3fe608060405260043610601c5760003560e01c8063a9cc4718146021575b600080fd5b60276029565b005b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040160599060bb565b60405180910390fd5b600082825260208201905092915050565b7f626967206661696c210000000000000000000000000000000000000000000000600082015250565b600060a76009836062565b915060b0826073565b602082019050919050565b6000602082019050818103600083015260d281609c565b905091905056fea264697066735822122068a0ae7d220da2e9f14cc48aec07eb227130c4a4ae2d89dca9c16c132f2c64fe64736f6c634300080d0033 \ No newline at end of file diff --git a/contracts/Failure.sol b/contracts/Failure.sol new file mode 100644 index 0000000000..c9e1102c47 --- /dev/null +++ b/contracts/Failure.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.9; + +contract Failure { + function fail() public payable { + revert("big fail!"); + } +} diff --git a/optimism/bindings/failure.go b/optimism/bindings/failure.go new file mode 100644 index 0000000000..052645c1b1 --- /dev/null +++ b/optimism/bindings/failure.go @@ -0,0 +1,223 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package bindings + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// FailureMetaData contains all meta data concerning the Failure contract. +var FailureMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"fail\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b5061010f806100206000396000f3fe608060405260043610601c5760003560e01c8063a9cc4718146021575b600080fd5b60276029565b005b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040160599060bb565b60405180910390fd5b600082825260208201905092915050565b7f626967206661696c210000000000000000000000000000000000000000000000600082015250565b600060a76009836062565b915060b0826073565b602082019050919050565b6000602082019050818103600083015260d281609c565b905091905056fea264697066735822122068a0ae7d220da2e9f14cc48aec07eb227130c4a4ae2d89dca9c16c132f2c64fe64736f6c634300080d0033", +} + +// FailureABI is the input ABI used to generate the binding from. +// Deprecated: Use FailureMetaData.ABI instead. +var FailureABI = FailureMetaData.ABI + +// FailureBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use FailureMetaData.Bin instead. +var FailureBin = FailureMetaData.Bin + +// DeployFailure deploys a new Ethereum contract, binding an instance of Failure to it. +func DeployFailure(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *Failure, error) { + parsed, err := FailureMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(FailureBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &Failure{FailureCaller: FailureCaller{contract: contract}, FailureTransactor: FailureTransactor{contract: contract}, FailureFilterer: FailureFilterer{contract: contract}}, nil +} + +// Failure is an auto generated Go binding around an Ethereum contract. +type Failure struct { + FailureCaller // Read-only binding to the contract + FailureTransactor // Write-only binding to the contract + FailureFilterer // Log filterer for contract events +} + +// FailureCaller is an auto generated read-only Go binding around an Ethereum contract. +type FailureCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FailureTransactor is an auto generated write-only Go binding around an Ethereum contract. +type FailureTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FailureFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type FailureFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// FailureSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type FailureSession struct { + Contract *Failure // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FailureCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type FailureCallerSession struct { + Contract *FailureCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// FailureTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type FailureTransactorSession struct { + Contract *FailureTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// FailureRaw is an auto generated low-level Go binding around an Ethereum contract. +type FailureRaw struct { + Contract *Failure // Generic contract binding to access the raw methods on +} + +// FailureCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type FailureCallerRaw struct { + Contract *FailureCaller // Generic read-only contract binding to access the raw methods on +} + +// FailureTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type FailureTransactorRaw struct { + Contract *FailureTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewFailure creates a new instance of Failure, bound to a specific deployed contract. +func NewFailure(address common.Address, backend bind.ContractBackend) (*Failure, error) { + contract, err := bindFailure(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Failure{FailureCaller: FailureCaller{contract: contract}, FailureTransactor: FailureTransactor{contract: contract}, FailureFilterer: FailureFilterer{contract: contract}}, nil +} + +// NewFailureCaller creates a new read-only instance of Failure, bound to a specific deployed contract. +func NewFailureCaller(address common.Address, caller bind.ContractCaller) (*FailureCaller, error) { + contract, err := bindFailure(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &FailureCaller{contract: contract}, nil +} + +// NewFailureTransactor creates a new write-only instance of Failure, bound to a specific deployed contract. +func NewFailureTransactor(address common.Address, transactor bind.ContractTransactor) (*FailureTransactor, error) { + contract, err := bindFailure(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &FailureTransactor{contract: contract}, nil +} + +// NewFailureFilterer creates a new log filterer instance of Failure, bound to a specific deployed contract. +func NewFailureFilterer(address common.Address, filterer bind.ContractFilterer) (*FailureFilterer, error) { + contract, err := bindFailure(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &FailureFilterer{contract: contract}, nil +} + +// bindFailure binds a generic wrapper to an already deployed contract. +func bindFailure(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(FailureABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Failure *FailureRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Failure.Contract.FailureCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Failure *FailureRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Failure.Contract.FailureTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Failure *FailureRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Failure.Contract.FailureTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Failure *FailureCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Failure.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Failure *FailureTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Failure.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Failure *FailureTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Failure.Contract.contract.Transact(opts, method, params...) +} + +// Fail is a paid mutator transaction binding the contract method 0xa9cc4718. +// +// Solidity: function fail() payable returns() +func (_Failure *FailureTransactor) Fail(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Failure.contract.Transact(opts, "fail") +} + +// Fail is a paid mutator transaction binding the contract method 0xa9cc4718. +// +// Solidity: function fail() payable returns() +func (_Failure *FailureSession) Fail() (*types.Transaction, error) { + return _Failure.Contract.Fail(&_Failure.TransactOpts) +} + +// Fail is a paid mutator transaction binding the contract method 0xa9cc4718. +// +// Solidity: function fail() payable returns() +func (_Failure *FailureTransactorSession) Fail() (*types.Transaction, error) { + return _Failure.Contract.Fail(&_Failure.TransactOpts) +} diff --git a/optimism/bindings/simple_erc20.go b/optimism/bindings/simple_erc20.go index bbcd526379..77a41779ab 100644 --- a/optimism/bindings/simple_erc20.go +++ b/optimism/bindings/simple_erc20.go @@ -8,7 +8,7 @@ import ( "math/big" "strings" - "github.com/ethereum/go-ethereum" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" diff --git a/simulators/optimism/l1ops/deposit_tests.go b/simulators/optimism/l1ops/deposit_tests.go index a4c1826714..8d01f7a1c1 100644 --- a/simulators/optimism/l1ops/deposit_tests.go +++ b/simulators/optimism/l1ops/deposit_tests.go @@ -4,12 +4,14 @@ import ( "encoding/json" "fmt" "math/big" + "math/rand" "time" "github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-node/testutils" "github.com/ethereum-optimism/optimism/op-node/withdrawals" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -36,7 +38,7 @@ func simplePortalDepositTest(t *hivesim.T, env *optimism.TestEnv) { require.EqualValues(t, 0, startBalance.Int64()) mintAmount := big.NewInt(0.5 * params.Ether) - doDeposit(t, env, depositor, mintAmount) + doDeposit(t, env, depositor, mintAmount, false, nil) endBalance, err := l2.BalanceAt(env.Ctx(), depositor, nil) require.Nil(t, err) @@ -105,7 +107,7 @@ func erc20RoundtripTest(t *hivesim.T, env *optimism.TestEnv) { require.NoError(t, err) // Deposit some ETH onto L2 - doDeposit(t, env, depositor, big.NewInt(0.5*params.Ether)) + doDeposit(t, env, depositor, big.NewInt(0.5*params.Ether), false, nil) // Deploy the bridged ERC20 l2Opts := l2Vault.KeyedTransactor(depositor) @@ -270,13 +272,75 @@ func erc20RoundtripTest(t *hivesim.T, env *optimism.TestEnv) { require.EqualValues(t, big.NewInt(500), balL2) } -func doDeposit(t *hivesim.T, env *optimism.TestEnv, depositor common.Address, mintAmount *big.Int) { - depositContract, err := bindings.NewOptimismPortal( - env.Devnet.Deployments.DeploymentsL1.OptimismPortalProxy, - env.Devnet.L1Client(0), - ) +func failingDepositWithMintTest(t *hivesim.T, env *optimism.TestEnv) { + // Initial setup + l1 := env.Devnet.L1Client(0) + l2 := env.Devnet.L2Client(0) + l1Vault := env.Devnet.L1Vault + l2Vault := env.Devnet.L2Vault + depositContract := env.Devnet.Bindings.BindingsL1.OptimismPortal + depositor := l1Vault.CreateAccount(env.TimeoutCtx(time.Minute), l1, big.NewInt(3*params.Ether)) + l2Vault.InsertKey(l1Vault.FindKey(depositor)) + l2Opts := l2Vault.KeyedTransactor(depositor) + + // Fund account on L2 + doDeposit(t, env, depositor, big.NewInt(0.5*params.Ether), false, nil) + + // Deploy the failure contract on L2 + + _, deployTx, failureContract, err := hivebindings.DeployFailure(l2Opts, l2) + require.NoError(t, err) + _, err = optimism.WaitReceipt(env.TimeoutCtx(30*time.Second), l2, deployTx.Hash()) require.NoError(t, err) + // Create the revert() call + l2Opts.NoSend = true + l2Opts.GasLimit = 1_000_000 + revertTx, err := failureContract.Fail(l2Opts) + require.NoError(t, err) + + // Create garbage data + randData := make([]byte, 32) + _, err = rand.Read(randData) + require.NoError(t, err) + + testData := [][]byte{ + randData, + revertTx.Data(), + } + mintAmount := big.NewInt(0.5 * params.Ether) + opts := l1Vault.KeyedTransactor(depositor) + opts.Value = mintAmount + opts.GasLimit = 3_000_000 + for _, data := range testData { + startBal, err := l2.BalanceAt(env.Ctx(), depositor, nil) + require.NoError(t, err) + tx, err := depositContract.DepositTransaction( + opts, + depositor, + mintAmount, + 1_000_000, + false, + data, + ) + require.NoError(t, err) + receipt, err := optimism.WaitReceipt(env.TimeoutCtx(time.Minute), l1, tx.Hash()) + require.NoError(t, err) + + reconstructedDep, err := derive.UnmarshalDepositLogEvent(receipt.Logs[0]) + require.NoError(t, err, "could not reconstruct L2 deposit") + tx = types.NewTx(reconstructedDep) + _, err = optimism.WaitReceipt(env.TimeoutCtx(45*time.Second), l2, tx.Hash()) + require.NoError(t, err) + + endBal, err := l2.BalanceAt(env.Ctx(), depositor, nil) + require.Nil(t, err) + require.True(t, testutils.BigEqual(mintAmount, new(big.Int).Sub(endBal, startBal))) + } +} + +func doDeposit(t *hivesim.T, env *optimism.TestEnv, depositor common.Address, mintAmount *big.Int, isCreation bool, data []byte) { + depositContract := env.Devnet.Bindings.BindingsL1.OptimismPortal l1 := env.Devnet.L1Client(0) l2 := env.Devnet.L2Client(0) l1Vault := env.Devnet.L1Vault @@ -287,7 +351,7 @@ func doDeposit(t *hivesim.T, env *optimism.TestEnv, depositor common.Address, mi // can sometimes be off by a bit as a result of the resource // metering code. opts.GasLimit = 3_000_000 - tx, err := depositContract.DepositTransaction(opts, depositor, common.Big0, 1_000_000, false, nil) + tx, err := depositContract.DepositTransaction(opts, depositor, common.Big0, 1_000_000, isCreation, data) require.NoError(t, err) receipt, err := optimism.WaitReceipt(env.TimeoutCtx(time.Minute), l1, tx.Hash()) require.NoError(t, err) diff --git a/simulators/optimism/l1ops/main.go b/simulators/optimism/l1ops/main.go index f8bf32e2a4..cf2dbd314e 100644 --- a/simulators/optimism/l1ops/main.go +++ b/simulators/optimism/l1ops/main.go @@ -14,6 +14,7 @@ var tests = []*optimism.TestSpec{ {Name: "deposit contract creation through the portal", Run: contractPortalDepositTest}, {Name: "erc20 roundtrip through the bridge", Run: erc20RoundtripTest}, {Name: "simple withdrawal", Run: simpleWithdrawalTest}, + {Name: "failing deposit with mint", Run: failingDepositWithMintTest}, } func main() { diff --git a/simulators/optimism/l1ops/withdrawal_tests.go b/simulators/optimism/l1ops/withdrawal_tests.go index e4de79f69d..78c1966300 100644 --- a/simulators/optimism/l1ops/withdrawal_tests.go +++ b/simulators/optimism/l1ops/withdrawal_tests.go @@ -1,6 +1,9 @@ package main import ( + "math/big" + "time" + "github.com/ethereum-optimism/optimism/op-bindings/bindings" "github.com/ethereum-optimism/optimism/op-bindings/predeploys" "github.com/ethereum-optimism/optimism/op-node/withdrawals" @@ -9,8 +12,6 @@ import ( "github.com/ethereum/hive/hivesim" "github.com/ethereum/hive/optimism" "github.com/stretchr/testify/require" - "math/big" - "time" ) func simpleWithdrawalTest(t *hivesim.T, env *optimism.TestEnv) { @@ -22,7 +23,7 @@ func simpleWithdrawalTest(t *hivesim.T, env *optimism.TestEnv) { l2Vault.InsertKey(l1Vault.FindKey(depositor)) mintAmount := big.NewInt(0.5 * params.Ether) - doDeposit(t, env, depositor, mintAmount) + doDeposit(t, env, depositor, mintAmount, false, nil) l2Wd, err := bindings.NewL2ToL1MessagePasser(predeploys.L2ToL1MessagePasserAddr, l2) require.Nil(t, err, "binding withdrawer on L2")