diff --git a/op-e2e/system/gastoken/gastoken_test.go b/op-e2e/system/gastoken/gastoken_test.go new file mode 100644 index 0000000000000..8fde994d2f8f9 --- /dev/null +++ b/op-e2e/system/gastoken/gastoken_test.go @@ -0,0 +1,624 @@ +package gastoken + +import ( + "context" + "math/big" + "testing" + "time" + + "github.com/ethereum-optimism/optimism/op-e2e/config" + + op_e2e "github.com/ethereum-optimism/optimism/op-e2e" + + "github.com/ethereum-optimism/optimism/op-e2e/system/e2esys" + "github.com/ethereum-optimism/optimism/op-e2e/system/helpers" + + "github.com/ethereum-optimism/optimism/op-e2e/bindings" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/receipts" + "github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait" + "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum-optimism/optimism/op-service/predeploys" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// setup expectations using custom gas token +type cgtTestExpectations struct { + tokenAddress common.Address + tokenName string + tokenSymbol string + tokenDecimals uint8 +} + +func TestMain(m *testing.M) { + op_e2e.RunMain(m) +} + +func TestCustomGasToken_L2OO(t *testing.T) { + testCustomGasToken(t, config.AllocTypeL2OO) +} + +func TestCustomGasToken_Standard(t *testing.T) { + // t.Skip("Custom gas token not supported") + testCustomGasToken(t, config.AllocTypeStandard) +} + +func testCustomGasToken(t *testing.T, allocType config.AllocType) { + op_e2e.InitParallel(t) + + disabledExpectations := cgtTestExpectations{ + common.HexToAddress("0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"), + "Ether", + "ETH", + uint8(18), + } + + setup := func(t *testing.T) gasTokenTestOpts { + cfg := e2esys.DefaultSystemConfig(t, e2esys.WithAllocType(allocType)) + offset := hexutil.Uint64(0) + cfg.DeployConfig.L2GenesisRegolithTimeOffset = &offset + cfg.DeployConfig.L1CancunTimeOffset = &offset + cfg.DeployConfig.L2GenesisCanyonTimeOffset = &offset + cfg.DeployConfig.L2GenesisDeltaTimeOffset = &offset + cfg.DeployConfig.L2GenesisEcotoneTimeOffset = &offset + + sys, err := cfg.Start(t) + require.NoError(t, err, "Error starting up system") + + l1Client := sys.NodeClient("l1") + aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) + require.NoError(t, err) + + // Deploy WETH9, we'll use this as our custom gas token for the purpose of the test + weth9Address, tx, weth9, err := bindings.DeployWETH9(aliceOpts, l1Client) + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + + enabledExpectations := cgtTestExpectations{} + enabledExpectations.tokenAddress = weth9Address + enabledExpectations.tokenName, err = weth9.Name(&bind.CallOpts{}) + require.NoError(t, err) + enabledExpectations.tokenSymbol, err = weth9.Symbol(&bind.CallOpts{}) + require.NoError(t, err) + enabledExpectations.tokenDecimals, err = weth9.Decimals(&bind.CallOpts{}) + require.NoError(t, err) + + // Get some WETH + aliceOpts.Value = big.NewInt(10_000_000) + tx, err = weth9.Deposit(aliceOpts) + waitForTx(t, tx, err, l1Client) + aliceOpts.Value = nil + newBalance, err := weth9.BalanceOf(&bind.CallOpts{}, aliceOpts.From) + require.NoError(t, err) + require.Equal(t, newBalance, big.NewInt(10_000_000)) + + return gasTokenTestOpts{ + aliceOpts: aliceOpts, + cfg: cfg, + weth9: weth9, + weth9Address: weth9Address, + allocType: allocType, + sys: sys, + enabledExpectations: enabledExpectations, + disabledExpectations: disabledExpectations, + } + } + + t.Run("deposit", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup(t) + checkDeposit(t, gto, false) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address, allocType) + checkDeposit(t, gto, true) + }) + + t.Run("withdrawal", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup(t) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address, allocType) + checkDeposit(t, gto, true) + checkWithdrawal(t, gto) + }) + + t.Run("fee withdrawal", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup(t) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address, allocType) + checkDeposit(t, gto, true) + checkFeeWithdrawal(t, gto, true) + }) + + t.Run("token name and symbol", func(t *testing.T) { + op_e2e.InitParallel(t) + gto := setup(t) + checkL1TokenNameAndSymbol(t, gto, gto.disabledExpectations) + checkL2TokenNameAndSymbol(t, gto, gto.disabledExpectations) + checkWETHTokenNameAndSymbol(t, gto, gto.disabledExpectations) + setCustomGasToken(t, gto.cfg, gto.sys, gto.weth9Address, allocType) + checkL1TokenNameAndSymbol(t, gto, gto.enabledExpectations) + checkL2TokenNameAndSymbol(t, gto, gto.enabledExpectations) + checkWETHTokenNameAndSymbol(t, gto, gto.enabledExpectations) + }) +} + +// setCustomGasToken enables the Custom Gas Token feature on a chain where it wasn't enabled at genesis. +// It reads existing parameters from the SystemConfig contract, inserts the supplied cgtAddress and reinitializes that contract. +// To do this it uses the ProxyAdmin and StorageSetter from the supplied cfg. +func setCustomGasToken(t *testing.T, cfg e2esys.SystemConfig, sys *e2esys.System, cgtAddress common.Address, allocType config.AllocType) { + l1Client := sys.NodeClient("l1") + deployerOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Deployer, cfg.L1ChainIDBig()) + require.NoError(t, err) + + // Bind a SystemConfig at the SystemConfigProxy address + systemConfig, err := bindings.NewSystemConfig(cfg.L1Deployments.SystemConfigProxy, l1Client) + require.NoError(t, err) + + // Get existing parameters from SystemConfigProxy contract + owner, err := systemConfig.Owner(&bind.CallOpts{}) + require.NoError(t, err) + basefeeScalar, err := systemConfig.BasefeeScalar(&bind.CallOpts{}) + require.NoError(t, err) + blobbasefeeScalar, err := systemConfig.BlobbasefeeScalar(&bind.CallOpts{}) + require.NoError(t, err) + batcherHash, err := systemConfig.BatcherHash(&bind.CallOpts{}) + require.NoError(t, err) + gasLimit, err := systemConfig.GasLimit(&bind.CallOpts{}) + require.NoError(t, err) + unsafeBlockSigner, err := systemConfig.UnsafeBlockSigner(&bind.CallOpts{}) + require.NoError(t, err) + resourceConfig, err := systemConfig.ResourceConfig(&bind.CallOpts{}) + require.NoError(t, err) + batchInbox, err := systemConfig.BatchInbox(&bind.CallOpts{}) + require.NoError(t, err) + addresses := bindings.SystemConfigAddresses{} + addresses.L1CrossDomainMessenger, err = systemConfig.L1CrossDomainMessenger(&bind.CallOpts{}) + require.NoError(t, err) + addresses.L1ERC721Bridge, err = systemConfig.L1ERC721Bridge(&bind.CallOpts{}) + require.NoError(t, err) + addresses.L1StandardBridge, err = systemConfig.L1StandardBridge(&bind.CallOpts{}) + require.NoError(t, err) + addresses.DisputeGameFactory, err = systemConfig.DisputeGameFactory(&bind.CallOpts{}) + require.NoError(t, err) + addresses.OptimismPortal, err = systemConfig.OptimismPortal(&bind.CallOpts{}) + require.NoError(t, err) + addresses.OptimismMintableERC20Factory, err = systemConfig.OptimismMintableERC20Factory(&bind.CallOpts{}) + require.NoError(t, err) + + // Queue up custom gas token address ready for reinitialization + addresses.GasPayingToken = cgtAddress + + // Bind a ProxyAdmin to the ProxyAdmin address + proxyAdmin, err := bindings.NewProxyAdmin(cfg.L1Deployments.ProxyAdmin, l1Client) + require.NoError(t, err) + + // Deploy a new StorageSetter contract + storageSetterAddr, tx, _, err := bindings.DeployStorageSetter(deployerOpts, l1Client) + waitForTx(t, tx, err, l1Client) + + // Set up a signer which controls the Proxy Admin. + // The deploy config's finalSystemOwner is the owner of the ProxyAdmin as well as the SystemConfig, + // so we can use that address for the proxy admin owner. + ownerSecret := cfg.Secrets.Deployer + if allocType == config.AllocTypeL2OO { + ownerSecret = cfg.Secrets.SysCfgOwner + } + proxyAdminOwnerOpts, err := bind.NewKeyedTransactorWithChainID(ownerSecret, cfg.L1ChainIDBig()) + require.NoError(t, err) + + // Execute the upgrade SystemConfigProxy -> StorageSetter via ProxyAdmin + tx, err = proxyAdmin.Upgrade(proxyAdminOwnerOpts, cfg.L1Deployments.SystemConfigProxy, storageSetterAddr) + waitForTx(t, tx, err, l1Client) + + // Bind a StorageSetter to the SystemConfigProxy address + storageSetter, err := bindings.NewStorageSetter(cfg.L1Deployments.SystemConfigProxy, l1Client) + require.NoError(t, err) + + // Use StorageSetter to clear out "initialize" slot + tx, err = storageSetter.SetBytes320(deployerOpts, [32]byte{0}, [32]byte{0}) + waitForTx(t, tx, err, l1Client) + + // Sanity check previous step worked + currentSlotValue, err := storageSetter.GetBytes32(&bind.CallOpts{}, [32]byte{0}) + require.NoError(t, err) + require.Equal(t, currentSlotValue, [32]byte{0}) + + // Execute SystemConfigProxy -> SystemConfig upgrade + tx, err = proxyAdmin.Upgrade(proxyAdminOwnerOpts, cfg.L1Deployments.SystemConfigProxy, cfg.L1Deployments.SystemConfig) + waitForTx(t, tx, err, l1Client) + + // Reinitialise with existing initializer values but with custom gas token set + tx, err = systemConfig.Initialize(deployerOpts, owner, + basefeeScalar, + blobbasefeeScalar, + batcherHash, + gasLimit, + unsafeBlockSigner, + resourceConfig, + batchInbox, + addresses) + require.NoError(t, err) + receipt, err := wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + + // Read Custom Gas Token and check it has been set properly + gpt, err := systemConfig.GasPayingToken(&bind.CallOpts{}) + require.NoError(t, err) + require.Equal(t, cgtAddress, gpt.Addr) + + optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) + require.NoError(t, err) + + depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "Should emit deposit event") + depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) + + require.NoError(t, err) + l2Client := sys.NodeClient("sequencer") + receipt, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) + require.NoError(t, err) + + l1Block, err := bindings.NewL1Block(predeploys.L1BlockAddr, l2Client) + require.NoError(t, err) + _, err = receipts.FindLog(receipt.Logs, l1Block.ParseGasPayingTokenSet) + require.NoError(t, err) +} + +// waitForTx is a thing wrapper around wait.ForReceiptOK which asserts on there being no errors. +func waitForTx(t *testing.T, tx *types.Transaction, err error, client *ethclient.Client) { + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), client, tx.Hash()) + require.NoError(t, err) +} + +type gasTokenTestOpts struct { + aliceOpts *bind.TransactOpts + cfg e2esys.SystemConfig + weth9 *bindings.WETH9 + weth9Address common.Address + allocType config.AllocType + sys *e2esys.System + enabledExpectations cgtTestExpectations + disabledExpectations cgtTestExpectations +} + +// Function to prepare and make call to depositERC20Transaction and make +// appropriate assertions dependent on whether custom gas tokens have been enabled or not. +func checkDeposit(t *testing.T, gto gasTokenTestOpts, enabled bool) { + aliceOpts := gto.aliceOpts + cfg := gto.cfg + l1Client := gto.sys.NodeClient("l1") + l2Client := gto.sys.NodeClient("sequencer") + weth9 := gto.weth9 + + // Set amount of WETH9 to bridge to the recipient on L2 + amountToBridge := big.NewInt(10) + recipient := common.HexToAddress("0xbeefdead") + + // Approve OptimismPortal + tx, err := weth9.Approve(aliceOpts, cfg.L1Deployments.OptimismPortalProxy, amountToBridge) + waitForTx(t, tx, err, l1Client) + + // Get recipient L2 balance before bridging + previousL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) + require.NoError(t, err) + + // Bridge the tokens + optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) + require.NoError(t, err) + tx, err = optimismPortal.DepositERC20Transaction(aliceOpts, + recipient, + amountToBridge, + amountToBridge, + 50_0000, // _gasLimit + false, + []byte{}, + ) + if enabled { + require.NoError(t, err) + receipt, err := wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + + // compute the deposit transaction hash + poll for it + depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "Should emit deposit event") + depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) + require.NoError(t, err) + + require.EventuallyWithT(t, func(t *assert.CollectT) { + // check for balance increase on L2 + newL2Balance, err := l2Client.BalanceAt(context.Background(), recipient, nil) + require.NoError(t, err) + l2BalanceIncrease := big.NewInt(0).Sub(newL2Balance, previousL2Balance) + require.Equal(t, amountToBridge, l2BalanceIncrease) + }, 10*time.Second, 1*time.Second) + } else { + require.Error(t, err) + } +} + +// Function to prepare and execute withdrawal flow for CGTs +// and assert token balance is increased on L1. +func checkWithdrawal(t *testing.T, gto gasTokenTestOpts) { + aliceOpts := gto.aliceOpts + cfg := gto.cfg + weth9 := gto.weth9 + allocType := gto.allocType + l1Client := gto.sys.NodeClient("l1") + l2Seq := gto.sys.NodeClient("sequencer") + l2Verif := gto.sys.NodeClient("verifier") + fromAddr := aliceOpts.From + ethPrivKey := cfg.Secrets.Alice + + // Start L2 balance for withdrawal + startBalanceBeforeWithdrawal, err := l2Seq.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + + withdrawAmount := big.NewInt(5) + tx, receipt := helpers.SendWithdrawal(t, cfg, l2Seq, cfg.Secrets.Alice, func(opts *helpers.WithdrawalTxOpts) { + opts.Value = withdrawAmount + opts.VerifyOnClients(l2Verif) + }) + + // Verify L2 balance after withdrawal + header, err := l2Verif.HeaderByNumber(context.Background(), receipt.BlockNumber) + require.NoError(t, err) + + endBalanceAfterWithdrawal, err := wait.ForBalanceChange(context.Background(), l2Seq, fromAddr, startBalanceBeforeWithdrawal) + require.NoError(t, err) + + // Take fee into account + diff := new(big.Int).Sub(startBalanceBeforeWithdrawal, endBalanceAfterWithdrawal) + fees := helpers.CalcGasFees(receipt.GasUsed, tx.GasTipCap(), tx.GasFeeCap(), header.BaseFee) + fees = fees.Add(fees, receipt.L1Fee) + diff = diff.Sub(diff, fees) + require.Equal(t, withdrawAmount, diff) + + // Take start token balance on L1 + startTokenBalanceBeforeFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) + require.NoError(t, err) + + startETHBalanceBeforeFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + + proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, gto.sys, "verifier", ethPrivKey, receipt) + + // Verify L1 ETH balance change + proveFee := new(big.Int).Mul(new(big.Int).SetUint64(proveReceipt.GasUsed), proveReceipt.EffectiveGasPrice) + finalizeFee := new(big.Int).Mul(new(big.Int).SetUint64(finalizeReceipt.GasUsed), finalizeReceipt.EffectiveGasPrice) + fees = new(big.Int).Add(proveFee, finalizeFee) + if allocType.UsesProofs() { + resolveClaimFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveClaimReceipt.GasUsed), resolveClaimReceipt.EffectiveGasPrice) + resolveFee := new(big.Int).Mul(new(big.Int).SetUint64(resolveReceipt.GasUsed), resolveReceipt.EffectiveGasPrice) + fees = new(big.Int).Add(fees, resolveClaimFee) + fees = new(big.Int).Add(fees, resolveFee) + } + + // Verify L1ETHBalance after withdrawal + // On CGT chains, the only change in ETH balance from a withdrawal + // is a decrease to pay for gas + endETHBalanceAfterFinalize, err := l1Client.BalanceAt(context.Background(), fromAddr, nil) + require.NoError(t, err) + diff = new(big.Int).Sub(endETHBalanceAfterFinalize, startETHBalanceBeforeFinalize) + require.Equal(t, new(big.Int).Sub(big.NewInt(0), fees), diff) + + // Verify token balance after withdrawal + // L1 Fees are paid in ETH, and + // withdrawal is of a Custom Gas Token, so we do not subtract l1 fees from expected balance change + // as we would if ETH was the gas paying token + endTokenBalanceAfterFinalize, err := weth9.BalanceOf(&bind.CallOpts{}, fromAddr) + require.NoError(t, err) + diff = new(big.Int).Sub(endTokenBalanceAfterFinalize, startTokenBalanceBeforeFinalize) + require.Equal(t, withdrawAmount, diff) +} + +// checkFeeWithdrawal ensures that the FeeVault can be withdrawn from +func checkFeeWithdrawal(t *testing.T, gto gasTokenTestOpts, enabled bool) { + cfg := gto.cfg + weth9 := gto.weth9 + allocType := gto.allocType + l1Client := gto.sys.NodeClient("l1") + l2Client := gto.sys.NodeClient("sequencer") + + feeVault, err := bindings.NewSequencerFeeVault(predeploys.SequencerFeeVaultAddr, l2Client) + require.NoError(t, err) + + // Alice will be sending transactions + aliceOpts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L2ChainIDBig()) + require.NoError(t, err) + + // Get the recipient of the funds + recipient, err := feeVault.RECIPIENT(&bind.CallOpts{}) + require.NoError(t, err) + + // This test depends on the withdrawal network being L1 which is represented + // by 0 in the enum. + withdrawalNetwork, err := feeVault.WITHDRAWALNETWORK(&bind.CallOpts{}) + require.NoError(t, err) + require.Equal(t, withdrawalNetwork, uint8(0)) + + // Get the balance of the recipient on L1 + var recipientBalanceBefore *big.Int + if enabled { + recipientBalanceBefore, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) + } else { + recipientBalanceBefore, err = l1Client.BalanceAt(context.Background(), recipient, nil) + } + require.NoError(t, err) + + // Get the min withdrawal amount for the FeeVault + amount, err := feeVault.MINWITHDRAWALAMOUNT(&bind.CallOpts{}) + require.NoError(t, err) + + l1opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Alice, cfg.L1ChainIDBig()) + require.NoError(t, err) + + optimismPortal, err := bindings.NewOptimismPortal(cfg.L1Deployments.OptimismPortalProxy, l1Client) + require.NoError(t, err) + + depositAmount := new(big.Int).Mul(amount, big.NewInt(14)) + l1opts.Value = depositAmount + + var receipt *types.Receipt + + // Alice deposits funds + if enabled { + // approve + transferFrom flow + // Cannot use `transfer` because of the tracking of balance in the OptimismPortal + dep, err := weth9.Deposit(l1opts) + waitForTx(t, dep, err, l1Client) + + l1opts.Value = nil + tx, err := weth9.Approve(l1opts, cfg.L1Deployments.OptimismPortalProxy, depositAmount) + waitForTx(t, tx, err, l1Client) + + require.NoError(t, err) + deposit, err := optimismPortal.DepositERC20Transaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, depositAmount, 500_000, false, []byte{}) + waitForTx(t, deposit, err, l1Client) + + receipt, err = wait.ForReceiptOK(context.Background(), l1Client, deposit.Hash()) + require.NoError(t, err) + } else { + // send ether to the portal directly, alice already has funds on L2 + tx, err := optimismPortal.DepositTransaction(l1opts, cfg.Secrets.Addresses().Alice, depositAmount, 500_000, false, []byte{}) + waitForTx(t, tx, err, l1Client) + + receipt, err = wait.ForReceiptOK(context.Background(), l1Client, tx.Hash()) + require.NoError(t, err) + } + + // Compute the deposit transaction hash + poll for it + depositEvent, err := receipts.FindLog(receipt.Logs, optimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "Should emit deposit event") + depositTx, err := derive.UnmarshalDepositLogEvent(&depositEvent.Raw) + require.NoError(t, err) + _, err = wait.ForReceiptOK(context.Background(), l2Client, types.NewTx(depositTx).Hash()) + require.NoError(t, err) + + // Get Alice's balance on L2 + aliceBalance, err := l2Client.BalanceAt(context.Background(), cfg.Secrets.Addresses().Alice, nil) + require.NoError(t, err) + require.GreaterOrEqual(t, aliceBalance.Uint64(), amount.Uint64()) + + // Send funds to the FeeVault so its balance is above the min withdrawal amount + aliceOpts.Value = amount + feeVaultTx, err := feeVault.Receive(aliceOpts) + waitForTx(t, feeVaultTx, err, l2Client) + + // Ensure that the balance of the vault is large enough to withdraw + vaultBalance, err := l2Client.BalanceAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) + require.NoError(t, err) + require.GreaterOrEqual(t, vaultBalance.Uint64(), amount.Uint64()) + + // Ensure there is code at the vault address + code, err := l2Client.CodeAt(context.Background(), predeploys.SequencerFeeVaultAddr, nil) + require.NoError(t, err) + require.NotEmpty(t, code) + + // Poke the fee vault to withdraw + l2Opts, err := bind.NewKeyedTransactorWithChainID(cfg.Secrets.Bob, cfg.L2ChainIDBig()) + require.NoError(t, err) + withdrawalTx, err := feeVault.Withdraw(l2Opts) + waitForTx(t, withdrawalTx, err, l2Client) + + // Get the receipt and the amount withdrawn + receipt, err = l2Client.TransactionReceipt(context.Background(), withdrawalTx.Hash()) + require.NoError(t, err) + + inclusionHeight := receipt.BlockNumber.Uint64() + it, err := feeVault.FilterWithdrawal(&bind.FilterOpts{ + Start: inclusionHeight, + End: &inclusionHeight, + }) + require.NoError(t, err) + require.True(t, it.Next()) + + withdrawnAmount := it.Event.Value + + // Finalize the withdrawal + proveReceipt, finalizeReceipt, resolveClaimReceipt, resolveReceipt := helpers.ProveAndFinalizeWithdrawal(t, cfg, gto.sys, "verifier", cfg.Secrets.Alice, receipt) + require.Equal(t, types.ReceiptStatusSuccessful, proveReceipt.Status) + require.Equal(t, types.ReceiptStatusSuccessful, finalizeReceipt.Status) + if allocType.UsesProofs() { + require.Equal(t, types.ReceiptStatusSuccessful, resolveClaimReceipt.Status) + require.Equal(t, types.ReceiptStatusSuccessful, resolveReceipt.Status) + } + + // Assert that the recipient's balance did increase + var recipientBalanceAfter *big.Int + if enabled { + recipientBalanceAfter, err = weth9.BalanceOf(&bind.CallOpts{}, recipient) + } else { + recipientBalanceAfter, err = l1Client.BalanceAt(context.Background(), recipient, nil) + } + require.NoError(t, err) + + require.Equal(t, recipientBalanceAfter, new(big.Int).Add(recipientBalanceBefore, withdrawnAmount)) +} + +func checkL1TokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, expectations cgtTestExpectations) { + l1Client := gto.sys.NodeClient("l1") + cfg := gto.cfg + + systemConfig, err := bindings.NewSystemConfig(cfg.L1Deployments.SystemConfigProxy, l1Client) + require.NoError(t, err) + + token, err := systemConfig.GasPayingToken(&bind.CallOpts{}) + require.NoError(t, err) + + name, err := systemConfig.GasPayingTokenName(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := systemConfig.GasPayingTokenSymbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, expectations.tokenAddress, token.Addr) + require.Equal(t, expectations.tokenDecimals, token.Decimals) + require.Equal(t, expectations.tokenName, name) + require.Equal(t, expectations.tokenSymbol, symbol) +} + +func checkL2TokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, enabledExpectations cgtTestExpectations) { + l2Client := gto.sys.NodeClient("sequencer") + + l1Block, err := bindings.NewL1Block(predeploys.L1BlockAddr, l2Client) + require.NoError(t, err) + + token, err := l1Block.GasPayingToken(&bind.CallOpts{}) + require.NoError(t, err) + + name, err := l1Block.GasPayingTokenName(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := l1Block.GasPayingTokenSymbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, enabledExpectations.tokenAddress, token.Addr) + require.Equal(t, enabledExpectations.tokenDecimals, token.Decimals) + require.Equal(t, enabledExpectations.tokenName, name) + require.Equal(t, enabledExpectations.tokenSymbol, symbol) +} + +func checkWETHTokenNameAndSymbol(t *testing.T, gto gasTokenTestOpts, expectations cgtTestExpectations) { + l2Client := gto.sys.NodeClient("sequencer") + + // Check name and symbol in WETH predeploy + weth, err := bindings.NewWETH(predeploys.WETHAddr, l2Client) + require.NoError(t, err) + + name, err := weth.Name(&bind.CallOpts{}) + require.NoError(t, err) + + symbol, err := weth.Symbol(&bind.CallOpts{}) + require.NoError(t, err) + + require.Equal(t, "Wrapped "+expectations.tokenName, name) + require.Equal(t, "W"+expectations.tokenSymbol, symbol) +} diff --git a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol index 0024ad28dc6e1..abafca5457273 100644 --- a/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol +++ b/packages/contracts-bedrock/interfaces/L1/IL1StandardBridge.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import { IStandardBridge } from "interfaces/universal/IStandardBridge.sol"; import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; interface IL1StandardBridge is IStandardBridge { event ERC20DepositInitiated( @@ -63,7 +64,8 @@ interface IL1StandardBridge is IStandardBridge { payable; function initialize( ICrossDomainMessenger _messenger, - ISuperchainConfig _superchainConfig + ISuperchainConfig _superchainConfig, + ISystemConfig _systemConfig ) external; function l2TokenBridge() external view returns (address); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol index bbf12c3fce4dc..d52294ea152f2 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol @@ -9,6 +9,7 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; interface IOptimismPortal2 { + error CustomGasTokenNotSupported(); error AlreadyFinalized(); error BadTarget(); error Blacklisted(); @@ -23,10 +24,13 @@ interface IOptimismPortal2 { error InvalidMerkleProof(); error InvalidProof(); error LargeCalldata(); + error NoValue(); error NonReentrant(); + error OnlyCustomGasToken(); error OutOfGas(); error ProposalNotValidated(); error SmallGasLimit(); + error TransferFailed(); error Unauthorized(); error UnexpectedList(); error UnexpectedString(); @@ -43,8 +47,18 @@ interface IOptimismPortal2 { receive() external payable; + function balance() external view returns (uint256); function blacklistDisputeGame(IDisputeGame _disputeGame) external; function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; + function depositERC20Transaction( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) + external; function depositTransaction( address _to, uint256 _value, @@ -96,6 +110,7 @@ interface IOptimismPortal2 { returns (IDisputeGame disputeGameProxy, uint64 timestamp); // nosemgrep function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); diff --git a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol index 6dec13a8335c9..51b6ab8f22426 100644 --- a/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/IOptimismPortalInterop.sol @@ -10,6 +10,7 @@ import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; interface IOptimismPortalInterop { + error CustomGasTokenNotSupported(); error AlreadyFinalized(); error BadTarget(); error Blacklisted(); @@ -24,10 +25,13 @@ interface IOptimismPortalInterop { error InvalidMerkleProof(); error InvalidProof(); error LargeCalldata(); + error NoValue(); error NonReentrant(); + error OnlyCustomGasToken(); error OutOfGas(); error ProposalNotValidated(); error SmallGasLimit(); + error TransferFailed(); error Unauthorized(); error UnexpectedList(); error UnexpectedString(); @@ -44,8 +48,18 @@ interface IOptimismPortalInterop { receive() external payable; + function balance() external view returns (uint256); function blacklistDisputeGame(IDisputeGame _disputeGame) external; function checkWithdrawal(bytes32 _withdrawalHash, address _proofSubmitter) external view; + function depositERC20Transaction( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) + external; function depositTransaction( address _to, uint256 _value, @@ -98,6 +112,7 @@ interface IOptimismPortalInterop { function respectedGameType() external view returns (GameType); function respectedGameTypeUpdatedAt() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setRespectedGameType(GameType _gameType) external; function superchainConfig() external view returns (ISuperchainConfig); function systemConfig() external view returns (ISystemConfig); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol index e687a17e048bb..a8de89b5c978f 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfig.sol @@ -3,7 +3,10 @@ pragma solidity ^0.8.0; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +/// @notice This interface corresponds to the Custom Gas Token version of the SystemConfig contract. interface ISystemConfig { + error CustomGasTokenNotSupported(); + enum UpdateType { BATCHER, FEE_SCALARS, @@ -19,6 +22,7 @@ interface ISystemConfig { address disputeGameFactory; address optimismPortal; address optimismMintableERC20Factory; + address gasPayingToken; } event ConfigUpdate(uint256 indexed version, UpdateType indexed updateType, bytes data); @@ -44,6 +48,9 @@ interface ISystemConfig { function eip1559Denominator() external view returns (uint32); function eip1559Elasticity() external view returns (uint32); function getAddresses() external view returns (Addresses memory); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function initialize( address _owner, uint32 _basefeeScalar, @@ -56,6 +63,7 @@ interface ISystemConfig { Addresses memory _addresses ) external; + function isCustomGasToken() external view returns (bool); function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); diff --git a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol index c9b63db2b1df0..59950cc353608 100644 --- a/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol +++ b/packages/contracts-bedrock/interfaces/L1/ISystemConfigInterop.sol @@ -5,6 +5,8 @@ import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; interface ISystemConfigInterop { + error CustomGasTokenNotSupported(); + event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); event Initialized(uint8 version); event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); @@ -27,6 +29,10 @@ interface ISystemConfigInterop { function gasLimit() external view returns (uint64); function eip1559Denominator() external view returns (uint32); function eip1559Elasticity() external view returns (uint32); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); + function isCustomGasToken() external view returns (bool); function l1CrossDomainMessenger() external view returns (address addr_); function l1ERC721Bridge() external view returns (address addr_); function l1StandardBridge() external view returns (address addr_); diff --git a/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol b/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol index cd87ad5c1d448..77c1c0b3caf25 100644 --- a/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol +++ b/packages/contracts-bedrock/interfaces/L2/IETHLiquidity.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; interface IETHLiquidity { + error NotCustomGasToken(); error Unauthorized(); event LiquidityBurned(address indexed caller, uint256 value); diff --git a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol index dba4a09cabaca..1e7cb415002f4 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1Block.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1Block.sol @@ -2,21 +2,26 @@ pragma solidity ^0.8.0; interface IL1Block { + error NotDepositor(); + + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + function DEPOSITOR_ACCOUNT() external pure returns (address addr_); function baseFeeScalar() external view returns (uint32); function basefee() external view returns (uint256); function batcherHash() external view returns (bytes32); function blobBaseFee() external view returns (uint256); function blobBaseFeeScalar() external view returns (uint32); - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); + function isCustomGasToken() external view returns (bool); function l1FeeOverhead() external view returns (uint256); function l1FeeScalar() external view returns (uint256); function number() external view returns (uint64); function sequenceNumber() external view returns (uint64); + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setL1BlockValues( uint64 _number, uint64 _timestamp, diff --git a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol b/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol index 8781d9eef95f6..b9fa78733e788 100644 --- a/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol +++ b/packages/contracts-bedrock/interfaces/L2/IL1BlockInterop.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; enum ConfigType { + SET_GAS_PAYING_TOKEN, ADD_DEPENDENCY, REMOVE_DEPENDENCY } @@ -16,6 +17,7 @@ interface IL1BlockInterop { event DependencyAdded(uint256 indexed chainId); event DependencyRemoved(uint256 indexed chainId); + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); function DEPOSITOR_ACCOUNT() external pure returns (address addr_); function baseFeeScalar() external view returns (uint32); @@ -25,11 +27,11 @@ interface IL1BlockInterop { function blobBaseFeeScalar() external view returns (uint32); function dependencySetSize() external view returns (uint8); function depositsComplete() external; - function gasPayingToken() external pure returns (address addr_, uint8 decimals_); - function gasPayingTokenName() external pure returns (string memory name_); - function gasPayingTokenSymbol() external pure returns (string memory symbol_); + function gasPayingToken() external view returns (address addr_, uint8 decimals_); + function gasPayingTokenName() external view returns (string memory name_); + function gasPayingTokenSymbol() external view returns (string memory symbol_); function hash() external view returns (bytes32); - function isCustomGasToken() external pure returns (bool is_); + function isCustomGasToken() external view returns (bool); function isDeposit() external view returns (bool isDeposit_); function isInDependencySet(uint256 _chainId) external view returns (bool); function l1FeeOverhead() external view returns (uint256); @@ -37,6 +39,7 @@ interface IL1BlockInterop { function number() external view returns (uint64); function sequenceNumber() external view returns (uint64); function setConfig(ConfigType _type, bytes memory _value) external; + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external; function setL1BlockValues( uint64 _number, uint64 _timestamp, diff --git a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol index e646807b8d158..a6b6ef3ce7bf7 100644 --- a/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol +++ b/packages/contracts-bedrock/interfaces/L2/ISuperchainWETH.sol @@ -7,6 +7,7 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; interface ISuperchainWETH is IWETH98, IERC7802, ISemver { error Unauthorized(); + error NotCustomGasToken(); error InvalidCrossDomainSender(); error ZeroAddress(); @@ -15,7 +16,7 @@ interface ISuperchainWETH is IWETH98, IERC7802, ISemver { event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); function balanceOf(address src) external view returns (uint256); - function withdraw(uint256 wad) external; + function withdraw(uint256 _amount) external; function supportsInterface(bytes4 _interfaceId) external view returns (bool); function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_); function relayETH(address _from, address _to, uint256 _amount) external; diff --git a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol index 7e6e22533109b..a720dd15e4e06 100644 --- a/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/Deploy.s.sol @@ -210,6 +210,12 @@ contract Deploy is Deployer { ); vm.stopPrank(); + if (cfg.useCustomGasToken()) { + // Reset the systemconfig then reinitialize it with the custom gas token + resetInitializedProxy("SystemConfig"); + initializeSystemConfig(); + } + if (cfg.useAltDA()) { bytes32 typeHash = keccak256(bytes(cfg.daCommitmentType())); bytes32 keccakHash = keccak256(bytes("KeccakCommitment")); @@ -305,6 +311,8 @@ contract Deploy is Deployer { artifacts.save("MipsSingleton", address(dio.mipsSingleton())); artifacts.save("OPContractsManager", address(dio.opcm())); artifacts.save("DelayedWETHImpl", address(dio.delayedWETHImpl())); + // added for CGT + artifacts.save("SystemConfigImpl", address(dio.systemConfigImpl())); // Get a contract set from the implementation addresses which were just deployed. Types.ContractSet memory impls = Types.ContractSet({ @@ -499,6 +507,11 @@ contract Deploy is Deployer { bytes32 batcherHash = bytes32(uint256(uint160(cfg.batchSenderAddress()))); + address customGasTokenAddress = Constants.ETHER; + if (cfg.useCustomGasToken()) { + customGasTokenAddress = cfg.customGasTokenAddress(); + } + IProxyAdmin proxyAdmin = IProxyAdmin(payable(artifacts.mustGetAddress("ProxyAdmin"))); proxyAdmin.upgradeAndCall({ _proxy: payable(systemConfigProxy), @@ -520,7 +533,8 @@ contract Deploy is Deployer { l1StandardBridge: artifacts.mustGetAddress("L1StandardBridgeProxy"), disputeGameFactory: artifacts.mustGetAddress("DisputeGameFactoryProxy"), optimismPortal: artifacts.mustGetAddress("OptimismPortalProxy"), - optimismMintableERC20Factory: artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy") + optimismMintableERC20Factory: artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy"), + gasPayingToken: customGasTokenAddress }) ) ) diff --git a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol index 0b1c8c8e0a39d..13ccedab62ef7 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployConfig.s.sol @@ -85,6 +85,9 @@ contract DeployConfig is Script { uint256 public daBondSize; uint256 public daResolverRefundPercentage; + bool public useCustomGasToken; + address public customGasTokenAddress; + bool public useInterop; bool public useSoulGasToken; bool public isSoulBackedByNative; @@ -170,6 +173,9 @@ contract DeployConfig is Script { daBondSize = _readOr(_json, "$.daBondSize", 1000000000); daResolverRefundPercentage = _readOr(_json, "$.daResolverRefundPercentage", 0); + useCustomGasToken = _readOr(_json, "$.useCustomGasToken", false); + customGasTokenAddress = _readOr(_json, "$.customGasTokenAddress", address(0)); + useInterop = _readOr(_json, "$.useInterop", false); useSoulGasToken = _readOr(_json, "$.useSoulGasToken", false); isSoulBackedByNative = _readOr(_json, "$.isSoulBackedByNative", false); @@ -240,6 +246,12 @@ contract DeployConfig is Script { fundDevAccounts = _fundDevAccounts; } + /// @notice Allow the `useCustomGasToken` config to be overridden in testing environments + function setUseCustomGasToken(address _token) public { + useCustomGasToken = true; + customGasTokenAddress = _token; + } + /// @notice Allow the `useUpgradedFork` config to be overridden in testing environments /// @dev When true, the forked system WILL be upgraded in setUp(). /// When false, the forked system WILL NOT be upgraded in setUp(). diff --git a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol index 2112d614251ae..e8899f1d8ddf7 100644 --- a/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol +++ b/packages/contracts-bedrock/scripts/deploy/DeployOPChain.s.sol @@ -540,6 +540,8 @@ contract DeployOPChain is Script { systemConfig.optimismMintableERC20Factory() == address(_doo.optimismMintableERC20FactoryProxy()), "SYSCON-210" ); + (address gasPayingToken,) = systemConfig.gasPayingToken(); + require(gasPayingToken == Constants.ETHER, "SYSCON-220"); } function assertValidL1CrossDomainMessenger(DeployOPChainInput _doi, DeployOPChainOutput _doo) internal { diff --git a/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json b/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json index 72385798e5f89..5fd386c52e5ae 100644 --- a/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json +++ b/packages/contracts-bedrock/snapshots/abi/ETHLiquidity.json @@ -70,6 +70,11 @@ "name": "LiquidityMinted", "type": "event" }, + { + "inputs": [], + "name": "NotCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/L1Block.json b/packages/contracts-bedrock/snapshots/abi/L1Block.json index cab59f5c3f784..a32eff778d6f2 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1Block.json +++ b/packages/contracts-bedrock/snapshots/abi/L1Block.json @@ -111,7 +111,7 @@ "type": "uint8" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -124,7 +124,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -137,7 +137,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -172,11 +172,11 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "", "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -231,6 +231,34 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -311,5 +339,41 @@ ], "stateMutability": "pure", "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "name", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "symbol", + "type": "bytes32" + } + ], + "name": "GasPayingTokenSet", + "type": "event" + }, + { + "inputs": [], + "name": "NotDepositor", + "type": "error" } ] \ No newline at end of file diff --git a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json b/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json index a34befff9274f..1d54b5af228ca 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/L1BlockInterop.json @@ -131,7 +131,7 @@ "type": "uint8" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -144,7 +144,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -157,7 +157,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -192,11 +192,11 @@ "outputs": [ { "internalType": "bool", - "name": "is_", + "name": "", "type": "bool" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -301,6 +301,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -415,6 +443,37 @@ "name": "DependencyRemoved", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint8", + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "name", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "symbol", + "type": "bytes32" + } + ], + "name": "GasPayingTokenSet", + "type": "event" + }, { "inputs": [], "name": "AlreadyDependency", diff --git a/packages/contracts-bedrock/snapshots/abi/L1StandardBridge.json b/packages/contracts-bedrock/snapshots/abi/L1StandardBridge.json index 3ca7b0bf63961..80bd4d9b9332a 100644 --- a/packages/contracts-bedrock/snapshots/abi/L1StandardBridge.json +++ b/packages/contracts-bedrock/snapshots/abi/L1StandardBridge.json @@ -34,6 +34,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "SYSTEM_CONFIG_SLOT", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -425,6 +438,11 @@ "internalType": "contract ISuperchainConfig", "name": "_superchainConfig", "type": "address" + }, + { + "internalType": "contract ISystemConfig", + "name": "_systemConfig", + "type": "address" } ], "name": "initialize", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json index 71b8677f7dd85..c62d9a540ee16 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortal2.json @@ -19,6 +19,19 @@ "stateMutability": "payable", "type": "receive" }, + { + "inputs": [], + "name": "balance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -50,6 +63,44 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_mint", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "_gasLimit", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "_isCreation", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "depositERC20Transaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -551,6 +602,34 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -767,6 +846,11 @@ "name": "ContentLengthMismatch", "type": "error" }, + { + "inputs": [], + "name": "CustomGasTokenNotSupported", + "type": "error" + }, { "inputs": [], "name": "EmptyItem", @@ -817,11 +901,21 @@ "name": "LegacyGame", "type": "error" }, + { + "inputs": [], + "name": "NoValue", + "type": "error" + }, { "inputs": [], "name": "NonReentrant", "type": "error" }, + { + "inputs": [], + "name": "OnlyCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", @@ -837,6 +931,11 @@ "name": "SmallGasLimit", "type": "error" }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json index 2dd27e68eaff9..23bddf214be97 100644 --- a/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/OptimismPortalInterop.json @@ -19,6 +19,19 @@ "stateMutability": "payable", "type": "receive" }, + { + "inputs": [], + "name": "balance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -50,6 +63,44 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_mint", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_value", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "_gasLimit", + "type": "uint64" + }, + { + "internalType": "bool", + "name": "_isCreation", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "depositERC20Transaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -569,6 +620,34 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_decimals", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "_name", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "_symbol", + "type": "bytes32" + } + ], + "name": "setGasPayingToken", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -785,6 +864,11 @@ "name": "ContentLengthMismatch", "type": "error" }, + { + "inputs": [], + "name": "CustomGasTokenNotSupported", + "type": "error" + }, { "inputs": [], "name": "EmptyItem", @@ -835,11 +919,21 @@ "name": "LegacyGame", "type": "error" }, + { + "inputs": [], + "name": "NoValue", + "type": "error" + }, { "inputs": [], "name": "NonReentrant", "type": "error" }, + { + "inputs": [], + "name": "OnlyCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "OutOfGas", @@ -855,6 +949,11 @@ "name": "SmallGasLimit", "type": "error" }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json index 24b27063f3423..e8df09a806406 100644 --- a/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json +++ b/packages/contracts-bedrock/snapshots/abi/SuperchainWETH.json @@ -305,7 +305,7 @@ "inputs": [ { "internalType": "uint256", - "name": "wad", + "name": "_amount", "type": "uint256" } ], @@ -519,6 +519,11 @@ "name": "InvalidCrossDomainSender", "type": "error" }, + { + "inputs": [], + "name": "NotCustomGasToken", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json index 7105f16bf4485..8c35c47610443 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfig.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfig.json @@ -238,6 +238,50 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "gasPayingToken", + "outputs": [ + { + "internalType": "address", + "name": "addr_", + "type": "address" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenName", + "outputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenSymbol", + "outputs": [ + { + "internalType": "string", + "name": "symbol_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getAddresses", @@ -273,6 +317,11 @@ "internalType": "address", "name": "optimismMintableERC20Factory", "type": "address" + }, + { + "internalType": "address", + "name": "gasPayingToken", + "type": "address" } ], "internalType": "struct SystemConfig.Addresses", @@ -388,6 +437,11 @@ "internalType": "address", "name": "optimismMintableERC20Factory", "type": "address" + }, + { + "internalType": "address", + "name": "gasPayingToken", + "type": "address" } ], "internalType": "struct SystemConfig.Addresses", @@ -400,6 +454,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessenger", diff --git a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json index f87d9e9e54678..7aade6ea0cbca 100644 --- a/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json +++ b/packages/contracts-bedrock/snapshots/abi/SystemConfigInterop.json @@ -259,6 +259,50 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "gasPayingToken", + "outputs": [ + { + "internalType": "address", + "name": "addr_", + "type": "address" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenName", + "outputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "gasPayingTokenSymbol", + "outputs": [ + { + "internalType": "string", + "name": "symbol_", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getAddresses", @@ -294,6 +338,11 @@ "internalType": "address", "name": "optimismMintableERC20Factory", "type": "address" + }, + { + "internalType": "address", + "name": "gasPayingToken", + "type": "address" } ], "internalType": "struct SystemConfig.Addresses", @@ -409,11 +458,21 @@ "internalType": "address", "name": "optimismMintableERC20Factory", "type": "address" + }, + { + "internalType": "address", + "name": "gasPayingToken", + "type": "address" } ], "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" + }, + { + "internalType": "address", + "name": "_dependencyManager", + "type": "address" } ], "name": "initialize", @@ -526,16 +585,16 @@ "internalType": "address", "name": "optimismMintableERC20Factory", "type": "address" + }, + { + "internalType": "address", + "name": "gasPayingToken", + "type": "address" } ], "internalType": "struct SystemConfig.Addresses", "name": "_addresses", "type": "tuple" - }, - { - "internalType": "address", - "name": "_dependencyManager", - "type": "address" } ], "name": "initialize", @@ -543,6 +602,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "isCustomGasToken", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1CrossDomainMessenger", diff --git a/packages/contracts-bedrock/snapshots/abi/WETH.json b/packages/contracts-bedrock/snapshots/abi/WETH.json index 0f97edfd828d3..03cf2880ccce0 100644 --- a/packages/contracts-bedrock/snapshots/abi/WETH.json +++ b/packages/contracts-bedrock/snapshots/abi/WETH.json @@ -104,7 +104,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { @@ -117,7 +117,7 @@ "type": "string" } ], - "stateMutability": "pure", + "stateMutability": "view", "type": "function" }, { diff --git a/packages/contracts-bedrock/snapshots/semver-lock.json b/packages/contracts-bedrock/snapshots/semver-lock.json index 0dacf1d7956b6..1702a59988e8c 100644 --- a/packages/contracts-bedrock/snapshots/semver-lock.json +++ b/packages/contracts-bedrock/snapshots/semver-lock.json @@ -4,31 +4,31 @@ "sourceCodeHash": "0xe772f7db8033e4a738850cb28ac4849d3a454c93732135a8a10d4f7cb498088e" }, "src/L1/L1CrossDomainMessenger.sol": { - "initCodeHash": "0x03a3c0eb2418aba9f93bb89723ba2ee7cb9e1988ca00f380503c960149c85b7a", - "sourceCodeHash": "0x5907cdb82ec5f6bef2184558a511d049ab3ee65388cf44d0c20d0f234ef8ca44" + "initCodeHash": "0xb1365849d7db8e69807d3874174e7756448e2d260888dd75f85dd881ef8b8e8b", + "sourceCodeHash": "0x8240408ca30580b1f38cb544435e3e1c0d138686c5e026bded6f5cfe5b80cd92" }, "src/L1/L1ERC721Bridge.sol": { "initCodeHash": "0x86ff2f104ae7aa24e34abac9b5467b4c803183d507ba8dae6e156ae2d7c4aca7", "sourceCodeHash": "0x79730cdb479b034943319c4c7a465befb0c93d5d286d2e2f716d12dfd75e7d79" }, "src/L1/L1StandardBridge.sol": { - "initCodeHash": "0x6aca5250f7c6248e455ccc7a689a818815610449a5fb8762bb1902646694f6ac", - "sourceCodeHash": "0xf9ba98657dc235355146e381b654fe3ed766feb7cd87636ec0c9d4c6dd3e1973" + "initCodeHash": "0xc0b5ea20835225286898827fd1241372402dae5d0c9a4423c57a44799c2a7a97", + "sourceCodeHash": "0x37176f2a34dd75e9f3032dce10d69524af86a2902eec9c5029b7df3a806eed9b" }, "src/L1/OPContractsManager.sol": { - "initCodeHash": "0x792cdc28c9fb20ea9afa840cc62f928195bb1e9e6aef8738e7a289a43b91ef80", - "sourceCodeHash": "0xe50c3d6ae2664000f942083d584fbbd28f1c622edfbdaaf2e7f31b344ca4207d" + "initCodeHash": "0x4f949618b7e238584b87d917be151aef89f46d6148a7393a63c3bb3be5a2970f", + "sourceCodeHash": "0xbb20fae2b7923b1d365eabafe7263a9671057aff3b2c9f76e1f6ee52936d8ba4" }, "src/L1/OPContractsManagerInterop.sol": { - "initCodeHash": "0xf573925d3911492c6476076076a8903197c6ebc91bda490856e94f2fc89f819e", + "initCodeHash": "0x79c5cf9807a115c55f1d98b85764000f14e3e373097951cc4cbfe90b298b730d", "sourceCodeHash": "0x0ba9c63e3f00dab87be058fd0091f19c51faeafd24609bc13667214796bcc106" }, "src/L1/OptimismPortal2.sol": { - "initCodeHash": "0x2cc5776c92d6cb154aa3d9897c476deaf49d98dc81493fabaea72987b9588853", - "sourceCodeHash": "0x09d877d68eb2439759e0a912a5d86d85b4742dfd205bdc1ac9dfbb32f8ccb7b0" + "initCodeHash": "0x5fbb33821327d0ac66779849311c5103fb0d2b147a5da8716e2e31fcfffec250", + "sourceCodeHash": "0x44f0b06a4420935a1259e8c812277b4d3fddd53c8dce07c00b5f940094aee3ce" }, "src/L1/OptimismPortalInterop.sol": { - "initCodeHash": "0x1d97b348b70a43a4dd96f8285252593931f4e0b2ff4e6d549169e82c8ddea4f9", + "initCodeHash": "0xdb9af808ac8e181d843d709c220e6bc99fae10f7c58b84312003a6c7f8bcbab2", "sourceCodeHash": "0x1610886629862a69b043f45fa8a187341f5a9ab05fd2c2d84f0add56e2be4439" }, "src/L1/ProtocolVersions.sol": { @@ -40,12 +40,12 @@ "sourceCodeHash": "0xfd56e63e76b1f203cceeb9bbb14396ae803cbbbf7e80ca0ee11fb586321812af" }, "src/L1/SystemConfig.sol": { - "initCodeHash": "0x98c1049952199f55ae63e34ec61a839d43bde52b0892c482ae4246d0c088e826", - "sourceCodeHash": "0x9016b1979c2f1def83a849389543708d857cf0430756815737dadda8e63047c5" + "initCodeHash": "0xeca97b4ef63ba3e843d67ee6568bac850e02da27fad9b6aa02714b501cbfbf63", + "sourceCodeHash": "0x4edd483046aa613d855656a75b017891e0dde78aac1df1760c5e04a1cd4837d4" }, "src/L1/SystemConfigInterop.sol": { - "initCodeHash": "0x36fd6c64d81c83fd97e039ab77fb6dfd3a76f12faba923bf04491bb124b590e8", - "sourceCodeHash": "0x609a10f2f85a2b1cc60a5accd795f65c84edc09b0e98124011bd9e7caeb905d9" + "initCodeHash": "0x4f7f4d8268e8b5dcc0d6544fbcd3cc0ec69c498a9ab53f302a837c80a3678818", + "sourceCodeHash": "0x278b79eb5e45b2aa5f33c3e5b8d9d8208bd1c4280ca78bae52f279deca8c4646" }, "src/L2/BaseFeeVault.sol": { "initCodeHash": "0xc403d4c555d8e69a2699e01d192ae7327136701fa02da10a6d75a584b3c364c9", @@ -56,40 +56,40 @@ "sourceCodeHash": "0x661d7659f09b7f909e8bd5e6c41e8c98f2091036ed2123b7e18a1a74120bd849" }, "src/L2/ETHLiquidity.sol": { - "initCodeHash": "0x776ece4a1bb24d97287806769470327641da240b083898a90943e2844957cc46", - "sourceCodeHash": "0xe5c08ce62327113e4bbaf29f47e5f1ddfad6fbd63c07132eedfba5af5325f331" + "initCodeHash": "0xdc7075deb7e7c407e6ec3be33ce352ee4a9ca0e82f4bffeb360c93b395a2452f", + "sourceCodeHash": "0x92bf6a7a13d1331cbfed35e22130b54b00576bd31de5feb309dfadef2a0b7dd9" }, "src/L2/GasPriceOracle.sol": { "initCodeHash": "0x83d50e3b34cd1b4de32f1cced28796b07aefc526cc17ceb1903ad55f4abc90b7", "sourceCodeHash": "0x305c72d7be9149fce7095bd4641a1a19acada3126fbc43599f674cadbf6e7d6c" }, "src/L2/L1Block.sol": { - "initCodeHash": "0x935992f6d9675ea6deb8477632e0876996c013743f9ccf64ba0019d4bb0c41a7", - "sourceCodeHash": "0x603894eec774b098b74e0caf653763bd1f6cf840f32ae27210ca69eafb78f22d" + "initCodeHash": "0x33f35f0b23e1c76afa86a10eeb50e4a312ac2dda175202480de68c9d7c60abbe", + "sourceCodeHash": "0xc9012dfd79ed68e448942aa75522f4f4dbca955c73f877584becbf8155bd4a75" }, "src/L2/L1BlockInterop.sol": { - "initCodeHash": "0x3528adb272a1c209044493c722668ec250a9f5860ec78ed50e063322724fbd94", - "sourceCodeHash": "0x01017177d32f567df0273acb1561043d97cf0a36308a95917e0f402682ae5209" + "initCodeHash": "0xb96ef2536087b67b919d592f619999244652390825f79626f690bc1381093992", + "sourceCodeHash": "0x9493f90136917fc95d2ac942f061c1b9cffeff6d327afb46fe4e69784e7f2100" }, "src/L2/L1FeeVault.sol": { "initCodeHash": "0x6745b7be3895a5e8d373df0066d931bae29c47672ac46c2f5829bd0052cc6d9e", "sourceCodeHash": "0xd0471c328c1d17c5863261322bf8d5aff2e7e9e3a1135631a993aa75667621df" }, "src/L2/L2CrossDomainMessenger.sol": { - "initCodeHash": "0x6117d2ca80029c802b1e5cc36341f03ec48efd07df0251121d3faf5e93aa5901", - "sourceCodeHash": "0x48001529220d274c5cd2e84787239b6d2244780d23894e0a8e96550a40be18fe" + "initCodeHash": "0xa6adb9d117af95f1a8be066b67a56ed8a029dd692ca363d636ad251ec9179945", + "sourceCodeHash": "0x74882c3cca14807219789b5b2b264ebd17479b409042c09db2da52736f9de36e" }, "src/L2/L2ERC721Bridge.sol": { "initCodeHash": "0xea899e672803634b98d619174bf85dc8b3f7e6407bb7306eb72ed4c8eefce0c0", "sourceCodeHash": "0xea896e18eceb9ba6e8125e9f3371549787e082db4b26d642b279b5697651d473" }, "src/L2/L2StandardBridge.sol": { - "initCodeHash": "0xbc702854c3b6da0e5c476fabc29b1a2464bee3a1ebda7175232a5c41d7ace5e6", - "sourceCodeHash": "0x0a2ea11f3114fd1c62fae90feec5032930afc898ac75af52a67a5266eeeedf9b" + "initCodeHash": "0xde3b3a1aa0056d25ab3f4ad6e29b9d56c4740b29cd9bef36588d849ffc0178f6", + "sourceCodeHash": "0x6c32dba4550b9c82d80666796e89c77799c21001691a116663970508121be03b" }, "src/L2/L2StandardBridgeInterop.sol": { - "initCodeHash": "0x38977293cf77c047b1c3f0df5848ee9d52f742ddc7916b67c4f714c103a5a2a5", - "sourceCodeHash": "0xd0f5eaf7c8f1d14f2f997f634e279d0c4fd6843cb20b0e7eb9b9c757da591d38" + "initCodeHash": "0xb8df14bf93beb53be269c641c80b50d6ad872894da49d5f60d0b976e0ba8839e", + "sourceCodeHash": "0x3b35ee19099fc4ffb111a4cd774434c414e56d33017cce47fdba3f712e2429b1" }, "src/L2/L2ToL1MessagePasser.sol": { "initCodeHash": "0xf9d82084dcef31a3737a76d8ee4e5842ea190d0f77ed4678adb3bbb95217050f", @@ -132,12 +132,12 @@ "sourceCodeHash": "0xcd2b49cb7cf6d18616ee8bec9183fe5b5b460941875bc0b4158c4d5390ec3b0c" }, "src/L2/SuperchainWETH.sol": { - "initCodeHash": "0x545686820e440d72529c815b7406844272d5ec33b741b2be6ebbe3a3db1ca8ad", - "sourceCodeHash": "0x6145e61cc0a0c95db882a76ecffea15c358c2b574d5157e53b85a69908701613" + "initCodeHash": "0x6ded8aeea6edf7e0ead7b0d2a12ef236f1fb7d21980a1dd564cbe86affca7927", + "sourceCodeHash": "0x11d711704a5afcae6076d017ee001b25bc705728973b1ad2e6a32274a8475f50" }, "src/L2/WETH.sol": { - "initCodeHash": "0x38b396fc35d72e8013bad2fe8d7dea5285499406d4c4b62e27c54252e1e0f00a", - "sourceCodeHash": "0xf4f83ca89d2519045a2916c670bda66f39b431a13921e639a5342bfc6157b178" + "initCodeHash": "0x480d4f8dbec1b0d3211bccbbdfb69796f3e90c784f724b1bbfd4703b0aafdeba", + "sourceCodeHash": "0xe9964aa66db1dfc86772958b4c9276697e67f7055529a43e6a49a055009bc995" }, "src/cannon/MIPS.sol": { "initCodeHash": "0x9d8a3c089fb84919159403a961fe0514d8be00f07b3a8be1a13a9289cc0ad11a", diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json index 654695522e049..0fdd65b3e88fb 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortal2.json @@ -120,7 +120,7 @@ }, { "bytes": "32", - "label": "spacer_61_0_32", + "label": "_balance", "offset": 0, "slot": "61", "type": "uint256" diff --git a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json index 654695522e049..0fdd65b3e88fb 100644 --- a/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json +++ b/packages/contracts-bedrock/snapshots/storageLayout/OptimismPortalInterop.json @@ -120,7 +120,7 @@ }, { "bytes": "32", - "label": "spacer_61_0_32", + "label": "_balance", "offset": 0, "slot": "61", "type": "uint256" diff --git a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol index 5e475c715c8be..81d6007c62591 100644 --- a/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L1/L1CrossDomainMessenger.sol @@ -31,6 +31,7 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ISemver { address private spacer_253_0_20; /// @notice Semantic version. + /// @custom:semver 2.5.0 string public constant version = "2.5.0"; @@ -48,6 +49,11 @@ contract L1CrossDomainMessenger is CrossDomainMessenger, ISemver { __CrossDomainMessenger_init({ _otherMessenger: CrossDomainMessenger(Predeploys.L2_CROSS_DOMAIN_MESSENGER) }); } + /// @inheritdoc CrossDomainMessenger + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = portal.systemConfig().gasPayingToken(); + } + /// @notice Getter function for the OptimismPortal contract on this chain. /// Public getter is legacy and will be removed in the future. Use `portal()` instead. /// @return Contract of the OptimismPortal on this chain. diff --git a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol index bb5737716a230..54913a471ea26 100644 --- a/packages/contracts-bedrock/src/L1/L1StandardBridge.sol +++ b/packages/contracts-bedrock/src/L1/L1StandardBridge.sol @@ -6,11 +6,13 @@ import { StandardBridge } from "src/universal/StandardBridge.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; +import { Storage } from "src/libraries/Storage.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; /// @custom:proxied true /// @title L1StandardBridge @@ -74,12 +76,17 @@ contract L1StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. + /// @custom:semver 2.2.1 string public constant version = "2.2.1"; /// @notice Address of the SuperchainConfig contract. ISuperchainConfig public superchainConfig; + /// @notice Storage slot that the systemConfig address is stored at. + /// TODO_QKC: add upgrade logic with OPCM's latest upgrade mechanism + bytes32 public constant SYSTEM_CONFIG_SLOT = bytes32(uint256(keccak256("opstack.systemconfig")) - 1); + /// @custom:legacy /// @custom:spacer systemConfig /// @notice Spacer taking up the legacy `systemConfig` slot. @@ -93,8 +100,16 @@ contract L1StandardBridge is StandardBridge, ISemver { /// @notice Initializer. /// @param _messenger Contract for the CrossDomainMessenger on this network. /// @param _superchainConfig Contract for the SuperchainConfig on this network. - function initialize(ICrossDomainMessenger _messenger, ISuperchainConfig _superchainConfig) external initializer { + function initialize( + ICrossDomainMessenger _messenger, + ISuperchainConfig _superchainConfig, + ISystemConfig _systemConfig + ) + external + initializer + { superchainConfig = _superchainConfig; + Storage.setAddress(SYSTEM_CONFIG_SLOT, address(_systemConfig)); __StandardBridge_init({ _messenger: _messenger, _otherBridge: StandardBridge(payable(Predeploys.L2_STANDARD_BRIDGE)) @@ -111,6 +126,11 @@ contract L1StandardBridge is StandardBridge, ISemver { _initiateETHDeposit(msg.sender, msg.sender, RECEIVE_DEFAULT_GAS_LIMIT, bytes("")); } + /// @inheritdoc StandardBridge + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = ISystemConfig(Storage.getAddress(SYSTEM_CONFIG_SLOT)).gasPayingToken(); + } + /// @custom:legacy /// @notice Deposits some amount of ETH into the sender's account on L2. /// @param _minGasLimit Minimum gas limit for the deposit message on L2. diff --git a/packages/contracts-bedrock/src/L1/OPContractsManager.sol b/packages/contracts-bedrock/src/L1/OPContractsManager.sol index f9e604355519d..959e6d4e32960 100644 --- a/packages/contracts-bedrock/src/L1/OPContractsManager.sol +++ b/packages/contracts-bedrock/src/L1/OPContractsManager.sol @@ -468,6 +468,7 @@ contract OPContractsManager is ISemver { } for (uint256 i = 0; i < _opChainConfigs.length; i++) { + (address gasPayingToken,) = _opChainConfigs[i].systemConfigProxy.gasPayingToken(); // After Upgrade 13, we will be able to use systemConfigProxy.getAddresses() here. ISystemConfig.Addresses memory opChainAddrs = ISystemConfig.Addresses({ l1CrossDomainMessenger: _opChainConfigs[i].systemConfigProxy.l1CrossDomainMessenger(), @@ -475,7 +476,8 @@ contract OPContractsManager is ISemver { l1StandardBridge: _opChainConfigs[i].systemConfigProxy.l1StandardBridge(), disputeGameFactory: address(getDisputeGameFactory(_opChainConfigs[i].systemConfigProxy)), optimismPortal: _opChainConfigs[i].systemConfigProxy.optimismPortal(), - optimismMintableERC20Factory: _opChainConfigs[i].systemConfigProxy.optimismMintableERC20Factory() + optimismMintableERC20Factory: _opChainConfigs[i].systemConfigProxy.optimismMintableERC20Factory(), + gasPayingToken: gasPayingToken }); if (IOptimismPortal2(payable(opChainAddrs.optimismPortal)).superchainConfig() != superchainConfig) { @@ -856,7 +858,10 @@ contract OPContractsManager is ISemver { virtual returns (bytes memory) { - return abi.encodeCall(IL1StandardBridge.initialize, (_output.l1CrossDomainMessengerProxy, superchainConfig)); + return abi.encodeCall( + IL1StandardBridge.initialize, + (_output.l1CrossDomainMessengerProxy, superchainConfig, _output.systemConfigProxy) + ); } function encodeDisputeGameFactoryInitializer() internal view virtual returns (bytes memory) { @@ -929,7 +934,8 @@ contract OPContractsManager is ISemver { l1StandardBridge: address(_output.l1StandardBridgeProxy), disputeGameFactory: address(_output.disputeGameFactoryProxy), optimismPortal: address(_output.optimismPortalProxy), - optimismMintableERC20Factory: address(_output.optimismMintableERC20FactoryProxy) + optimismMintableERC20Factory: address(_output.optimismMintableERC20FactoryProxy), + gasPayingToken: Constants.ETHER }); assertValidContractAddress(opChainAddrs_.l1CrossDomainMessenger); diff --git a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol index 6c65381b2cd37..8a36b9c708817 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortal2.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortal2.sol @@ -12,11 +12,15 @@ import { Constants } from "src/libraries/Constants.sol"; import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { SecureMerkleTrie } from "src/libraries/trie/SecureMerkleTrie.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { BadTarget, LargeCalldata, SmallGasLimit, + TransferFailed, + OnlyCustomGasToken, + NoValue, Unauthorized, CallPaused, GasEstimation, @@ -41,6 +45,10 @@ import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISuperchainConfig } from "interfaces/L1/ISuperchainConfig.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; + +/// @notice This is temporary. Error thrown when a chain uses a custom gas token. +error CustomGasTokenNotSupported(); /// @custom:proxied true /// @title OptimismPortal2 @@ -130,10 +138,12 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { /// proof submission should be used when finalizing a withdrawal. mapping(bytes32 => address[]) public proofSubmitters; - /// @custom:legacy - /// @custom:spacer _balance - /// @notice Spacer taking up the legacy `_balance` slot. - uint256 private spacer_61_0_32; + /// @notice Represents the amount of native asset minted in L2. This may not + /// be 100% accurate due to the ability to send ether to the contract + /// without triggering a deposit transaction. It also is used to prevent + /// overflows for L2 account balances when custom gas tokens are used. + /// It is not safe to trust `ERC20.balanceOf` as it may lie. + uint256 internal _balance; /// @notice Emitted when a transaction is deposited from L1 to L2. /// The parameters of this event are read by the rollup node and used to derive deposit @@ -223,6 +233,19 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { __ResourceMetering_init(); } + /// @notice Getter for the balance of the contract. + function balance() public view returns (uint256) { + (address token,) = gasPayingToken(); + if (token == Constants.ETHER) { + return address(this).balance; + } else { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + return _balance; + } + } + /// @notice Getter function for the address of the guardian. /// Public getter is legacy and will be removed in the future. Use `SuperchainConfig.guardian()` instead. /// @return Address of the guardian. @@ -272,6 +295,11 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Intentionally empty. } + /// @notice Returns the gas paying token and its decimals. + function gasPayingToken() internal view returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = systemConfig.gasPayingToken(); + } + /// @notice Getter for the resource config. /// Used internally by the ResourceMetering contract. /// The SystemConfig is the source of truth for the resource config. @@ -408,14 +436,53 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Set the l2Sender so contracts know who triggered this withdrawal on L2. l2Sender = _tx.sender; - // Trigger the call to the target contract. We use a custom low level method - // SafeCall.callWithMinGas to ensure two key properties - // 1. Target contracts cannot force this call to run out of gas by returning a very large - // amount of data (and this is OK because we don't care about the returndata here). - // 2. The amount of gas provided to the execution context of the target is at least the - // gas limit specified by the user. If there is not enough gas in the current context - // to accomplish this, `callWithMinGas` will revert. - bool success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data); + bool success; + (address token,) = gasPayingToken(); + if (token == Constants.ETHER) { + // Trigger the call to the target contract. We use a custom low level method + // SafeCall.callWithMinGas to ensure two key properties + // 1. Target contracts cannot force this call to run out of gas by returning a very large + // amount of data (and this is OK because we don't care about the returndata here). + // 2. The amount of gas provided to the execution context of the target is at least the + // gas limit specified by the user. If there is not enough gas in the current context + // to accomplish this, `callWithMinGas` will revert. + success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, _tx.value, _tx.data); + } else { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + // Cannot call the token contract directly from the portal. This would allow an attacker + // to call approve from a withdrawal and drain the balance of the portal. + if (_tx.target == token) revert BadTarget(); + + // Only transfer value when a non zero value is specified. This saves gas in the case of + // using the standard bridge or arbitrary message passing. + if (_tx.value != 0) { + // Update the contracts internal accounting of the amount of native asset in L2. + _balance -= _tx.value; + + // Read the balance of the target contract before the transfer so the consistency + // of the transfer can be checked afterwards. + uint256 startBalance = IERC20(token).balanceOf(address(this)); + + // Transfer the ERC20 balance to the target, accounting for non standard ERC20 + // implementations that may not return a boolean. This reverts if the low level + // call is not successful. + IERC20(token).safeTransfer({ to: _tx.target, value: _tx.value }); + + // The balance must be transferred exactly. + if (IERC20(token).balanceOf(address(this)) != startBalance - _tx.value) { + revert TransferFailed(); + } + } + + // Make a call to the target contract only if there is calldata. + if (_tx.data.length != 0) { + success = SafeCall.callWithMinGas(_tx.target, _tx.gasLimit, 0, _tx.data); + } else { + success = true; + } + } // Reset the l2Sender back to the default value. l2Sender = Constants.DEFAULT_L2_SENDER; @@ -432,6 +499,58 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { } } + /// @notice Entrypoint to depositing an ERC20 token as a custom gas token. + /// This function depends on a well formed ERC20 token. There are only + /// so many checks that can be done on chain for this so it is assumed + /// that chain operators will deploy chains with well formed ERC20 tokens. + /// @param _to Target address on L2. + /// @param _mint Units of ERC20 token to deposit into L2. + /// @param _value Units of ERC20 token to send on L2 to the recipient. + /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. + /// @param _isCreation Whether or not the transaction is a contract creation. + /// @param _data Data to trigger the recipient with. + function depositERC20Transaction( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) + public + metered(_gasLimit) + { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + // Can only be called if an ERC20 token is used for gas paying on L2 + (address token,) = gasPayingToken(); + if (token == Constants.ETHER) revert OnlyCustomGasToken(); + + // Gives overflow protection for L2 account balances. + _balance += _mint; + + // Get the balance of the portal before the transfer. + uint256 startBalance = IERC20(token).balanceOf(address(this)); + + // Take ownership of the token. It is assumed that the user has given the portal an approval. + IERC20(token).safeTransferFrom({ from: msg.sender, to: address(this), value: _mint }); + + // Double check that the portal now has the exact amount of token. + if (IERC20(token).balanceOf(address(this)) != startBalance + _mint) { + revert TransferFailed(); + } + + _depositTransaction({ + _to: _to, + _mint: _mint, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + /// @notice Accepts deposits of ETH and data, and emits a TransactionDeposited event for use in /// deriving deposit transactions. Note that if a deposit is made by a contract, its /// address will be aliased when retrieved using `tx.origin` or `msg.sender`. Consider @@ -451,6 +570,40 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { public payable metered(_gasLimit) + { + (address token,) = gasPayingToken(); + + // Temporary revert till we support custom gas tokens + // if (token != Constants.ETHER) revert CustomGasTokenNotSupported(); + + if (token != Constants.ETHER && msg.value != 0) revert NoValue(); + + _depositTransaction({ + _to: _to, + _mint: msg.value, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + + /// @notice Common logic for creating deposit transactions. + /// @param _to Target address on L2. + /// @param _mint Units of asset to deposit into L2. + /// @param _value Units of asset to send on L2 to the recipient. + /// @param _gasLimit Amount of L2 gas to purchase by burning gas on L1. + /// @param _isCreation Whether or not the transaction is a contract creation. + /// @param _data Data to trigger the recipient with. + function _depositTransaction( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes memory _data + ) + internal { // Just to be safe, make sure that people specify address(0) as the target when doing // contract creations. @@ -475,13 +628,41 @@ contract OptimismPortal2 is Initializable, ResourceMetering, ISemver { // Compute the opaque data that will be emitted as part of the TransactionDeposited event. // We use opaque data so that we can update the TransactionDeposited event in the future // without breaking the current interface. - bytes memory opaqueData = abi.encodePacked(msg.value, _value, _gasLimit, _isCreation, _data); + bytes memory opaqueData = abi.encodePacked(_mint, _value, _gasLimit, _isCreation, _data); // Emit a TransactionDeposited event so that the rollup node can derive a deposit // transaction for this deposit. emit TransactionDeposited(from, _to, DEPOSIT_VERSION, opaqueData); } + /// @notice Sets the gas paying token for the L2 system. This token is used as the + /// L2 native asset. Only the SystemConfig contract can call this function. + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + if (msg.sender != address(systemConfig)) revert Unauthorized(); + + // Set L2 deposit gas as used without paying burning gas. Ensures that deposits cannot use too much L2 gas. + // This value must be large enough to cover the cost of calling `L1Block.setGasPayingToken`. + useGas(SYSTEM_DEPOSIT_GAS_LIMIT); + + // Emit the special deposit transaction directly that sets the gas paying + // token in the L1Block predeploy contract. + emit TransactionDeposited( + Constants.DEPOSITOR_ACCOUNT, + Predeploys.L1_BLOCK_ATTRIBUTES, + DEPOSIT_VERSION, + abi.encodePacked( + uint256(0), // mint + uint256(0), // value + uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit + false, // isCreation, + abi.encodeCall(IL1Block.setGasPayingToken, (_token, _decimals, _name, _symbol)) + ) + ); + } + /// @notice Blacklists a dispute game. Should only be used in the event that a dispute game resolves incorrectly. /// @param _disputeGame Dispute game to blacklist. function blacklistDisputeGame(IDisputeGame _disputeGame) external { diff --git a/packages/contracts-bedrock/src/L1/SystemConfig.sol b/packages/contracts-bedrock/src/L1/SystemConfig.sol index d5cc62db1a6d5..fd94f56e0c704 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfig.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfig.sol @@ -3,20 +3,27 @@ pragma solidity 0.8.15; // Contracts import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // Libraries import { Storage } from "src/libraries/Storage.sol"; +import { Constants } from "src/libraries/Constants.sol"; +import { GasPayingToken, IGasToken } from "src/libraries/GasPayingToken.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +/// @dev This is temporary. Error thrown when a chain uses a custom gas token. +error CustomGasTokenNotSupported(); + /// @custom:proxied true /// @title SystemConfig /// @notice The SystemConfig contract is used to manage configuration of an Optimism network. /// All configuration is stored on L1 and picked up by L2 as part of the derviation of /// the L2 chain. -contract SystemConfig is OwnableUpgradeable, ISemver { +contract SystemConfig is OwnableUpgradeable, ISemver, IGasToken { /// @notice Enum representing different types of updates. /// @custom:value BATCHER Represents an update to the batcher hash. /// @custom:value FEE_SCALARS Represents an update to l1 data fee scalars. @@ -41,6 +48,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { address disputeGameFactory; address optimismPortal; address optimismMintableERC20Factory; + address gasPayingToken; } /// @notice Version identifier, used for upgrades. @@ -83,6 +91,9 @@ contract SystemConfig is OwnableUpgradeable, ISemver { bytes32 public constant DISPUTE_GAME_FACTORY_SLOT = bytes32(uint256(keccak256("systemconfig.disputegamefactory")) - 1); + /// @notice The number of decimals that the gas paying token has. + uint8 internal constant GAS_PAYING_TOKEN_DECIMALS = 18; + /// @notice The maximum gas limit that can be set for L2 blocks. This limit is used to enforce that the blocks /// on L2 are not too large to process and prove. Over time, this value can be increased as various /// optimizations and improvements are made to the system at large. @@ -186,6 +197,7 @@ contract SystemConfig is OwnableUpgradeable, ISemver { Storage.setAddress(OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT, _addresses.optimismMintableERC20Factory); _setStartBlock(); + _setGasPayingToken(_addresses.gasPayingToken); _setResourceConfig(_config); } @@ -248,13 +260,15 @@ contract SystemConfig is OwnableUpgradeable, ISemver { /// @notice Consolidated getter for the Addresses struct. function getAddresses() external view returns (Addresses memory) { + (address token,) = gasPayingToken(); return Addresses({ l1CrossDomainMessenger: l1CrossDomainMessenger(), l1ERC721Bridge: l1ERC721Bridge(), l1StandardBridge: l1StandardBridge(), disputeGameFactory: disputeGameFactory(), optimismPortal: optimismPortal(), - optimismMintableERC20Factory: optimismMintableERC20Factory() + optimismMintableERC20Factory: optimismMintableERC20Factory(), + gasPayingToken: token }); } @@ -268,6 +282,55 @@ contract SystemConfig is OwnableUpgradeable, ISemver { startBlock_ = Storage.getUint(START_BLOCK_SLOT); } + /// @notice Getter for the gas paying asset address. + function gasPayingToken() public view returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = GasPayingToken.getToken(); + } + + /// @notice Getter for custom gas token paying networks. Returns true if the + /// network uses a custom gas token. + function isCustomGasToken() public view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + + /// @notice Getter for the gas paying token name. + function gasPayingTokenName() external view returns (string memory name_) { + name_ = GasPayingToken.getName(); + } + + /// @notice Getter for the gas paying token symbol. + function gasPayingTokenSymbol() external view returns (string memory symbol_) { + symbol_ = GasPayingToken.getSymbol(); + } + + /// @notice Internal setter for the gas paying token address, includes validation. + /// The token must not already be set and must be non zero and not the ether address + /// to set the token address. This prevents the token address from being changed + /// and makes it explicitly opt-in to use custom gas token. + /// @param _token Address of the gas paying token. + function _setGasPayingToken(address _token) internal virtual { + if (_token != address(0) && _token != Constants.ETHER && !isCustomGasToken()) { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + require( + ERC20(_token).decimals() == GAS_PAYING_TOKEN_DECIMALS, "SystemConfig: bad decimals of gas paying token" + ); + bytes32 name = GasPayingToken.sanitize(ERC20(_token).name()); + bytes32 symbol = GasPayingToken.sanitize(ERC20(_token).symbol()); + + // Set the gas paying token in storage and in the OptimismPortal. + GasPayingToken.set({ _token: _token, _decimals: GAS_PAYING_TOKEN_DECIMALS, _name: name, _symbol: symbol }); + IOptimismPortal2(payable(optimismPortal())).setGasPayingToken({ + _token: _token, + _decimals: GAS_PAYING_TOKEN_DECIMALS, + _name: name, + _symbol: symbol + }); + } + } + /// @notice Updates the unsafe block signer address. Can only be called by the owner. /// @param _unsafeBlockSigner New unsafe block signer address. function setUnsafeBlockSigner(address _unsafeBlockSigner) external onlyOwner { diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol index e18ed7400f39c..e2d181b2f99f4 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol @@ -2,9 +2,12 @@ pragma solidity 0.8.15; // Contracts +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SystemConfig } from "src/L1/SystemConfig.sol"; // Libraries +import { Constants } from "src/libraries/Constants.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; import { Storage } from "src/libraries/Storage.sol"; @@ -13,6 +16,9 @@ import { IOptimismPortalInterop as IOptimismPortal } from "interfaces/L1/IOptimi import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; +/// @dev This is temporary. Error thrown when a chain uses a custom gas token. +error CustomGasTokenNotSupported(); + /// @custom:proxied true /// @title SystemConfigInterop /// @notice The SystemConfig contract is used to manage configuration of an Optimism network. @@ -70,6 +76,38 @@ contract SystemConfigInterop is SystemConfig { return string.concat(super.version(), "+interop"); } + /// @notice Internal setter for the gas paying token address, includes validation. + /// The token must not already be set and must be non zero and not the ether address + /// to set the token address. This prevents the token address from being changed + /// and makes it explicitly opt-in to use custom gas token. Additionally, + /// OptimismPortal's address must be non zero, since otherwise the call to set the + /// config for the gas paying token to OptimismPortal will fail. + /// @param _token Address of the gas paying token. + function _setGasPayingToken(address _token) internal override { + if (_token != address(0) && _token != Constants.ETHER && !isCustomGasToken()) { + // Temporary revert till we support custom gas tokens + // if (true) revert CustomGasTokenNotSupported(); + + require( + ERC20(_token).decimals() == GAS_PAYING_TOKEN_DECIMALS, "SystemConfig: bad decimals of gas paying token" + ); + bytes32 name = GasPayingToken.sanitize(ERC20(_token).name()); + bytes32 symbol = GasPayingToken.sanitize(ERC20(_token).symbol()); + + // Set the gas paying token in storage and in the OptimismPortal. + GasPayingToken.set({ _token: _token, _decimals: GAS_PAYING_TOKEN_DECIMALS, _name: name, _symbol: symbol }); + IOptimismPortal(payable(optimismPortal())).setConfig( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ + _token: _token, + _decimals: GAS_PAYING_TOKEN_DECIMALS, + _name: name, + _symbol: symbol + }) + ); + } + } + /// @notice Adds a chain to the interop dependency set. Can only be called by the dependency manager. /// @param _chainId Chain ID of chain to add. function addDependency(uint256 _chainId) external { diff --git a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol index 5a8c97f819b8e..2b876d1e52877 100644 --- a/packages/contracts-bedrock/src/L2/ETHLiquidity.sol +++ b/packages/contracts-bedrock/src/L2/ETHLiquidity.sol @@ -5,11 +5,12 @@ pragma solidity 0.8.15; import { SafeSend } from "src/universal/SafeSend.sol"; // Libraries -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000025 @@ -24,12 +25,13 @@ contract ETHLiquidity is ISemver { event LiquidityMinted(address indexed caller, uint256 value); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.6 - string public constant version = "1.0.0-beta.6"; + /// @custom:semver 1.0.0-beta.5 + string public constant version = "1.0.0-beta.5"; /// @notice Allows an address to lock ETH liquidity into this contract. function burn() external payable { if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); emit LiquidityBurned(msg.sender, msg.value); } @@ -37,6 +39,7 @@ contract ETHLiquidity is ISemver { /// @param _amount The amount of liquidity to unlock. function mint(uint256 _amount) external { if (msg.sender != Predeploys.SUPERCHAIN_WETH) revert Unauthorized(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); new SafeSend{ value: _amount }(payable(msg.sender)); emit LiquidityMinted(msg.sender, _amount); } diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 9814adbf843a7..48d6e70e9c504 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { GasPayingToken, IGasToken } from "src/libraries/GasPayingToken.sol"; import { NotDepositor } from "src/libraries/L1BlockErrors.sol"; // Interfaces @@ -15,7 +16,10 @@ import { ISemver } from "interfaces/universal/ISemver.sol"; /// Values within this contract are updated once per epoch (every L1 block) and can only be /// set by the "depositor" account, a special system address. Depositor account transactions /// are created by the protocol whenever we move to a new epoch. -contract L1Block is ISemver { +contract L1Block is ISemver, IGasToken { + /// @notice Event emitted when the gas paying token is set. + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + /// @notice Address of the special depositor account. function DEPOSITOR_ACCOUNT() public pure returns (address addr_) { addr_ = Constants.DEPOSITOR_ACCOUNT; @@ -56,35 +60,34 @@ contract L1Block is ISemver { /// @notice The latest L1 blob base fee. uint256 public blobBaseFee; - /// @custom:semver 1.5.1-beta.6 + /// @custom:semver 1.5.1-beta.5 function version() public pure virtual returns (string memory) { - return "1.5.1-beta.6"; + return "1.5.1-beta.5"; } /// @notice Returns the gas paying token, its decimals, name and symbol. - function gasPayingToken() public pure returns (address addr_, uint8 decimals_) { - addr_ = Constants.ETHER; - decimals_ = 18; + /// If nothing is set in state, then it means ether is used. + function gasPayingToken() public view returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = GasPayingToken.getToken(); } /// @notice Returns the gas paying token name. /// If nothing is set in state, then it means ether is used. - /// This function cannot be removed because WETH depends on it. - function gasPayingTokenName() public pure returns (string memory name_) { - name_ = "Ether"; + function gasPayingTokenName() public view returns (string memory name_) { + name_ = GasPayingToken.getName(); } /// @notice Returns the gas paying token symbol. /// If nothing is set in state, then it means ether is used. - /// This function cannot be removed because WETH depends on it. - function gasPayingTokenSymbol() public pure returns (string memory symbol_) { - symbol_ = "ETH"; + function gasPayingTokenSymbol() public view returns (string memory symbol_) { + symbol_ = GasPayingToken.getSymbol(); } /// @notice Getter for custom gas token paying networks. Returns true if the /// network uses a custom gas token. - function isCustomGasToken() public pure returns (bool is_) { - is_ = false; + function isCustomGasToken() public view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; } /// @notice size of historyHashes. @@ -201,4 +204,15 @@ contract L1Block is ISemver { function historySize() external pure returns (uint256) { return HISTORY_SIZE; } + + /// @notice Sets the gas paying token for the L2 system. Can only be called by the special + /// depositor account. This function is not called on every L2 block but instead + /// only called by specially crafted L1 deposit transactions. + function setGasPayingToken(address _token, uint8 _decimals, bytes32 _name, bytes32 _symbol) external { + if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); + + GasPayingToken.set({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }); + + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: _name, symbol: _symbol }); + } } diff --git a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol b/packages/contracts-bedrock/src/L2/L1BlockInterop.sol index 3cc03c1ef5349..7b92b202a052d 100644 --- a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol +++ b/packages/contracts-bedrock/src/L2/L1BlockInterop.sol @@ -6,6 +6,7 @@ import { L1Block } from "src/L2/L1Block.sol"; // Libraries import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { @@ -18,9 +19,11 @@ import { } from "src/libraries/L1BlockErrors.sol"; /// @notice Enum representing different types of configurations that can be set on L1BlockInterop. +/// @custom:value SET_GAS_PAYING_TOKEN Represents the config type for setting the gas paying token. /// @custom:value ADD_DEPENDENCY Represents the config type for adding a chain to the interop dependency set. /// @custom:value REMOVE_DEPENDENCY Represents the config type for removing a chain from the interop dependency set. enum ConfigType { + SET_GAS_PAYING_TOKEN, ADD_DEPENDENCY, REMOVE_DEPENDENCY } @@ -46,9 +49,9 @@ contract L1BlockInterop is L1Block { /// keccak256(abi.encode(uint256(keccak256("l1Block.identifier.isDeposit")) - 1)) & ~bytes32(uint256(0xff)) uint256 internal constant IS_DEPOSIT_SLOT = 0x921bd3a089295c6e5540e8fba8195448d253efd6f2e3e495b499b627dc36a300; - /// @custom:semver +interop-beta.4 + /// @custom:semver +interop-beta.3 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop-beta.4"); + return string.concat(super.version(), "+interop-beta.3"); } /// @notice Returns whether the call was triggered from a a deposit or not. @@ -104,13 +107,25 @@ contract L1BlockInterop is L1Block { function setConfig(ConfigType _type, bytes calldata _value) external { if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); - if (_type == ConfigType.ADD_DEPENDENCY) { + if (_type == ConfigType.SET_GAS_PAYING_TOKEN) { + _setGasPayingToken(_value); + } else if (_type == ConfigType.ADD_DEPENDENCY) { _addDependency(_value); } else if (_type == ConfigType.REMOVE_DEPENDENCY) { _removeDependency(_value); } } + /// @notice Internal method to set the gas paying token. + /// @param _value The encoded value with which to set the gas paying token. + function _setGasPayingToken(bytes calldata _value) internal { + (address token, uint8 decimals, bytes32 name, bytes32 symbol) = StaticConfig.decodeSetGasPayingToken(_value); + + GasPayingToken.set({ _token: token, _decimals: decimals, _name: name, _symbol: symbol }); + + emit GasPayingTokenSet({ token: token, decimals: decimals, name: name, symbol: symbol }); + } + /// @notice Internal method to add a dependency to the interop dependency set. /// @param _value The encoded value with which to add the dependency. function _addDependency(bytes calldata _value) internal { diff --git a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol index 8c5af32f016ea..58143132d713b 100644 --- a/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/L2/L2CrossDomainMessenger.sol @@ -11,6 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000007 @@ -19,8 +20,8 @@ import { IL2ToL1MessagePasser } from "interfaces/L2/IL2ToL1MessagePasser.sol"; /// L2 on the L2 side. Users are generally encouraged to use this contract instead of lower /// level message passing contracts. contract L2CrossDomainMessenger is CrossDomainMessenger, ISemver { - /// @custom:semver 2.1.1-beta.8 - string public constant version = "2.1.1-beta.8"; + /// @custom:semver 2.1.1-beta.7 + string public constant version = "2.1.1-beta.7"; /// @notice Constructs the L2CrossDomainMessenger contract. constructor() { @@ -48,6 +49,11 @@ contract L2CrossDomainMessenger is CrossDomainMessenger, ISemver { ); } + /// @inheritdoc CrossDomainMessenger + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingToken(); + } + /// @inheritdoc CrossDomainMessenger function _isOtherMessenger() internal view override returns (bool) { return AddressAliasHelper.undoL1ToL2Alias(msg.sender) == address(otherMessenger); diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol index 7fe46d67b7fa9..6acff3790a8ef 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridge.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridge.sol @@ -11,6 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { ICrossDomainMessenger } from "interfaces/universal/ICrossDomainMessenger.sol"; import { OptimismMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @custom:proxied true /// @custom:predeploy 0x4200000000000000000000000000000000000010 @@ -57,9 +58,9 @@ contract L2StandardBridge is StandardBridge, ISemver { ); /// @notice Semantic version. - /// @custom:semver 1.11.1-beta.8 + /// @custom:semver 1.11.1-beta.7 function version() public pure virtual returns (string memory) { - return "1.11.1-beta.8"; + return "1.11.1-beta.7"; } /// @notice Constructs the L2StandardBridge contract. @@ -83,6 +84,11 @@ contract L2StandardBridge is StandardBridge, ISemver { ); } + /// @inheritdoc StandardBridge + function gasPayingToken() internal view override returns (address addr_, uint8 decimals_) { + (addr_, decimals_) = IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingToken(); + } + /// @custom:legacy /// @notice Initiates a withdrawal from L2 to L1. /// This function only works with OptimismMintableERC20 tokens or ether. Use the @@ -103,6 +109,7 @@ contract L2StandardBridge is StandardBridge, ISemver { virtual onlyEOA { + require(isCustomGasToken() == false, "L2StandardBridge: not supported with custom gas token"); _initiateWithdrawal(_l2Token, msg.sender, msg.sender, _amount, _minGasLimit, _extraData); } @@ -131,6 +138,7 @@ contract L2StandardBridge is StandardBridge, ISemver { payable virtual { + require(isCustomGasToken() == false, "L2StandardBridge: not supported with custom gas token"); _initiateWithdrawal(_l2Token, msg.sender, _to, _amount, _minGasLimit, _extraData); } diff --git a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol index 0ac13b27f8bb2..589454b724723 100644 --- a/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol +++ b/packages/contracts-bedrock/src/L2/L2StandardBridgeInterop.sol @@ -39,9 +39,9 @@ contract L2StandardBridgeInterop is L2StandardBridge { event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount); /// @notice Semantic version. - /// @custom:semver +interop-beta.8 + /// @custom:semver +interop-beta.7 function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop-beta.8"); + return string.concat(super.version(), "+interop-beta.7"); } /// @notice Converts `amount` of `from` token to `to` token. diff --git a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol index 989d6d55ca25a..ab6ff44a33aba 100644 --- a/packages/contracts-bedrock/src/L2/SuperchainWETH.sol +++ b/packages/contracts-bedrock/src/L2/SuperchainWETH.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { WETH98 } from "src/universal/WETH98.sol"; // Libraries -import { Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; import { SafeSend } from "src/universal/SafeSend.sol"; @@ -13,6 +13,7 @@ import { SafeSend } from "src/universal/SafeSend.sol"; // Interfaces import { ISemver } from "interfaces/universal/ISemver.sol"; import { IL2ToL2CrossDomainMessenger } from "interfaces/L2/IL2ToL2CrossDomainMessenger.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; import { IETHLiquidity } from "interfaces/L2/IETHLiquidity.sol"; import { IERC7802, IERC165 } from "interfaces/L2/IERC7802.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -42,8 +43,20 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { event RelayETH(address indexed from, address indexed to, uint256 amount, uint256 source); /// @notice Semantic version. - /// @custom:semver 1.0.0-beta.14 - string public constant version = "1.0.0-beta.14"; + /// @custom:semver 1.0.0-beta.13 + string public constant version = "1.0.0-beta.13"; + + /// @inheritdoc WETH98 + function deposit() public payable override { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + super.deposit(); + } + + /// @inheritdoc WETH98 + function withdraw(uint256 _amount) public override { + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) revert NotCustomGasToken(); + super.withdraw(_amount); + } /// @inheritdoc WETH98 function allowance(address owner, address spender) public view override returns (uint256) { @@ -76,8 +89,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { _mint(_to, _amount); // Withdraw from ETHLiquidity contract. - // NOTE: 'mint' will soon change to 'withdraw'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // NOTE: 'mint' will soon change to 'withdraw'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + } emit CrosschainMint(_to, _amount, msg.sender); } @@ -91,8 +106,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { _burn(_from, _amount); // Deposit to ETHLiquidity contract. - // NOTE: 'burn' will soon change to 'deposit'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _amount }(); + if (!IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // NOTE: 'burn' will soon change to 'deposit'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: _amount }(); + } emit CrosschainBurn(_from, _amount, msg.sender); } @@ -110,6 +127,10 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { function sendETH(address _to, uint256 _chainId) external payable returns (bytes32 msgHash_) { if (_to == address(0)) revert ZeroAddress(); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + revert NotCustomGasToken(); + } + // NOTE: 'burn' will soon change to 'deposit'. IETHLiquidity(Predeploys.ETH_LIQUIDITY).burn{ value: msg.value }(); @@ -134,11 +155,16 @@ contract SuperchainWETH is WETH98, IERC7802, ISemver { if (crossDomainMessageSender != address(this)) revert InvalidCrossDomainSender(); - // NOTE: 'mint' will soon change to 'withdraw'. - IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isCustomGasToken()) { + // Since ETH is not the native asset on custom gas token chains, send SuperchainWETH to the recipient. + _mint(_to, _amount); + } else { + // NOTE: 'mint' will soon change to 'withdraw'. + IETHLiquidity(Predeploys.ETH_LIQUIDITY).mint(_amount); - // This is a forced ETH send to the recipient, the recipient should NOT expect to be called. - new SafeSend{ value: _amount }(payable(_to)); + // This is a forced ETH send to the recipient, the recipient should NOT expect to be called. + new SafeSend{ value: _amount }(payable(_to)); + } emit RelayETH(_from, _to, _amount, source); } diff --git a/packages/contracts-bedrock/src/L2/WETH.sol b/packages/contracts-bedrock/src/L2/WETH.sol index 558beaaa8d890..5dc716fca569b 100644 --- a/packages/contracts-bedrock/src/L2/WETH.sol +++ b/packages/contracts-bedrock/src/L2/WETH.sol @@ -13,20 +13,19 @@ import { IL1Block } from "interfaces/L2/IL1Block.sol"; /// @title WETH contract that reads the name and symbol from the L1Block contract. /// Allows for nice rendering of token names for chains using custom gas token. -/// This contract is not proxied and contains calls to the custom gas token methods. contract WETH is WETH98, ISemver { - /// @custom:semver 1.1.0-beta.5 - string public constant version = "1.1.0-beta.5"; + /// @custom:semver 1.1.0-beta.4 + string public constant version = "1.1.0-beta.4"; /// @notice Returns the name of the wrapped native asset. Will be "Wrapped Ether" /// if the native asset is Ether. - function name() external pure override returns (string memory name_) { + function name() external view override returns (string memory name_) { name_ = string.concat("Wrapped ", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenName()); } /// @notice Returns the symbol of the wrapped native asset. Will be "WETH" if the /// native asset is Ether. - function symbol() external pure override returns (string memory symbol_) { + function symbol() external view override returns (string memory symbol_) { symbol_ = string.concat("W", IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).gasPayingTokenSymbol()); } } diff --git a/packages/contracts-bedrock/src/libraries/PortalErrors.sol b/packages/contracts-bedrock/src/libraries/PortalErrors.sol index 9096a2938fdc1..6004066b397ee 100644 --- a/packages/contracts-bedrock/src/libraries/PortalErrors.sol +++ b/packages/contracts-bedrock/src/libraries/PortalErrors.sol @@ -9,6 +9,8 @@ error LargeCalldata(); error SmallGasLimit(); /// @notice Error for when a withdrawal transfer fails. error TransferFailed(); +/// @notice Error for when a method is called that only works when using a custom gas token. +error OnlyCustomGasToken(); /// @notice Error for when a method cannot be called with non zero CALLVALUE. error NoValue(); /// @notice Error for an unauthorized CALLER. diff --git a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol index c04d915bd2326..30ce96972a191 100644 --- a/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol +++ b/packages/contracts-bedrock/src/libraries/errors/CommonErrors.sol @@ -4,6 +4,12 @@ pragma solidity ^0.8.0; /// @notice Error for an unauthorized CALLER. error Unauthorized(); +/// @notice Error for when a method is called that only works when using a custom gas token. +error OnlyCustomGasToken(); + +/// @notice Error for when a method is called that only works when NOT using a custom gas token. +error NotCustomGasToken(); + /// @notice Error for when a transfer via call fails. error TransferFailed(); diff --git a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol index 65b781707febe..85d801f0a1a6c 100644 --- a/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol +++ b/packages/contracts-bedrock/src/universal/CrossDomainMessenger.sol @@ -175,6 +175,10 @@ abstract contract CrossDomainMessenger is /// @param _message Message to trigger the target address with. /// @param _minGasLimit Minimum gas limit that the message can be executed with. function sendMessage(address _target, bytes calldata _message, uint32 _minGasLimit) external payable { + if (isCustomGasToken()) { + require(msg.value == 0, "CrossDomainMessenger: cannot send value with custom gas token"); + } + // Triggers a message to the other messenger. Note that the amount of gas provided to the // message is the amount of gas requested by the user PLUS the base gas value. We want to // guarantee the property that the call to the target contract will always have at least @@ -359,6 +363,15 @@ abstract contract CrossDomainMessenger is + RELAY_GAS_CHECK_BUFFER; } + /// @notice Returns the address of the gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + /// @notice Initializer. /// @param _otherMessenger CrossDomainMessenger contract on the other chain. function __CrossDomainMessenger_init(CrossDomainMessenger _otherMessenger) internal onlyInitializing { diff --git a/packages/contracts-bedrock/src/universal/StandardBridge.sol b/packages/contracts-bedrock/src/universal/StandardBridge.sol index 0bfa5698ce8df..51316b82dac5a 100644 --- a/packages/contracts-bedrock/src/universal/StandardBridge.sol +++ b/packages/contracts-bedrock/src/universal/StandardBridge.sol @@ -9,6 +9,7 @@ import { Address } from "@openzeppelin/contracts/utils/Address.sol"; import { ERC165Checker } from "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { SafeCall } from "src/libraries/SafeCall.sol"; +import { Constants } from "src/libraries/Constants.sol"; // Interfaces import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -134,6 +135,15 @@ abstract contract StandardBridge is Initializable { /// Must be implemented by contracts that inherit. receive() external payable virtual; + /// @notice Returns the address of the custom gas token and the token's decimals. + function gasPayingToken() internal view virtual returns (address, uint8); + + /// @notice Returns whether the chain uses a custom gas token or not. + function isCustomGasToken() internal view returns (bool) { + (address token,) = gasPayingToken(); + return token != Constants.ETHER; + } + /// @notice Getter for messenger contract. /// Public getter is legacy and will be removed in the future. Use `messenger` instead. /// @return Contract of the messenger on this domain. @@ -247,6 +257,7 @@ abstract contract StandardBridge is Initializable { onlyOtherBridge { require(paused() == false, "StandardBridge: paused"); + require(isCustomGasToken() == false, "StandardBridge: cannot bridge ETH with custom gas token"); require(msg.value == _amount, "StandardBridge: amount sent does not match amount required"); require(_to != address(this), "StandardBridge: cannot send to self"); require(_to != address(messenger), "StandardBridge: cannot send to messenger"); @@ -315,6 +326,7 @@ abstract contract StandardBridge is Initializable { ) internal { + require(isCustomGasToken() == false, "StandardBridge: cannot bridge ETH with custom gas token"); require(msg.value == _amount, "StandardBridge: bridging ETH must include sufficient ETH value"); // Emit the correct events. By default this will be _amount, but child diff --git a/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol index 762ac3ed19063..4f76331555c60 100644 --- a/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L1/L1CrossDomainMessenger.t.sol @@ -702,6 +702,147 @@ contract L1CrossDomainMessenger_Test is CommonTest { assertTrue(l1CrossDomainMessenger.paused()); assertEq(l1CrossDomainMessenger.paused(), superchainConfig.paused()); } + + /// @dev Temporary test that checks that correct calls to sendMessage when using a custom gas token revert with the + /// expected error. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_sendMessage_customGasToken_reverts() external { + skipIfForkTest("Custom gas token is still supported on forked tests"); + + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + + vm.prank(alice); + // vm.expectRevert(IOptimismPortal2.CustomGasTokenNotSupported.selector); + l1CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that sendMessage succeeds with a custom gas token when the call value is zero. + function test_sendMessage_customGasTokenButNoValue_succeeds() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + + // deposit transaction on the optimism portal should be called + vm.expectCall( + address(optimismPortal2), + abi.encodeCall( + IOptimismPortal2.depositTransaction, + ( + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + 0, + l1CrossDomainMessenger.baseGas(hex"ff", 100), + false, + Encoding.encodeCrossDomainMessage( + l1CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff" + ) + ) + ) + ); + + // TransactionDeposited event + vm.expectEmit(address(optimismPortal2)); + emitTransactionDeposited( + AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)), + Predeploys.L2_CROSS_DOMAIN_MESSENGER, + 0, + 0, + l1CrossDomainMessenger.baseGas(hex"ff", 100), + false, + Encoding.encodeCrossDomainMessage(l1CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff") + ); + + // SentMessage event + vm.expectEmit(address(l1CrossDomainMessenger)); + emit SentMessage(recipient, alice, hex"ff", l1CrossDomainMessenger.messageNonce(), 100); + + // SentMessageExtension1 event + vm.expectEmit(address(l1CrossDomainMessenger)); + emit SentMessageExtension1(alice, 0); + + vm.prank(alice); + l1CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that the sendMessage reverts when call value is non-zero with custom gas token. + function test_sendMessage_customGasTokenWithValue_reverts() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1CrossDomainMessenger_Test: gas paying token functionality DNE on op mainnet"); + + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + + vm.expectRevert("CrossDomainMessenger: cannot send value with custom gas token"); + l1CrossDomainMessenger.sendMessage{ value: 1 }(recipient, hex"aa", uint32(500_000)); + } + + /// @dev Tests that the relayMessage succeeds with a custom gas token when the call value is zero. + function test_relayMessage_customGasTokenAndNoValue_succeeds() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1CrossDomainMessenger_Test: gas paying token functionality DNE on op mainnet"); + + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + + address target = address(0xabcd); + address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER; + + vm.expectCall(target, hex"1111"); + + // set the value of op.l2Sender() to be the L2 Cross Domain Messenger. + vm.store(address(optimismPortal2), bytes32(senderSlotIndex), bytes32(abi.encode(sender))); + vm.prank(address(optimismPortal2)); + + vm.expectEmit(address(l1CrossDomainMessenger)); + + bytes32 hash = Hashing.hashCrossDomainMessage( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), sender, target, 0, 0, hex"1111" + ); + + emit RelayedMessage(hash); + + l1CrossDomainMessenger.relayMessage( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), // nonce + sender, + target, + 0, // value + 0, + hex"1111" + ); + + // the message hash is in the successfulMessages mapping + assertTrue(l1CrossDomainMessenger.successfulMessages(hash)); + // it is not in the received messages mapping + assertEq(l1CrossDomainMessenger.failedMessages(hash), false); + } + + /// @dev Tests that the relayMessage reverts when call value is non-zero with custom gas token. + /// The L2CrossDomainMessenger contract cannot `sendMessage` with value when using a custom gas token. + function test_relayMessage_customGasTokenWithValue_reverts() external virtual { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.expectRevert("CrossDomainMessenger: value must be zero unless message is from a system address"); + + l1CrossDomainMessenger.relayMessage{ value: 1 }( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), + address(0xabcd), + address(0xabcd), + 1, // value + 0, + hex"1111" + ); + } } /// @dev A regression test against a reentrancy vulnerability in the CrossDomainMessenger contract, which diff --git a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol index 1eb9e3f952ef5..0a8405fe2f131 100644 --- a/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L1/L1StandardBridge.t.sol @@ -191,6 +191,26 @@ contract L1StandardBridge_Receive_Test is CommonTest { } } +contract L1StandardBridge_Receive_TestFail is CommonTest { + /// @dev Tests receive function reverts with custom gas token. + function testFuzz_receive_customGasToken_reverts(uint256 _value) external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_Receive_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.prank(alice, alice); + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.deal(alice, _value); + (bool success, bytes memory data) = address(l1StandardBridge).call{ value: _value }(hex""); + assertFalse(success); + assembly { + data := add(data, 0x04) + } + assertEq(abi.decode(data, (string)), "StandardBridge: cannot bridge ETH with custom gas token"); + } +} + contract PreBridgeETH is CommonTest { /// @dev Asserts the expected calls and events for bridging ETH depending /// on whether the bridge call is legacy or not. @@ -280,6 +300,19 @@ contract L1StandardBridge_DepositETH_TestFail is CommonTest { vm.prank(alice); l1StandardBridge.depositETH{ value: 1 }(300, hex""); } + + /// @dev Tests that depositing reverts with custom gas token. + function test_depositETH_customGasToken_reverts() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_DepositETH_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.prank(alice, alice); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + l1StandardBridge.depositETH(50000, hex"dead"); + } } contract L1StandardBridge_BridgeETH_Test is PreBridgeETH { @@ -296,6 +329,22 @@ contract L1StandardBridge_BridgeETH_Test is PreBridgeETH { } } +contract L1StandardBridge_BridgeETH_TestFail is PreBridgeETH { + /// @dev Tests that bridging eth reverts with custom gas token. + function test_bridgeETH_customGasToken_reverts() external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_BridgeETH_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.prank(alice, alice); + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l1StandardBridge.bridgeETH(50000, hex"dead"); + } +} + contract PreBridgeETHTo is CommonTest { /// @dev Asserts the expected calls and events for bridging ETH to a different /// address depending on whether the bridge call is legacy or not. @@ -376,6 +425,29 @@ contract L1StandardBridge_DepositETHTo_Test is PreBridgeETHTo { } } +contract L1StandardBridge_DepositETHTo_TestFail is CommonTest { + /// @dev Tests that depositETHTo reverts with custom gas token. + function testFuzz_depositETHTo_customGasToken_reverts( + uint256 _value, + address _to, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_DepositETHTo_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.deal(address(this), _value); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l1StandardBridge.depositETHTo{ value: _value }(_to, _minGasLimit, _extraData); + } +} + contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { /// @dev Tests that bridging ETH to a different address succeeds. /// Emits ETHDepositInitiated and ETHBridgeInitiated events. @@ -390,6 +462,28 @@ contract L1StandardBridge_BridgeETHTo_Test is PreBridgeETHTo { } } +contract L1StandardBridge_BridgeETHTo_TestFail is PreBridgeETHTo { + /// @dev Tests that bridging reverts with custom gas token. + function testFuzz_bridgeETHTo_customGasToken_reverts( + uint256 _value, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_BridgeETHTo_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.deal(address(this), _value); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l1StandardBridge.bridgeETHTo{ value: _value }(bob, _minGasLimit, _extraData); + } +} + contract L1StandardBridge_DepositERC20_Test is CommonTest { using stdStorage for StdStorage; @@ -581,6 +675,35 @@ contract L1StandardBridge_FinalizeETHWithdrawal_Test is CommonTest { } } +contract L1StandardBridge_FinalizeETHWithdrawal_TestFail is CommonTest { + /// @dev Tests that finalizeETHWithdrawal reverts with custom gas token. + function testFuzz_finalizeETHWithdrawal_customGasToken_reverts( + uint256 _value, + bytes calldata _extraData + ) + external + { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest( + "L1StandardBridge_FinalizeETHWithdrawal_TestFail: gas paying token functionality DNE on op mainnet" + ); + + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.mockCall( + address(l1StandardBridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) + ); + vm.deal(address(l1StandardBridge.messenger()), _value); + vm.prank(address(l1StandardBridge.messenger())); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l1StandardBridge.finalizeETHWithdrawal{ value: _value }(alice, alice, _value, _extraData); + } +} + contract L1StandardBridge_FinalizeERC20Withdrawal_Test is CommonTest { using stdStorage for StdStorage; @@ -666,6 +789,26 @@ contract L1StandardBridge_FinalizeBridgeETH_Test is CommonTest { } contract L1StandardBridge_FinalizeBridgeETH_TestFail is CommonTest { + /// @dev Tests that finalizing bridged reverts with custom gas token. + function testFuzz_finalizeBridgeETH_customGasToken_reverts(uint256 _value, bytes calldata _extraData) external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("L1StandardBridge_FinalizeBridgeETH_TestFail: gas paying token functionality DNE on op mainnet"); + + vm.mockCall( + address(l1StandardBridge.messenger()), + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(address(l1StandardBridge.OTHER_BRIDGE())) + ); + vm.deal(address(l1CrossDomainMessenger), _value); + vm.prank(address(l1CrossDomainMessenger)); + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2)) + ); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l1StandardBridge.finalizeBridgeETH{ value: _value }(alice, alice, _value, _extraData); + } + /// @dev Tests that finalizing bridged ETH reverts if the amount is incorrect. function test_finalizeBridgeETH_incorrectValue_reverts() external { address messenger = address(l1StandardBridge.messenger()); diff --git a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol index b64eb09559dd7..73e5376d8967d 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortal2.t.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.15; // Testing +import { stdError } from "forge-std/Test.sol"; import { VmSafe } from "forge-std/Vm.sol"; +import { MockERC20 } from "solmate/test/utils/mocks/MockERC20.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { NextImpl } from "test/mocks/NextImpl.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; @@ -14,6 +16,8 @@ import { SuperchainConfig } from "src/L1/SuperchainConfig.sol"; import { Types } from "src/libraries/Types.sol"; import { Hashing } from "src/libraries/Hashing.sol"; import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; import "src/dispute/lib/Types.sol"; @@ -21,6 +25,7 @@ import "src/libraries/PortalErrors.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; import { IOptimismPortal2 } from "interfaces/L1/IOptimismPortal2.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { IFaultDisputeGame } from "interfaces/dispute/IFaultDisputeGame.sol"; @@ -34,12 +39,6 @@ contract OptimismPortal2_Test is CommonTest { depositor = makeAddr("depositor"); } - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(optimismPortal2.version()).length > 0); - } - /// @dev Tests that the constructor sets the correct values. /// @notice Marked virtual to be overridden in /// test/kontrol/deployment/DeploymentSummary.t.sol @@ -311,6 +310,186 @@ contract OptimismPortal2_Test is CommonTest { assertEq(address(optimismPortal2).balance, balanceBefore + _mint); } + /// @dev Temporary test that checks that correct calls to setGasPayingToken when using a custom gas token revert + /// with the expected error. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_setGasPayingToken_customGasToken_reverts( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + external + { + skipIfForkTest("Custom gas token is still supported on forked tests"); + + vm.expectRevert(Unauthorized.selector); + optimismPortal2.setGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }); + } + + /// @dev Tests that the gas paying token can be set. + function testFuzz_setGasPayingToken_succeeds( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + + vm.expectEmit(address(optimismPortal2)); + emit TransactionDeposited( + 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001, + Predeploys.L1_BLOCK_ATTRIBUTES, + 0, + abi.encodePacked( + uint256(0), // mint + uint256(0), // value + uint64(200_000), // gasLimit + false, // isCreation, + abi.encodeCall(IL1Block.setGasPayingToken, (_token, _decimals, _name, _symbol)) + ) + ); + + vm.prank(address(systemConfig)); + optimismPortal2.setGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }); + } + + /// @notice Ensures that the deposit event is correct for the `setGasPayingToken` + /// code path that manually emits a deposit transaction outside of the + /// `depositTransaction` function. This is a simple differential test. + function test_setGasPayingToken_correctEvent_succeeds( + address _token, + string calldata _name, + string calldata _symbol + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + + if (bytes(_name).length > 32) { + _name = _name[0:32]; + } + if (bytes(_symbol).length > 32) { + _symbol = _symbol[0:32]; + } + + bytes32 name = GasPayingToken.sanitize(_name); + bytes32 symbol = GasPayingToken.sanitize(_symbol); + + vm.recordLogs(); + + vm.deal(address(systemConfig), 100 ether); + vm.prank(address(systemConfig)); + optimismPortal2.setGasPayingToken({ _token: _token, _decimals: 18, _name: name, _symbol: symbol }); + + vm.prank(Constants.DEPOSITOR_ACCOUNT, Constants.DEPOSITOR_ACCOUNT); + optimismPortal2.depositTransaction({ + _to: Predeploys.L1_BLOCK_ATTRIBUTES, + _value: 0, + _gasLimit: 200_000, + _isCreation: false, + _data: abi.encodeCall(IL1Block.setGasPayingToken, (_token, 18, name, symbol)) + }); + + VmSafe.Log[] memory logs = vm.getRecordedLogs(); + assertEq(logs.length, 2); + + VmSafe.Log memory systemPath = logs[0]; + VmSafe.Log memory userPath = logs[1]; + + assertEq(systemPath.topics.length, 4); + assertEq(systemPath.topics.length, userPath.topics.length); + assertEq(systemPath.topics[0], userPath.topics[0]); + assertEq(systemPath.topics[1], userPath.topics[1]); + assertEq(systemPath.topics[2], userPath.topics[2]); + assertEq(systemPath.topics[3], userPath.topics[3]); + assertEq(systemPath.data, userPath.data); + } + + /// @dev Tests that the gas paying token cannot be set by a non-system config. + function test_setGasPayingToken_notSystemConfig_fails(address _caller) external { + // vm.skip(true, "Custom gas token not supported"); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + + vm.assume(_caller != address(systemConfig)); + vm.prank(_caller); + vm.expectRevert(Unauthorized.selector); + optimismPortal2.setGasPayingToken({ _token: address(0), _decimals: 0, _name: "", _symbol: "" }); + } + + /// @dev Temporary test that checks that correct calls to depositERC20Transaction when using a custom gas token + /// revert + /// with the expected error. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_depositERC20Transaction_customGasToken_reverts() external { + skipIfForkTest("Custom gas token is still supported on forked tests"); + + vm.expectRevert(IOptimismPortal2.OnlyCustomGasToken.selector); + optimismPortal2.depositERC20Transaction(address(0), 0, 0, 0, false, ""); + } + + /// @dev Tests that `depositERC20Transaction` reverts when the gas paying token is ether. + function test_depositERC20Transaction_noCustomGasToken_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + + // Check that the gas paying token is set to ether + (address token,) = systemConfig.gasPayingToken(); + assertEq(token, Constants.ETHER); + + vm.expectRevert(OnlyCustomGasToken.selector); + optimismPortal2.depositERC20Transaction(address(0), 0, 0, 0, false, ""); + } + + function test_depositERC20Transaction_balanceOverflow_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + vm.mockCall(address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(42), 18)); + + // The balance slot + vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(type(uint256).max)); + assertEq(optimismPortal2.balance(), type(uint256).max); + + vm.expectRevert(stdError.arithmeticError); + optimismPortal2.depositERC20Transaction({ + _to: address(0), + _mint: 1, + _value: 1, + _gasLimit: 10_000, + _isCreation: false, + _data: "" + }); + } + + /// @dev Tests that `balance()` returns the correct balance when the gas paying token is ether. + function testFuzz_balance_ether_succeeds(uint256 _amount) external { + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + // Check that the gas paying token is set to ether + (address token,) = systemConfig.gasPayingToken(); + assertEq(token, Constants.ETHER); + + // Increase the balance of the gas paying token + vm.deal(address(optimismPortal2), _amount); + + // Check that the balance has been correctly updated + assertEq(optimismPortal2.balance(), address(optimismPortal2).balance); + } + /// @dev Tests that the donateETH function donates ETH and does no state read/write function test_donateETH_succeeds(uint256 _amount) external { vm.startPrank(alice); @@ -907,6 +1086,152 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assert(bob.balance == bobBalanceBefore + 100); } + /// @dev Tests that `finalizeWithdrawalTransaction` reverts when using a custom gas token. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_finalizeWithdrawalTransaction_customGasToken_reverts() external { + Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"" + }); + // Get withdrawal proof data we can use for testing. + ( + bytes32 _stateRoot_noData, + bytes32 _storageRoot_noData, + bytes32 _outputRoot_noData, + bytes32 _withdrawalHash_noData, + bytes[] memory _withdrawalProof_noData + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + // Setup a dummy output root proof for reuse. + Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot_noData, + messagePasserStorageRoot: _storageRoot_noData, + latestBlockhash: bytes32(uint256(0)) + }); + uint256 _proposedBlockNumber_noData = 0xFF; + IFaultDisputeGame game_noData = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create( + optimismPortal2.respectedGameType(), + Claim.wrap(_outputRoot_noData), + abi.encode(_proposedBlockNumber_noData) + ) + ) + ) + ); + uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); + // Fund the portal so that we can withdraw ETH. + vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); + deal(address(L1Token), address(optimismPortal2), 0xFFFFFFFF); + + // modify the gas token to be non ether + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(L1Token), 18) + ); + + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalProven(_withdrawalHash_noData, alice, bob); + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx_noData, + _disputeGameIndex: _proposedGameIndex_noData, + _outputRootProof: _outputRootProof_noData, + _withdrawalProof: _withdrawalProof_noData + }); + + // Warp and resolve the dispute game. + game_noData.resolveClaim(0, 0); + game_noData.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + + // vm.expectRevert(IOptimismPortal2.CustomGasTokenNotSupported.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx_noData); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds when _tx.data is empty and with a custom gas token. + function test_finalizeWithdrawalTransaction_noTxDataNonEtherGasToken_succeeds() external { + // vm.skip(true, "Custom gas token not supported"); + + Types.WithdrawalTransaction memory _defaultTx_noData = Types.WithdrawalTransaction({ + nonce: 0, + sender: alice, + target: bob, + value: 100, + gasLimit: 100_000, + data: hex"" + }); + // Get withdrawal proof data we can use for testing. + ( + bytes32 _stateRoot_noData, + bytes32 _storageRoot_noData, + bytes32 _outputRoot_noData, + bytes32 _withdrawalHash_noData, + bytes[] memory _withdrawalProof_noData + ) = ffi.getProveWithdrawalTransactionInputs(_defaultTx_noData); + // Setup a dummy output root proof for reuse. + Types.OutputRootProof memory _outputRootProof_noData = Types.OutputRootProof({ + version: bytes32(uint256(0)), + stateRoot: _stateRoot_noData, + messagePasserStorageRoot: _storageRoot_noData, + latestBlockhash: bytes32(uint256(0)) + }); + uint256 _proposedBlockNumber_noData = 0xFF; + IFaultDisputeGame game_noData = IFaultDisputeGame( + payable( + address( + disputeGameFactory.create( + optimismPortal2.respectedGameType(), + Claim.wrap(_outputRoot_noData), + abi.encode(_proposedBlockNumber_noData) + ) + ) + ) + ); + uint256 _proposedGameIndex_noData = disputeGameFactory.gameCount() - 1; + // Warp beyond the chess clocks and finalize the game. + vm.warp(block.timestamp + game_noData.maxClockDuration().raw() + 1 seconds); + // Fund the portal so that we can withdraw ETH. + vm.store(address(optimismPortal2), bytes32(uint256(61)), bytes32(uint256(0xFFFFFFFF))); + deal(address(L1Token), address(optimismPortal2), 0xFFFFFFFF); + + // modify the gas token to be non ether + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(L1Token), 18) + ); + + uint256 bobBalanceBefore = L1Token.balanceOf(bob); + + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalProven(_withdrawalHash_noData, alice, bob); + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalProvenExtension1(_withdrawalHash_noData, address(this)); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx_noData, + _disputeGameIndex: _proposedGameIndex_noData, + _outputRootProof: _outputRootProof_noData, + _withdrawalProof: _withdrawalProof_noData + }); + + // Warp and resolve the dispute game. + game_noData.resolveClaim(0, 0); + game_noData.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1 seconds); + + vm.expectEmit(true, true, false, true); + emit WithdrawalFinalized(_withdrawalHash_noData, true); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx_noData); + + assert(L1Token.balanceOf(bob) == bobBalanceBefore + 100); + } + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. function test_finalizeWithdrawalTransaction_provenWithdrawalHashEther_succeeds() external { uint256 bobBalanceBefore = address(bob).balance; @@ -995,6 +1320,32 @@ contract OptimismPortal2_FinalizeWithdrawal_Test is CommonTest { assert(address(bob).balance == bobBalanceBefore + 100); } + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. + function test_finalizeWithdrawalTransaction_provenWithdrawalHashNonEtherTargetToken_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + vm.mockCall( + address(systemConfig), + abi.encodeCall(systemConfig.gasPayingToken, ()), + abi.encode(address(_defaultTx.target), 18) + ); + + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp to after the finalization period + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + vm.expectRevert(BadTarget.selector); + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + } + /// @dev Tests that `finalizeWithdrawalTransaction` reverts if the contract is paused. function test_finalizeWithdrawalTransaction_paused_reverts() external { vm.prank(optimismPortal2.guardian()); @@ -1730,3 +2081,381 @@ contract OptimismPortal2_ResourceFuzz_Test is CommonTest { }); } } + +contract OptimismPortal2WithMockERC20_Test is OptimismPortal2_FinalizeWithdrawal_Test { + MockERC20 token; + + function setUp() public virtual override { + super.setUp(); + token = new MockERC20("Test", "TST", 18); + + // TODO(opcm upgrades): remove skip once upgrade path is implemented + skipIfForkTest("OptimismPortal2_Test: gas paying token functionality DNE on op mainnet"); + } + + function depositERC20Transaction( + address _from, + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + internal + { + if (_isCreation) { + _to = address(0); + } + if (_data.length > 120_000) { + _data = _data[0:120_000]; + } + IResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig(); + _gasLimit = + uint64(bound(_gasLimit, optimismPortal2.minimumGasLimit(uint64(_data.length)), rcfg.maxResourceLimit)); + + // Mint the token to the contract and approve the token for the portal + token.mint(address(this), _mint); + token.approve(address(optimismPortal2), _mint); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + bytes memory opaqueData = abi.encodePacked(_mint, _value, _gasLimit, _isCreation, _data); + + vm.expectEmit(address(optimismPortal2)); + emit TransactionDeposited( + _from, // from + _to, + uint256(0), // DEPOSIT_VERSION + opaqueData + ); + + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(_to, _mint, _value, _gasLimit, _isCreation, _data); + + // Assert final balance equals the deposited amount + assertEq(token.balanceOf(address(optimismPortal2)), _mint); + assertEq(optimismPortal2.balance(), _mint); + } + + /// @dev Tests that `depositERC20Transaction` succeeds when msg.sender == tx.origin. + function testFuzz_depositERC20Transaction_senderIsOrigin_succeeds( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // Ensure that msg.sender == tx.origin + vm.startPrank(address(this), address(this)); + + depositERC20Transaction({ + _from: address(this), + _to: _to, + _mint: _mint, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + + /// @dev Tests that `depositERC20Transaction` succeeds when msg.sender != tx.origin. + function testFuzz_depositERC20Transaction_senderNotOrigin_succeeds( + address _to, + uint256 _mint, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // Ensure that msg.sender != tx.origin + vm.startPrank(address(this), address(1)); + + depositERC20Transaction({ + _from: AddressAliasHelper.applyL1ToL2Alias(address(this)), + _to: _to, + _mint: _mint, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + + /// @dev Tests that `depositERC20Transaction` reverts when not enough of the token is approved. + function test_depositERC20Transaction_notEnoughAmount_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + vm.expectRevert(stdError.arithmeticError); + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(0), 1, 0, 0, false, ""); + } + + /// @dev Tests that `depositERC20Transaction` reverts when token balance does not update correctly after transfer. + function test_depositERC20Transaction_incorrectTokenBalance_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mint the token to the contract and approve the token for the portal + token.mint(address(this), 100); + token.approve(address(optimismPortal2), 100); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + // Mock the token balance + vm.mockCall(address(token), abi.encodeCall(token.balanceOf, (address(optimismPortal2))), abi.encode(0)); + + // Call minimumGasLimit(0) before vm.expectRevert to ensure vm.expectRevert is for depositERC20Transaction + uint64 gasLimit = optimismPortal2.minimumGasLimit(0); + + vm.expectRevert(TransferFailed.selector); + + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(1), 100, 0, gasLimit, false, ""); + } + + /// @dev Tests that `depositERC20Transaction` reverts when creating a contract with a non-zero target. + function test_depositERC20Transaction_isCreationNotZeroTarget_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + // Call minimumGasLimit(0) before vm.expectRevert to ensure vm.expectRevert is for depositERC20Transaction + uint64 gasLimit = optimismPortal2.minimumGasLimit(0); + + vm.expectRevert(BadTarget.selector); + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(1), 0, 0, gasLimit, true, ""); + } + + /// @dev Tests that `depositERC20Transaction` reverts when the gas limit is too low. + function test_depositERC20Transaction_gasLimitTooLow_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + vm.expectRevert(SmallGasLimit.selector); + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(0), 0, 0, 0, false, ""); + } + + /// @dev Tests that `depositERC20Transaction` reverts when the data is too large. + function test_depositERC20Transaction_dataTooLarge_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + bytes memory data = new bytes(120_001); + data[120_000] = 0x01; + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + uint64 gasLimit = optimismPortal2.minimumGasLimit(120_001); + vm.expectRevert(LargeCalldata.selector); + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(0), 0, 0, gasLimit, false, data); + } + + /// @dev Tests that `balance()` returns the correct balance when the gas paying token is not ether. + function testFuzz_balance_nonEther_succeeds(uint256 _amount) external { + // vm.skip(true, "Custom gas token not supported"); + + // Mint the token to the contract and approve the token for the portal + token.mint(address(this), _amount); + token.approve(address(optimismPortal2), _amount); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction(address(0), _amount, 0, optimismPortal2.minimumGasLimit(0), false, ""); + + // Check that the balance has been correctly updated + assertEq(optimismPortal2.balance(), _amount); + } + + /// @dev Tests that `finalizeWithdrawalTransaction` succeeds. + function test_finalizeWithdrawalTransaction_provenWithdrawalHashWithNonEther_succeeds() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mint the token to the contract and approve the token for the portal + token.mint(address(this), _defaultTx.value); + token.approve(address(optimismPortal2), _defaultTx.value); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + // Deposit the token into the portal + optimismPortal2.depositERC20Transaction( + address(bob), _defaultTx.value, 0, optimismPortal2.minimumGasLimit(0), false, "" + ); + + assertEq(optimismPortal2.balance(), _defaultTx.value); + + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalProven(_withdrawalHash, alice, bob); + optimismPortal2.proveWithdrawalTransaction({ + _tx: _defaultTx, + _disputeGameIndex: _proposedGameIndex, + _outputRootProof: _outputRootProof, + _withdrawalProof: _withdrawalProof + }); + + // Warp past the finalization period. + game.resolveClaim(0, 0); + game.resolve(); + vm.warp(block.timestamp + optimismPortal2.proofMaturityDelaySeconds() + 1); + + vm.expectEmit(address(optimismPortal2)); + emit WithdrawalFinalized(_withdrawalHash, true); + + vm.expectCall(_defaultTx.target, 0, _defaultTx.data); + + vm.expectCall(address(token), 0, abi.encodeCall(token.transfer, (_defaultTx.target, _defaultTx.value))); + + optimismPortal2.finalizeWithdrawalTransaction(_defaultTx); + + assertEq(optimismPortal2.balance(), 0); + assertEq(token.balanceOf(address(bob)), 100); + } + + /// @dev Helper for depositing a transaction. + function depositTransaction( + address _from, + address _to, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + internal + { + if (_isCreation) { + _to = address(0); + } + if (_data.length > 120_000) { + _data = _data[0:120_000]; + } + + IResourceMetering.ResourceConfig memory rcfg = systemConfig.resourceConfig(); + _gasLimit = + uint64(bound(_gasLimit, optimismPortal2.minimumGasLimit(uint64(_data.length)), rcfg.maxResourceLimit)); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + bytes memory opaqueData = abi.encodePacked(uint256(0), _value, _gasLimit, _isCreation, _data); + + vm.expectEmit(address(optimismPortal2)); + emit TransactionDeposited( + _from, // from + _to, + uint256(0), // DEPOSIT_VERSION + opaqueData + ); + + // Deposit the token into the portal + optimismPortal2.depositTransaction(_to, _value, _gasLimit, _isCreation, _data); + + // Assert final balance equals the deposited amount + assertEq(token.balanceOf(address(optimismPortal2)), 0); + assertEq(optimismPortal2.balance(), 0); + } + + /// @dev Tests that `depositTransaction` succeeds when a custom gas token is used but the msg.value is zero. + function testFuzz_depositTransaction_customGasTokenWithNoValueAndSenderIsOrigin_succeeds( + address _to, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // Ensure that msg.sender == tx.origin + vm.startPrank(address(this), address(this)); + + depositTransaction({ + _from: address(this), + _to: _to, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + + /// @dev Tests that `depositTransaction` succeeds when a custom gas token is used but the msg.value is zero. + function testFuzz_depositTransaction_customGasTokenWithNoValueAndSenderNotOrigin_succeeds( + address _to, + uint256 _value, + uint64 _gasLimit, + bool _isCreation, + bytes calldata _data + ) + external + { + // vm.skip(true, "Custom gas token not supported"); + + // Ensure that msg.sender != tx.origin + vm.startPrank(address(this), address(1)); + + depositTransaction({ + _from: AddressAliasHelper.applyL1ToL2Alias(address(this)), + _to: _to, + _value: _value, + _gasLimit: _gasLimit, + _isCreation: _isCreation, + _data: _data + }); + } + + /// @dev Tests that `depositTransaction` fails when a custom gas token is used and msg.value is non-zero. + function test_depositTransaction_customGasTokenWithValue_reverts() external { + // vm.skip(true, "Custom gas token not supported"); + + // Mock the gas paying token to be the ERC20 token + vm.mockCall( + address(systemConfig), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(token), 18) + ); + + vm.expectRevert(NoValue.selector); + + // Deposit the token into the portal + optimismPortal2.depositTransaction{ value: 100 }(address(0), 0, 0, false, ""); + } +} diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index df9100ce8817f..1e395b3279c4f 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -21,10 +21,27 @@ contract OptimismPortalInterop_Test is CommonTest { super.setUp(); } - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(_optimismPortalInterop().version()).length > 0); + /// @dev Tests that the config for the gas paying token can be set. + function testFuzz_setConfig_gasPayingToken_succeeds(bytes calldata _value) public { + vm.expectEmit(address(optimismPortal2)); + emitTransactionDeposited({ + _from: Constants.DEPOSITOR_ACCOUNT, + _to: Predeploys.L1_BLOCK_ATTRIBUTES, + _value: 0, + _mint: 0, + _gasLimit: 200_000, + _isCreation: false, + _data: abi.encodeCall(IL1BlockInterop.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value)) + }); + + vm.prank(address(_optimismPortalInterop().systemConfig())); + _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); + } + + /// @dev Tests that setting the gas paying token config as not the system config reverts. + function testFuzz_setConfig_gasPayingTokenButNotSystemConfig_reverts(bytes calldata _value) public { + vm.expectRevert(Unauthorized.selector); + _optimismPortalInterop().setConfig(ConfigType.SET_GAS_PAYING_TOKEN, _value); } /// @dev Tests that the config for adding a dependency can be set. diff --git a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol index fbae6b41524b3..44d4b7547aa8e 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfig.t.sol @@ -4,13 +4,19 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +// Contracts +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + // Libraries import { Constants } from "src/libraries/Constants.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces import { IResourceMetering } from "interfaces/L1/IResourceMetering.sol"; import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; +import { IL1Block } from "interfaces/L2/IL1Block.sol"; contract SystemConfig_Init is CommonTest { event ConfigUpdate(uint256 indexed version, ISystemConfig.UpdateType indexed updateType, bytes data); @@ -41,12 +47,6 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { optimismMintableERC20Factory = artifacts.mustGetAddress("OptimismMintableERC20FactoryProxy"); } - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(systemConfig.version()).length > 0); - } - /// @dev Tests that constructor sets the correct values. function test_constructor_succeeds() external view { ISystemConfig impl = ISystemConfig(systemConfigImpl); @@ -74,6 +74,10 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(address(impl.disputeGameFactory()), address(0)); assertEq(address(impl.optimismPortal()), address(0)); assertEq(address(impl.optimismMintableERC20Factory()), address(0)); + // Check gas paying token + (address token, uint8 decimals) = impl.gasPayingToken(); + assertEq(token, Constants.ETHER); + assertEq(decimals, 18); } /// @dev Tests that initialization sets the correct values. @@ -114,6 +118,10 @@ contract SystemConfig_Initialize_Test is SystemConfig_Init { assertEq(addrs.optimismPortal, address(optimismPortal2)); assertEq(address(systemConfig.optimismMintableERC20Factory()), address(optimismMintableERC20Factory)); assertEq(addrs.optimismMintableERC20Factory, address(optimismMintableERC20Factory)); + // Check gas paying token + (address token, uint8 decimals) = systemConfig.gasPayingToken(); + assertEq(token, Constants.ETHER); + assertEq(decimals, 18); } } @@ -144,7 +152,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) }); } @@ -173,7 +182,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) }); assertEq(systemConfig.startBlock(), block.number); @@ -203,7 +213,8 @@ contract SystemConfig_Initialize_TestFail is SystemConfig_Initialize_Test { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) }); assertEq(systemConfig.startBlock(), 1); @@ -317,12 +328,193 @@ contract SystemConfig_Init_ResourceConfig is SystemConfig_Init { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: address(0) }) }); } } +contract SystemConfig_Init_CustomGasToken is SystemConfig_Init { + ERC20 token; + + function setUp() public override { + // vm.skip(true, "Custom gas token not supported"); + + token = new ERC20("Silly", "SIL"); + super.enableCustomGasToken(address(token)); + + super.setUp(); + } + + /// @dev Helper to clean storage and then initialize the system config with an arbitrary gas token address. + function cleanStorageAndInit(address _gasPayingToken) internal { + vm.store(address(systemConfig), bytes32(0), bytes32(0)); // initailizer + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_SLOT, bytes32(0)); + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_NAME_SLOT, bytes32(0)); + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_SYMBOL_SLOT, bytes32(0)); + + systemConfig.initialize({ + _owner: alice, + _basefeeScalar: 2100, + _blobbasefeeScalar: 1000000, + _batcherHash: bytes32(hex"abcd"), + _gasLimit: 30_000_000, + _unsafeBlockSigner: address(1), + _config: Constants.DEFAULT_RESOURCE_CONFIG(), + _batchInbox: address(0), + _addresses: ISystemConfig.Addresses({ + l1CrossDomainMessenger: address(0), + l1ERC721Bridge: address(0), + disputeGameFactory: address(0), + l1StandardBridge: address(0), + optimismPortal: address(optimismPortal2), + optimismMintableERC20Factory: address(0), + gasPayingToken: _gasPayingToken + }) + }); + } + + /// @dev Tests that initialization sets the correct values and getters work. + function test_initialize_customGasToken_succeeds() external view { + (address addr, uint8 decimals) = systemConfig.gasPayingToken(); + assertEq(addr, address(token)); + assertEq(decimals, 18); + + assertEq(systemConfig.gasPayingTokenName(), token.name()); + assertEq(systemConfig.gasPayingTokenSymbol(), token.symbol()); + } + + /// @dev Tests that initialization sets the correct values and getters work. + function testFuzz_initialize_customGasToken_succeeds( + address _token, + string calldata _name, + string calldata _symbol + ) + external + { + // don't use vm's address + vm.assume(_token != address(vm)); + // don't use console's address + vm.assume(_token != CONSOLE); + // don't use create2 deployer's address + vm.assume(_token != CREATE2_FACTORY); + // don't use default test's address + vm.assume(_token != DEFAULT_TEST_CONTRACT); + // don't use multicall3's address + vm.assume(_token != MULTICALL3_ADDRESS); + + // Using vm.assume() would cause too many test rejections. + string memory name = _name; + if (bytes(_name).length > 32) { + name = _name[:32]; + } + + // Using vm.assume() would cause too many test rejections. + string memory symbol = _symbol; + if (bytes(_symbol).length > 32) { + symbol = _symbol[:32]; + } + + vm.mockCall(_token, abi.encodeCall(token.decimals, ()), abi.encode(18)); + vm.mockCall(_token, abi.encodeCall(token.name, ()), abi.encode(name)); + vm.mockCall(_token, abi.encodeCall(token.symbol, ()), abi.encode(symbol)); + + cleanStorageAndInit(_token); + + (address addr, uint8 decimals) = systemConfig.gasPayingToken(); + assertEq(decimals, 18); + + if (_token == address(0) || _token == Constants.ETHER) { + assertEq(addr, Constants.ETHER); + assertEq(systemConfig.gasPayingTokenName(), "Ether"); + assertEq(systemConfig.gasPayingTokenSymbol(), "ETH"); + } else { + assertEq(addr, _token); + assertEq(systemConfig.gasPayingTokenName(), name); + assertEq(systemConfig.gasPayingTokenSymbol(), symbol); + } + } + + /// @dev Tests that initialization sets the correct values and getters work when token address passed is 0. + function test_initialize_customGasTokenWithZeroTokenAddress_succeeds() external { + cleanStorageAndInit(address(0)); + + (address addr, uint8 decimals) = systemConfig.gasPayingToken(); + assertEq(addr, address(Constants.ETHER)); + assertEq(decimals, 18); + + assertEq(systemConfig.gasPayingTokenName(), "Ether"); + assertEq(systemConfig.gasPayingTokenSymbol(), "ETH"); + } + + /// @dev Tests that initialization sets the correct values and getters work when token address is Constants.ETHER + function test_initialize_customGasTokenWithEtherTokenAddress_succeeds() external { + cleanStorageAndInit(Constants.ETHER); + + (address addr, uint8 decimals) = systemConfig.gasPayingToken(); + assertEq(addr, address(Constants.ETHER)); + assertEq(decimals, 18); + + assertEq(systemConfig.gasPayingTokenName(), "Ether"); + assertEq(systemConfig.gasPayingTokenSymbol(), "ETH"); + } + + /// @dev Tests that initialization fails if decimals are not 18. + function test_initialize_customGasTokenWrongDecimals_fails() external { + vm.mockCall(address(token), abi.encodeCall(token.decimals, ()), abi.encode(8)); + vm.expectRevert("SystemConfig: bad decimals of gas paying token"); + + cleanStorageAndInit(address(token)); + } + + /// @dev Tests that initialization fails if name is too long. + function test_initialize_customGasTokenNameTooLong_fails() external { + string memory name = new string(32); + name = string.concat(name, "a"); + + vm.mockCall(address(token), abi.encodeCall(token.name, ()), abi.encode(name)); + vm.expectRevert("GasPayingToken: string cannot be greater than 32 bytes"); + + cleanStorageAndInit(address(token)); + } + + /// @dev Tests that initialization fails if symbol is too long. + function test_initialize_customGasTokenSymbolTooLong_fails() external { + string memory symbol = new string(33); + symbol = string.concat(symbol, "a"); + + vm.mockCall(address(token), abi.encodeCall(token.symbol, ()), abi.encode(symbol)); + vm.expectRevert("GasPayingToken: string cannot be greater than 32 bytes"); + + cleanStorageAndInit(address(token)); + } + + /// @dev Tests that initialization works with OptimismPortal. + function test_initialize_customGasTokenCall_succeeds() external { + vm.expectCall( + address(optimismPortal2), + abi.encodeCall(optimismPortal2.setGasPayingToken, (address(token), 18, bytes32("Silly"), bytes32("SIL"))) + ); + + vm.expectEmit(address(optimismPortal2)); + emit TransactionDeposited( + 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001, + Predeploys.L1_BLOCK_ATTRIBUTES, + 0, // deposit version + abi.encodePacked( + uint256(0), // mint + uint256(0), // value + uint64(200_000), // gasLimit + false, // isCreation, + abi.encodeCall(IL1Block.setGasPayingToken, (address(token), 18, bytes32("Silly"), bytes32("SIL"))) + ) + ); + + cleanStorageAndInit(address(token)); + } +} + contract SystemConfig_Setters_TestFail is SystemConfig_Init { /// @dev Tests that `setBatcherHash` reverts if the caller is not the owner. function test_setBatcherHash_notOwner_reverts() external { diff --git a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol index 2f953ef7413f4..d37cab1d9b336 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol @@ -4,10 +4,16 @@ pragma solidity 0.8.15; // Testing import { CommonTest } from "test/setup/CommonTest.sol"; +// Contracts +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + // Libraries +import { Constants } from "src/libraries/Constants.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; +import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; // Interfaces +import { ISystemConfig } from "interfaces/L1/ISystemConfig.sol"; import { ISystemConfigInterop } from "interfaces/L1/ISystemConfigInterop.sol"; import { IOptimismPortalInterop } from "interfaces/L1/IOptimismPortalInterop.sol"; import { ConfigType } from "interfaces/L2/IL1BlockInterop.sol"; @@ -20,10 +26,110 @@ contract SystemConfigInterop_Test is CommonTest { super.setUp(); } - /// @notice Tests that the version function returns a valid string. We avoid testing the - /// specific value of the string as it changes frequently. - function test_version_succeeds() external view { - assert(bytes(_systemConfigInterop().version()).length > 0); + /// @dev Temporary test that checks that correct calls to initialize when using a custom gas token revert with the + /// expected error. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_initialize_customGasToken_reverts() external { + // vm.expectRevert(ISystemConfig.CustomGasTokenNotSupported.selector); + _cleanStorageAndInit(address(L1Token)); + } + + /// @dev Tests that when the decimals is not 18, initialization reverts. + function test_initialize_decimalsIsNot18_reverts(uint8 decimals) external { + // vm.skip(true, "Custom gas token not supported"); + + vm.assume(decimals != 18); + address _token = address(L1Token); + + vm.mockCall(_token, abi.encodeCall(ERC20.name, ()), abi.encode("Token")); + vm.mockCall(_token, abi.encodeCall(ERC20.symbol, ()), abi.encode("TKN")); + vm.mockCall(_token, abi.encodeCall(ERC20.decimals, ()), abi.encode(decimals)); + + vm.expectRevert("SystemConfig: bad decimals of gas paying token"); + _cleanStorageAndInit(_token); + } + + /// @dev Temporary test that checks that correct calls to setGasPayingToken when using a custom gas token revert + /// with the expected error. + /// @dev Should be removed when/if Custom Gas Token functionality is allowed again. + function test_setGasPayingToken_customGasToken_reverts( + address _token, + string calldata _name, + string calldata _symbol + ) + external + { + assumeNotForgeAddress(_token); + vm.assume(_token != address(0)); + vm.assume(_token != Constants.ETHER); + + // Using vm.assume() would cause too many test rejections. + string memory name = _name; + if (bytes(_name).length > 32) { + name = _name[:32]; + } + + // Using vm.assume() would cause too many test rejections. + string memory symbol = _symbol; + if (bytes(_symbol).length > 32) { + symbol = _symbol[:32]; + } + + vm.mockCall(_token, abi.encodeCall(ERC20.decimals, ()), abi.encode(18)); + vm.mockCall(_token, abi.encodeCall(ERC20.name, ()), abi.encode(name)); + vm.mockCall(_token, abi.encodeCall(ERC20.symbol, ()), abi.encode(symbol)); + + // vm.expectRevert(ISystemConfig.CustomGasTokenNotSupported.selector); + _cleanStorageAndInit(_token); + } + + /// @dev Tests that the gas paying token can be set. + function testFuzz_setGasPayingToken_succeeds( + address _token, + string calldata _name, + string calldata _symbol + ) + public + { + // vm.skip(true, "Custom gas token not supported"); + + assumeNotForgeAddress(_token); + vm.assume(_token != address(0)); + vm.assume(_token != Constants.ETHER); + + // Using vm.assume() would cause too many test rejections. + string memory name = _name; + if (bytes(_name).length > 32) { + name = _name[:32]; + } + + // Using vm.assume() would cause too many test rejections. + string memory symbol = _symbol; + if (bytes(_symbol).length > 32) { + symbol = _symbol[:32]; + } + + vm.mockCall(_token, abi.encodeCall(ERC20.decimals, ()), abi.encode(18)); + vm.mockCall(_token, abi.encodeCall(ERC20.name, ()), abi.encode(name)); + vm.mockCall(_token, abi.encodeCall(ERC20.symbol, ()), abi.encode(symbol)); + + vm.expectCall( + address(optimismPortal2), + abi.encodeCall( + IOptimismPortalInterop.setConfig, + ( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ + _token: _token, + _decimals: 18, + _name: GasPayingToken.sanitize(name), + _symbol: GasPayingToken.sanitize(symbol) + }) + ) + ) + ); + + _cleanStorageAndInit(_token); } /// @dev Tests that a dependency can be added. @@ -70,6 +176,35 @@ contract SystemConfigInterop_Test is CommonTest { _systemConfigInterop().removeDependency(_chainId); } + /// @dev Helper to clean storage and then initialize the system config with an arbitrary gas token address. + function _cleanStorageAndInit(address _token) internal { + // Wipe out the initialized slot so the proxy can be initialized again + vm.store(address(systemConfig), bytes32(0), bytes32(0)); + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_SLOT, bytes32(0)); + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_NAME_SLOT, bytes32(0)); + vm.store(address(systemConfig), GasPayingToken.GAS_PAYING_TOKEN_SYMBOL_SLOT, bytes32(0)); + + systemConfig.initialize({ + _owner: alice, + _basefeeScalar: 2100, + _blobbasefeeScalar: 1000000, + _batcherHash: bytes32(hex"abcd"), + _gasLimit: 30_000_000, + _unsafeBlockSigner: address(1), + _config: Constants.DEFAULT_RESOURCE_CONFIG(), + _batchInbox: address(0), + _addresses: ISystemConfig.Addresses({ + l1CrossDomainMessenger: address(0), + l1ERC721Bridge: address(0), + disputeGameFactory: address(0), + l1StandardBridge: address(0), + optimismPortal: address(optimismPortal2), + optimismMintableERC20Factory: address(0), + gasPayingToken: _token + }) + }); + } + /// @dev Returns the SystemConfigInterop instance. function _systemConfigInterop() internal view returns (ISystemConfigInterop) { return ISystemConfigInterop(address(systemConfig)); diff --git a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol index bae3601b5105e..9814e776b2e5e 100644 --- a/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol +++ b/packages/contracts-bedrock/test/L2/ETHLiquidity.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.15; import { CommonTest } from "test/setup/CommonTest.sol"; // Error imports -import { Unauthorized } from "src/libraries/errors/CommonErrors.sol"; +import { Unauthorized, NotCustomGasToken } from "src/libraries/errors/CommonErrors.sol"; /// @title ETHLiquidity_Test /// @notice Contract for testing the ETHLiquidity contract. @@ -73,6 +73,26 @@ contract ETHLiquidity_Test is CommonTest { assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); } + /// @notice Tests that the burn function reverts when called on a custom gas token chain. + /// @param _amount Amount of ETH (in wei) to call the burn function with. + function testFuzz_burn_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(address(superchainWeth), _amount); + vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(address(superchainWeth)); + vm.expectRevert(NotCustomGasToken.selector); + ethLiquidity.burn{ value: _amount }(); + + // Assert + assertEq(address(superchainWeth).balance, _amount); + assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); + } + /// @notice Tests that the mint function fails when the amount requested is greater than the /// available balance. In practice this should never happen because the starting /// balance is expected to be uint248 wei, the total ETH supply is far less than that @@ -137,4 +157,24 @@ contract ETHLiquidity_Test is CommonTest { assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0); } + + /// @notice Tests that the mint function reverts when called on a custom gas token chain. + /// @param _amount Amount of ETH (in wei) to call the mint function with. + function testFuzz_mint_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.mockCall(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(address(superchainWeth)); + vm.expectRevert(NotCustomGasToken.selector); + ethLiquidity.mint(_amount); + + // Assert + assertEq(address(superchainWeth).balance, 0); + assertEq(address(ethLiquidity).balance, STARTING_LIQUIDITY_BALANCE); + assertEq(superchainWeth.balanceOf(address(ethLiquidity)), 0); + } } diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index 2d320cca053ba..245331da4c2a7 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -12,29 +12,13 @@ import "src/libraries/L1BlockErrors.sol"; contract L1BlockTest is CommonTest { address depositor; + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + /// @dev Sets up the test suite. function setUp() public virtual override { super.setUp(); depositor = l1Block.DEPOSITOR_ACCOUNT(); } - - function test_isCustomGasToken_succeeds() external view { - assertFalse(l1Block.isCustomGasToken()); - } - - function test_gasPayingToken_succeeds() external view { - (address token, uint8 decimals) = l1Block.gasPayingToken(); - assertEq(token, Constants.ETHER); - assertEq(uint256(decimals), uint256(18)); - } - - function test_gasPayingTokenName_succeeds() external view { - assertEq("Ether", l1Block.gasPayingTokenName()); - } - - function test_gasPayingTokenSymbol_succeeds() external view { - assertEq("ETH", l1Block.gasPayingTokenSymbol()); - } } contract L1BlockBedrock_Test is L1BlockTest { @@ -236,3 +220,50 @@ contract L1BlockEcotone_Test is L1BlockTest { } } } + +contract L1BlockCustomGasToken_Test is L1BlockTest { + function testFuzz_setGasPayingToken_succeeds( + address _token, + uint8 _decimals, + string calldata _name, + string calldata _symbol + ) + external + { + vm.assume(_token != address(0)); + vm.assume(_token != Constants.ETHER); + + // Using vm.assume() would cause too many test rejections. + string memory name = _name; + if (bytes(_name).length > 32) { + name = _name[:32]; + } + bytes32 b32name = bytes32(abi.encodePacked(name)); + + // Using vm.assume() would cause too many test rejections. + string memory symbol = _symbol; + if (bytes(_symbol).length > 32) { + symbol = _symbol[:32]; + } + bytes32 b32symbol = bytes32(abi.encodePacked(symbol)); + + vm.expectEmit(address(l1Block)); + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: b32name, symbol: b32symbol }); + + vm.prank(depositor); + l1Block.setGasPayingToken({ _token: _token, _decimals: _decimals, _name: b32name, _symbol: b32symbol }); + + (address token, uint8 decimals) = l1Block.gasPayingToken(); + assertEq(token, _token); + assertEq(decimals, _decimals); + + assertEq(name, l1Block.gasPayingTokenName()); + assertEq(symbol, l1Block.gasPayingTokenSymbol()); + assertTrue(l1Block.isCustomGasToken()); + } + + function test_setGasPayingToken_isDepositor_reverts() external { + vm.expectRevert(NotDepositor.selector); + l1Block.setGasPayingToken(address(this), 18, "Test", "TST"); + } +} diff --git a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol index c569f42025f36..40dfa459e16d8 100644 --- a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol @@ -80,6 +80,45 @@ contract L1BlockInteropTest is CommonTest { assertEq(_l1BlockInterop().dependencySetSize(), 0); } + /// @dev Tests that the config for setting the gas paying token succeeds. + function testFuzz_setConfig_gasPayingToken_succeeds( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + public + prankDepositor + { + vm.assume(_token != address(vm)); + + vm.expectEmit(address(l1Block)); + emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: _name, symbol: _symbol }); + + _l1BlockInterop().setConfig( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) + ); + } + + /// @dev Tests that setting the gas paying token config as not the depositor reverts. + function testFuzz_setConfig_gasPayingTokenButNotDepositor_reverts( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + public + { + vm.assume(_token != address(vm)); + + vm.expectRevert(NotDepositor.selector); + _l1BlockInterop().setConfig( + ConfigType.SET_GAS_PAYING_TOKEN, + StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) + ); + } + /// @dev Tests that the config for adding a dependency can be set. function testFuzz_setConfig_addDependency_succeeds(uint256 _chainId) public prankDepositor { vm.assume(_chainId != block.chainid); diff --git a/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol b/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol index 470c6d82d8599..bb673d7304cc8 100644 --- a/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol +++ b/packages/contracts-bedrock/test/L2/L2CrossDomainMessenger.t.sol @@ -311,4 +311,105 @@ contract L2CrossDomainMessenger_Test is CommonTest { assertEq(l2CrossDomainMessenger.successfulMessages(hash), true); assertEq(l2CrossDomainMessenger.failedMessages(hash), true); } + + /// @dev Tests that sendMessage succeeds with a custom gas token when the call value is zero. + function test_sendMessage_customGasTokenButNoValue_succeeds() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + bytes memory xDomainCallData = + Encoding.encodeCrossDomainMessage(l2CrossDomainMessenger.messageNonce(), alice, recipient, 0, 100, hex"ff"); + vm.expectCall( + address(l2ToL1MessagePasser), + abi.encodeCall( + IL2ToL1MessagePasser.initiateWithdrawal, + (address(l1CrossDomainMessenger), l2CrossDomainMessenger.baseGas(hex"ff", 100), xDomainCallData) + ) + ); + + // MessagePassed event + vm.expectEmit(true, true, true, true); + emit MessagePassed( + l2ToL1MessagePasser.messageNonce(), + address(l2CrossDomainMessenger), + address(l1CrossDomainMessenger), + 0, + l2CrossDomainMessenger.baseGas(hex"ff", 100), + xDomainCallData, + Hashing.hashWithdrawal( + Types.WithdrawalTransaction({ + nonce: l2ToL1MessagePasser.messageNonce(), + sender: address(l2CrossDomainMessenger), + target: address(l1CrossDomainMessenger), + value: 0, + gasLimit: l2CrossDomainMessenger.baseGas(hex"ff", 100), + data: xDomainCallData + }) + ) + ); + + vm.prank(alice); + l2CrossDomainMessenger.sendMessage(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that the sendMessage reverts when call value is non-zero with custom gas token. + function test_sendMessage_customGasTokenWithValue_reverts() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + vm.expectRevert("CrossDomainMessenger: cannot send value with custom gas token"); + l2CrossDomainMessenger.sendMessage{ value: 1 }(recipient, hex"ff", uint32(100)); + } + + /// @dev Tests that the relayMessage succeeds with a custom gas token when the call value is zero. + function test_relayMessage_customGasTokenAndNoValue_succeeds() external { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + + address target = address(0xabcd); + address sender = address(l1CrossDomainMessenger); + address caller = AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)); + + vm.expectCall(target, hex"1111"); + + vm.prank(caller); + + vm.expectEmit(true, true, true, true); + + bytes32 hash = + Hashing.hashCrossDomainMessage(Encoding.encodeVersionedNonce(0, 1), sender, target, 0, 0, hex"1111"); + + emit RelayedMessage(hash); + + l2CrossDomainMessenger.relayMessage( + Encoding.encodeVersionedNonce(0, 1), // nonce + sender, + target, + 0, // value + 0, + hex"1111" + ); + + // the message hash is in the successfulMessages mapping + assert(l2CrossDomainMessenger.successfulMessages(hash)); + // it is not in the received messages mapping + assertEq(l2CrossDomainMessenger.failedMessages(hash), false); + } + + /// @dev Tests that the relayMessage reverts when call value is non-zero with custom gas token. + /// The L1CrossDomainMessenger `sendMessage` function cannot send value with a custom gas token. + function test_relayMessage_customGasTokenWithValue_reverts() external virtual { + // Mock the gasPayingToken function to return a custom gas token + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("CrossDomainMessenger: value must be zero unless message is from a system address"); + + l2CrossDomainMessenger.relayMessage{ value: 1 }( + Encoding.encodeVersionedNonce({ _nonce: 0, _version: 1 }), + address(0xabcd), + address(0xabcd), + 1, // value + 0, + hex"1111" + ); + } } diff --git a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol index 48d0f978d359b..43a22f6a95f5a 100644 --- a/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/L2/L2StandardBridge.t.sol @@ -127,6 +127,19 @@ contract L2StandardBridge_Test is CommonTest { assertEq(address(l2ToL1MessagePasser).balance, 100); } + /// @dev Tests that the receive function reverts with custom gas token. + function testFuzz_receive_customGasToken_reverts(uint256 _value) external { + vm.prank(alice, alice); + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.deal(alice, _value); + (bool success, bytes memory data) = address(l2StandardBridge).call{ value: _value }(hex""); + assertFalse(success); + assembly { + data := add(data, 0x04) + } + assertEq(abi.decode(data, (string)), "StandardBridge: cannot bridge ETH with custom gas token"); + } + /// @dev Tests that `withdraw` reverts if the amount is not equal to the value sent. function test_withdraw_insufficientValue_reverts() external { assertEq(address(l2ToL1MessagePasser).balance, 0); @@ -154,6 +167,90 @@ contract L2StandardBridge_Test is CommonTest { l2StandardBridge.withdrawTo{ value: 100 }(address(L2Token), alice, 100, 1, hex""); } + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdraw_customGasToken_reverts() external { + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw(address(Predeploys.LEGACY_ERC20_ETH), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdrawERC20_customGasToken_reverts() external { + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw(address(L1Token), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` reverts with custom gas token. + function test_withdrawERC20WithValue_customGasToken_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw{ value: 1 ether }(address(L1Token), 1, 1, hex""); + } + + /// @dev Tests that `withdraw` with value reverts with custom gas token. + function test_withdraw_customGasTokenWithValue_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdraw{ value: 1 ether }(address(Predeploys.LEGACY_ERC20_ETH), 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawTo_customGasToken_reverts() external { + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo(address(Predeploys.LEGACY_ERC20_ETH), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawToERC20_customGasToken_reverts() external { + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo(address(L2Token), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` reverts with custom gas token. + function test_withdrawToERC20WithValue_customGasToken_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo{ value: 1 ether }(address(L2Token), bob, 1, 1, hex""); + } + + /// @dev Tests that `withdrawTo` with value reverts with custom gas token. + function test_withdrawTo_customGasTokenWithValue_reverts() external { + vm.deal(alice, 1 ether); + vm.mockCall( + address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(18)) + ); + vm.expectRevert("L2StandardBridge: not supported with custom gas token"); + vm.prank(alice, alice); + l2StandardBridge.withdrawTo{ value: 1 ether }(address(Predeploys.LEGACY_ERC20_ETH), bob, 1, 1, hex""); + } + /// @dev Tests that the legacy `withdraw` interface on the L2StandardBridge /// successfully initiates a withdrawal. function test_withdraw_ether_succeeds() external { @@ -477,6 +574,15 @@ contract L2StandardBridge_Bridge_Test is CommonTest { l2StandardBridge.bridgeETH{ value: _value }(_minGasLimit, _extraData); } + /// @dev Tests that bridging reverts with custom gas token. + function test_bridgeETH_customGasToken_reverts() external { + vm.prank(alice, alice); + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l2StandardBridge.bridgeETH(50000, hex"dead"); + } + /// @dev Tests that bridging ETH to a different address succeeds. function testFuzz_bridgeETHTo_succeeds(uint256 _value, uint32 _minGasLimit, bytes calldata _extraData) external { uint256 nonce = l2CrossDomainMessenger.messageNonce(); @@ -513,6 +619,20 @@ contract L2StandardBridge_Bridge_Test is CommonTest { l2StandardBridge.bridgeETHTo{ value: _value }(bob, _minGasLimit, _extraData); } + + /// @dev Tests that bridging reverts with custom gas token. + function testFuzz_bridgeETHTo_customGasToken_reverts( + uint256 _value, + uint32 _minGasLimit, + bytes calldata _extraData + ) + external + { + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + vm.deal(address(this), _value); + l2StandardBridge.bridgeETHTo{ value: _value }(bob, _minGasLimit, _extraData); + } } contract L2StandardBridge_FinalizeBridgeETH_Test is CommonTest { @@ -535,6 +655,22 @@ contract L2StandardBridge_FinalizeBridgeETH_Test is CommonTest { l2StandardBridge.finalizeBridgeETH{ value: 100 }(alice, alice, 100, hex""); } + + /// @dev Tests that finalizing bridged reverts with custom gas token. + function test_finalizeBridgeETH_customGasToken_reverts() external { + address messenger = address(l2StandardBridge.messenger()); + vm.mockCall( + messenger, + abi.encodeCall(ICrossDomainMessenger.xDomainMessageSender, ()), + abi.encode(address(l2StandardBridge.OTHER_BRIDGE())) + ); + vm.deal(address(l2CrossDomainMessenger), 1); + vm.prank(address(l2CrossDomainMessenger)); + vm.mockCall(address(l1Block), abi.encodeCall(systemConfig.gasPayingToken, ()), abi.encode(address(1), uint8(2))); + vm.expectRevert("StandardBridge: cannot bridge ETH with custom gas token"); + + l2StandardBridge.finalizeBridgeETH(alice, alice, 1, hex""); + } } contract L2StandardBridge_FinalizeBridgeERC20_Test is CommonTest { diff --git a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol index e122e79794ae1..bc59c76c116fc 100644 --- a/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol +++ b/packages/contracts-bedrock/test/L2/SuperchainWETH.t.sol @@ -6,7 +6,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; // Libraries import { Predeploys } from "src/libraries/Predeploys.sol"; -import { Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; +import { NotCustomGasToken, Unauthorized, ZeroAddress } from "src/libraries/errors/CommonErrors.sol"; import { Preinstalls } from "src/libraries/Preinstalls.sol"; // Interfaces @@ -54,12 +54,13 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests that the deposit function can be called on a non-custom gas token chain. /// @param _amount The amount of WETH to send. - function testFuzz_deposit_succeeds(uint256 _amount) public { + function testFuzz_deposit_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); // Arrange vm.deal(alice, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -72,9 +73,29 @@ contract SuperchainWETH_Test is CommonTest { assertEq(superchainWeth.balanceOf(alice), _amount); } + /// @notice Tests that the deposit function reverts when called on a custom gas token chain. + /// @param _amount The amount of WETH to send. + function testFuzz_deposit_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(address(alice), _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(alice); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.deposit{ value: _amount }(); + + // Assert + assertEq(alice.balance, _amount); + assertEq(superchainWeth.balanceOf(alice), 0); + } + /// @notice Tests that the withdraw function can be called on a non-custom gas token chain. /// @param _amount The amount of WETH to send. - function testFuzz_withdraw_succeeds(uint256 _amount) public { + function testFuzz_withdraw_fromNonCustomGasTokenChain_succeeds(uint256 _amount) public { // Assume _amount = bound(_amount, 0, type(uint248).max - 1); @@ -82,6 +103,7 @@ contract SuperchainWETH_Test is CommonTest { vm.deal(alice, _amount); vm.prank(alice); superchainWeth.deposit{ value: _amount }(); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Act vm.expectEmit(address(superchainWeth)); @@ -94,6 +116,28 @@ contract SuperchainWETH_Test is CommonTest { assertEq(superchainWeth.balanceOf(alice), 0); } + /// @notice Tests that the withdraw function reverts when called on a custom gas token chain. + /// @param _amount The amount of WETH to send. + function testFuzz_withdraw_fromCustomGasTokenChain_fails(uint256 _amount) public { + // Assume + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(alice, _amount); + vm.prank(alice); + superchainWeth.deposit{ value: _amount }(); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Act + vm.prank(alice); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.withdraw(_amount); + + // Assert + assertEq(alice.balance, 0); + assertEq(superchainWeth.balanceOf(alice), _amount); + } + /// @notice Tests the `crosschainMint` function reverts when the caller is not the `SuperchainTokenBridge`. function testFuzz_crosschainMint_callerNotBridge_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the bridge @@ -108,7 +152,7 @@ contract SuperchainWETH_Test is CommonTest { } /// @notice Tests the `crosschainMint` with non custom gas token succeeds and emits the `CrosschainMint` event. - function testFuzz_crosschainMint_fromBridge_succeeds(address _to, uint256 _amount) public { + function testFuzz_crosschainMint_fromBridgeNonCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { // Ensure `_to` is not the zero address vm.assume(_to != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); @@ -125,6 +169,9 @@ contract SuperchainWETH_Test is CommonTest { vm.expectEmit(address(superchainWeth)); emit CrosschainMint(_to, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + // Expect the call to the `mint` function in the `ETHLiquidity` contract vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 1); @@ -138,6 +185,39 @@ contract SuperchainWETH_Test is CommonTest { assertEq(address(superchainWeth).balance, _amount); } + /// @notice Tests the `crosschainMint` with custom gas token succeeds and emits the `CrosschainMint` event. + function testFuzz_crosschainMint_fromBridgeCustomGasTokenChain_succeeds(address _to, uint256 _amount) public { + // Ensure `_to` is not the zero address + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Get the balance of `_to` before the mint to compare later on the assertions + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `CrosschainMint` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainMint(_to, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Expect to not call the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); + + // Call the `mint` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.totalSupply(), 0); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests the `crosschainBurn` function reverts when the caller is not the `SuperchainTokenBridge`. function testFuzz_crosschainBurn_callerNotBridge_reverts(address _caller, address _from, uint256 _amount) public { // Ensure the caller is not the bridge @@ -153,7 +233,7 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests the `crosschainBurn` with non custom gas token burns the amount and emits the `CrosschainBurn` /// event. - function testFuzz_crosschainBurn_succeeds(address _from, uint256 _amount) public { + function testFuzz_crosschainBurn_fromBridgeNonCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { // Ensure `_from` is not the zero address vm.assume(_from != ZERO_ADDRESS); _amount = bound(_amount, 0, type(uint248).max - 1); @@ -175,6 +255,9 @@ contract SuperchainWETH_Test is CommonTest { vm.expectEmit(address(superchainWeth)); emit CrosschainBurn(_from, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); + // Expect the call to the `burn` function in the `ETHLiquidity` contract vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 1); @@ -188,6 +271,45 @@ contract SuperchainWETH_Test is CommonTest { assertEq(address(superchainWeth).balance, 0); } + /// @notice Tests the `crosschainBurn` with custom gas token burns the amount and emits the `CrosschainBurn` + /// event. + function testFuzz_crosschainBurn_fromBridgeCustomGasTokenChain_succeeds(address _from, uint256 _amount) public { + // Ensure `_from` is not the zero address + vm.assume(_from != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Mock the `isCustomGasToken` function to return false + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Mint some tokens to `_from` so then they can be burned + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainMint(_from, _amount); + + // Get the total supply and balance of `_from` before the burn to compare later on the assertions + uint256 _totalSupplyBefore = superchainWeth.totalSupply(); + uint256 _fromBalanceBefore = superchainWeth.balanceOf(_from); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(_from, ZERO_ADDRESS, _amount); + + // Look for the emit of the `CrosschainBurn` event + vm.expectEmit(address(superchainWeth)); + emit CrosschainBurn(_from, _amount, Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + + // Expect to not call the `burn` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.burn, ()), 0); + + // Call the `burn` function with the bridge caller + vm.prank(Predeploys.SUPERCHAIN_TOKEN_BRIDGE); + superchainWeth.crosschainBurn(_from, _amount); + + // Check the total supply and balance of `_from` after the burn were updated correctly + assertEq(superchainWeth.balanceOf(_from), _fromBalanceBefore - _amount); + assertEq(superchainWeth.totalSupply(), _totalSupplyBefore); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests that the `crosschainBurn` function reverts when called with insufficient balance. function testFuzz_crosschainBurn_insufficientBalance_fails(address _from, uint256 _amount) public { // Assume @@ -250,7 +372,7 @@ contract SuperchainWETH_Test is CommonTest { } /// @notice Test that the burn function reverts to protect against accidentally changing the visibility. - function testFuzz_calling_burnFunction_reverts(address _caller, address _from, uint256 _amount) public { + function testFuzz_calling_burnFuunction_reverts(address _caller, address _from, uint256 _amount) public { // Arrange // nosemgrep: sol-style-use-abi-encodecall bytes memory _calldata = abi.encodeWithSignature("burn(address,uint256)", _from, _amount); @@ -372,7 +494,7 @@ contract SuperchainWETH_Test is CommonTest { /// @notice Tests the `sendETH` function burns the sender ETH, sends the message, and emits the `SendETH` /// event. - function testFuzz_sendETH_succeeds( + function testFuzz_sendETH_fromNonCustomGasTokenChain_succeeds( address _sender, address _to, uint256 _amount, @@ -389,6 +511,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); // Get the total balance of `_sender` before the send to compare later on the assertions uint256 _senderBalanceBefore = _sender.balance; @@ -419,6 +542,30 @@ contract SuperchainWETH_Test is CommonTest { assertEq(_sender.balance, _senderBalanceBefore - _amount); } + /// @notice Tests the `sendETH` function reverts when called on a custom gas token chain. + function testFuzz_sendETH_fromCustomGasTokenChain_fails( + address _sender, + address _to, + uint256 _amount, + uint256 _chainId + ) + external + { + // Assume + vm.assume(_sender != ZERO_ADDRESS); + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Arrange + vm.deal(_sender, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + + // Call the `sendETH` function + vm.prank(_sender); + vm.expectRevert(NotCustomGasToken.selector); + superchainWeth.sendETH{ value: _amount }(_to, _chainId); + } + /// @notice Tests the `relayETH` function reverts when the caller is not the L2ToL2CrossDomainMessenger. function testFuzz_relayETH_notMessenger_reverts(address _caller, address _to, uint256 _amount) public { // Ensure the caller is not the messenger @@ -459,6 +606,51 @@ contract SuperchainWETH_Test is CommonTest { superchainWeth.relayETH(_crossDomainMessageSender, _to, _amount); } + /// @notice Tests the `relayETH` function succeeds and sends SuperchainWETH to the recipient on a custom gas token + /// chain. + function testFuzz_relayETH_fromCustomGasTokenChain_succeeds( + address _from, + address _to, + uint256 _amount, + uint256 _source + ) + public + { + // Assume + vm.assume(_to != ZERO_ADDRESS); + _amount = bound(_amount, 0, type(uint248).max - 1); + + // Get the balance of `_to` before the mint to compare later on the assertions + uint256 _toBalanceBefore = superchainWeth.balanceOf(_to); + + // Look for the emit of the `Transfer` event + vm.expectEmit(address(superchainWeth)); + emit Transfer(ZERO_ADDRESS, _to, _amount); + + // Look for the emit of the `RelayETH` event + vm.expectEmit(address(superchainWeth)); + emit RelayETH(_from, _to, _amount, _source); + + // Arrange + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(true)); + _mockAndExpect( + Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, + abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), + abi.encode(address(superchainWeth), _source) + ); + // Expect to not call the `mint` function in the `ETHLiquidity` contract + vm.expectCall(Predeploys.ETH_LIQUIDITY, abi.encodeCall(IETHLiquidity.mint, (_amount)), 0); + + // Act + vm.prank(Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER); + superchainWeth.relayETH(_from, _to, _amount); + + // Check the total supply and balance of `_to` after the mint were updated correctly + assertEq(superchainWeth.balanceOf(_to), _toBalanceBefore + _amount); + assertEq(superchainWeth.totalSupply(), 0); + assertEq(address(superchainWeth).balance, 0); + } + /// @notice Tests the `relayETH` function relays the proper amount of ETH and emits the `RelayETH` event. function testFuzz_relayETH_succeeds(address _from, address _to, uint256 _amount, uint256 _source) public { // Assume @@ -469,6 +661,7 @@ contract SuperchainWETH_Test is CommonTest { // Arrange vm.deal(address(superchainWeth), _amount); vm.deal(Predeploys.ETH_LIQUIDITY, _amount); + _mockAndExpect(address(l1Block), abi.encodeCall(l1Block.isCustomGasToken, ()), abi.encode(false)); _mockAndExpect( Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER, abi.encodeCall(IL2ToL2CrossDomainMessenger.crossDomainMessageContext, ()), diff --git a/packages/contracts-bedrock/test/L2/WETH.t.sol b/packages/contracts-bedrock/test/L2/WETH.t.sol index 0a4985ca874ff..84bb138d74fba 100644 --- a/packages/contracts-bedrock/test/L2/WETH.t.sol +++ b/packages/contracts-bedrock/test/L2/WETH.t.sol @@ -26,11 +26,13 @@ contract WETH_Test is CommonTest { /// @dev Tests that the name function returns the correct value. function test_name_ether_succeeds() external view { + assertFalse(l1Block.isCustomGasToken()); assertEq("Wrapped Ether", weth.name()); } /// @dev Tests that the symbol function returns the correct value. function test_symbol_ether_succeeds() external view { + assertFalse(l1Block.isCustomGasToken()); assertEq("WETH", weth.symbol()); } } diff --git a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol index 4ca84d96b0209..68add058f60dd 100644 --- a/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol +++ b/packages/contracts-bedrock/test/invariants/SystemConfig.t.sol @@ -44,7 +44,8 @@ contract SystemConfig_GasLimitBoundaries_Invariant is Test { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) ) ) diff --git a/packages/contracts-bedrock/test/setup/CommonTest.sol b/packages/contracts-bedrock/test/setup/CommonTest.sol index 55f7abedb9738..d9612c7938a64 100644 --- a/packages/contracts-bedrock/test/setup/CommonTest.sol +++ b/packages/contracts-bedrock/test/setup/CommonTest.sol @@ -16,6 +16,7 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; // Libraries +import { Constants } from "src/libraries/Constants.sol"; import { console } from "forge-std/console.sol"; // Interfaces @@ -33,6 +34,7 @@ contract CommonTest is Test, Setup, Events { FFIInterface constant ffi = FFIInterface(address(uint160(uint256(keccak256(abi.encode("optimism.ffi")))))); bool useAltDAOverride; + address customGasToken; bool useInteropOverride; bool useSoulGasToken; bool isSoulBackedByNative; @@ -65,6 +67,9 @@ contract CommonTest is Test, Setup, Events { if (useAltDAOverride) { deploy.cfg().setUseAltDA(true); } + if (customGasToken != address(0)) { + deploy.cfg().setUseCustomGasToken(customGasToken); + } if (useInteropOverride) { deploy.cfg().setUseInterop(true); } @@ -82,7 +87,7 @@ contract CommonTest is Test, Setup, Events { if (isForkTest()) { // Skip any test suite which uses a nonstandard configuration. - if (useAltDAOverride || useInteropOverride) { + if (useAltDAOverride || customGasToken != address(0) || useInteropOverride) { vm.skip(true); } } else { @@ -199,6 +204,13 @@ contract CommonTest is Test, Setup, Events { useAltDAOverride = true; } + /// @dev Sets a custom gas token for testing. Cannot be ETH. + function enableCustomGasToken(address _token) public { + _checkNotDeployed("custom gas token"); + require(_token != Constants.ETHER, "CommonTest: Cannot set gas token to ETHER"); + customGasToken = _token; + } + /// @dev Enables interoperability mode for testing function enableInterop() public { _checkNotDeployed("interop"); diff --git a/packages/contracts-bedrock/test/setup/DeployVariations.t.sol b/packages/contracts-bedrock/test/setup/DeployVariations.t.sol index 6e08a10b1968d..c0a6293b25f08 100644 --- a/packages/contracts-bedrock/test/setup/DeployVariations.t.sol +++ b/packages/contracts-bedrock/test/setup/DeployVariations.t.sol @@ -3,6 +3,9 @@ pragma solidity 0.8.15; // Testing utilities import { CommonTest } from "test/setup/CommonTest.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +error CustomGasTokenNotSupported(); contract DeployVariations_Test is CommonTest { function setUp() public override { @@ -11,22 +14,34 @@ contract DeployVariations_Test is CommonTest { } // Enable features which should be possible to enable or disable regardless of other options. - function enableAddOns(bool _enableAltDa) public { + function enableAddOns(bool _enableCGT, bool _enableAltDa) public { + if (_enableCGT) { + if (true) revert CustomGasTokenNotSupported(); + + ERC20 token = new ERC20("Silly", "SIL"); + super.enableCustomGasToken(address(token)); + } if (_enableAltDa) { super.enableAltDA(); } } - /// @dev It should be possible to enable Fault Proofs with Alt-DA. - function testFuzz_enableFaultProofs_succeeds(bool _enableAltDa) public virtual { - enableAddOns(_enableAltDa); + /// @dev It should be possible to enable Fault Proofs with any mix of CGT and Alt-DA. + function testFuzz_enableFaultProofs_succeeds(bool _enableCGT, bool _enableAltDa) public virtual { + // We don't support CGT yet, so we need to set it to false + _enableCGT = false; + + enableAddOns(_enableCGT, _enableAltDa); super.setUp(); } - /// @dev It should be possible to enable Fault Proofs and Interop with Alt-DA. - function test_enableInteropAndFaultProofs_succeeds(bool _enableAltDa) public virtual { - enableAddOns(_enableAltDa); + /// @dev It should be possible to enable Fault Proofs and Interop with any mix of CGT and Alt-DA. + function test_enableInteropAndFaultProofs_succeeds(bool _enableCGT, bool _enableAltDa) public virtual { + // We don't support CGT yet, so we need to set it to false + _enableCGT = false; + + enableAddOns(_enableCGT, _enableAltDa); super.enableInterop(); super.setUp(); diff --git a/packages/contracts-bedrock/test/universal/Specs.t.sol b/packages/contracts-bedrock/test/universal/Specs.t.sol index 462ca615e4eb5..18d356ffe05c8 100644 --- a/packages/contracts-bedrock/test/universal/Specs.t.sol +++ b/packages/contracts-bedrock/test/universal/Specs.t.sol @@ -173,6 +173,7 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "L1StandardBridge", _sel: _getSel("depositETH(uint32,bytes)") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("depositETHTo(address,uint32,bytes)") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("deposits(address,address)") }); + _addSpec({ _name: "L1StandardBridge", _sel: _getSel("SYSTEM_CONFIG_SLOT()") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("finalizeBridgeERC20(address,address,address,address,uint256,bytes)"), @@ -197,7 +198,7 @@ contract Specification_Test is CommonTest { _auth: Role.MESSENGER, _pausable: true }); - _addSpec({ _name: "L1StandardBridge", _sel: _getSel("initialize(address,address)") }); + _addSpec({ _name: "L1StandardBridge", _sel: _getSel("initialize(address,address,address)") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("l2TokenBridge()") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("messenger()") }); _addSpec({ _name: "L1StandardBridge", _sel: _getSel("otherBridge()") }); @@ -257,6 +258,12 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("balance()") }); + _addSpec({ + _name: "OptimismPortalInterop", + _sel: _getSel("depositERC20Transaction(address,uint256,uint256,uint64,bool,bytes)") + }); + _addSpec({ _name: "OptimismPortalInterop", _sel: _getSel("setGasPayingToken(address,uint8,bytes32,bytes32)") }); _addSpec({ _name: "OptimismPortalInterop", _sel: IOptimismPortalInterop.setConfig.selector, @@ -303,6 +310,12 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "OptimismPortal2", _sel: _getSel("respectedGameTypeUpdatedAt()") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("proofSubmitters(bytes32,uint256)") }); _addSpec({ _name: "OptimismPortal2", _sel: _getSel("numProofSubmitters(bytes32)") }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("balance()") }); + _addSpec({ + _name: "OptimismPortal2", + _sel: _getSel("depositERC20Transaction(address,uint256,uint256,uint64,bool,bytes)") + }); + _addSpec({ _name: "OptimismPortal2", _sel: _getSel("setGasPayingToken(address,uint8,bytes32,bytes32)") }); // ProtocolVersions _addSpec({ _name: "ProtocolVersions", _sel: _getSel("RECOMMENDED_SLOT()") }); @@ -379,6 +392,10 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfig", _sel: _getSel("OPTIMISM_PORTAL_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("BATCH_INBOX_SLOT()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("gasPayingToken()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("gasPayingTokenName()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("gasPayingTokenSymbol()") }); + _addSpec({ _name: "SystemConfig", _sel: _getSel("isCustomGasToken()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("DISPUTE_GAME_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfig", _sel: _getSel("disputeGameFactory()") }); _addSpec({ @@ -452,6 +469,10 @@ contract Specification_Test is CommonTest { _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("OPTIMISM_PORTAL_SLOT()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("OPTIMISM_MINTABLE_ERC20_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("BATCH_INBOX_SLOT()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("gasPayingToken()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("gasPayingTokenName()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("gasPayingTokenSymbol()") }); + _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("isCustomGasToken()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("DISPUTE_GAME_FACTORY_SLOT()") }); _addSpec({ _name: "SystemConfigInterop", _sel: _getSel("disputeGameFactory()") }); _addSpec({ diff --git a/packages/contracts-bedrock/test/universal/StandardBridge.t.sol b/packages/contracts-bedrock/test/universal/StandardBridge.t.sol index d268e649ddab0..be7f8a51107c7 100644 --- a/packages/contracts-bedrock/test/universal/StandardBridge.t.sol +++ b/packages/contracts-bedrock/test/universal/StandardBridge.t.sol @@ -5,6 +5,7 @@ import { StandardBridge } from "src/universal/StandardBridge.sol"; import { CommonTest } from "test/setup/CommonTest.sol"; import { OptimismMintableERC20, ILegacyMintableERC20 } from "src/universal/OptimismMintableERC20.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { Constants } from "src/libraries/Constants.sol"; /// @title StandardBridgeTester /// @notice Simple wrapper around the StandardBridge contract that exposes @@ -20,6 +21,10 @@ contract StandardBridgeTester is StandardBridge { return _isCorrectTokenPair(_mintableToken, _otherToken); } + function gasPayingToken() internal pure override returns (address, uint8) { + return (Constants.ETHER, 18); + } + receive() external payable override { } } diff --git a/packages/contracts-bedrock/test/vendor/Initializable.t.sol b/packages/contracts-bedrock/test/vendor/Initializable.t.sol index c332295c6fa42..33d14f6f70a81 100644 --- a/packages/contracts-bedrock/test/vendor/Initializable.t.sol +++ b/packages/contracts-bedrock/test/vendor/Initializable.t.sol @@ -11,6 +11,7 @@ import { Process } from "scripts/libraries/Process.sol"; // Libraries import { LibString } from "@solady/utils/LibString.sol"; import { GameType, Hash, OutputRoot } from "src/dispute/lib/Types.sol"; +import { Constants } from "src/libraries/Constants.sol"; import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol"; // Interfaces @@ -180,7 +181,8 @@ contract Initializer_Test is CommonTest { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) ) ) @@ -215,7 +217,8 @@ contract Initializer_Test is CommonTest { l1StandardBridge: address(0), disputeGameFactory: address(0), optimismPortal: address(0), - optimismMintableERC20Factory: address(0) + optimismMintableERC20Factory: address(0), + gasPayingToken: Constants.ETHER }) ) ) @@ -262,7 +265,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "L1StandardBridgeImpl", target: EIP1967Helper.getImplementation(address(l1StandardBridge)), - initCalldata: abi.encodeCall(l1StandardBridge.initialize, (l1CrossDomainMessenger, superchainConfig)) + initCalldata: abi.encodeCall( + l1StandardBridge.initialize, (l1CrossDomainMessenger, superchainConfig, systemConfig) + ) }) ); // L1StandardBridgeProxy @@ -270,7 +275,9 @@ contract Initializer_Test is CommonTest { InitializeableContract({ name: "L1StandardBridgeProxy", target: address(l1StandardBridge), - initCalldata: abi.encodeCall(l1StandardBridge.initialize, (l1CrossDomainMessenger, superchainConfig)) + initCalldata: abi.encodeCall( + l1StandardBridge.initialize, (l1CrossDomainMessenger, superchainConfig, systemConfig) + ) }) ); // L1ERC721BridgeImpl