diff --git a/op-node/rollup/derive/attributes.go b/op-node/rollup/derive/attributes.go index c65198bcec7..92ccd32cf5f 100644 --- a/op-node/rollup/derive/attributes.go +++ b/op-node/rollup/derive/attributes.go @@ -121,9 +121,21 @@ func (ba *FetchingAttributesBuilder) PreparePayloadAttributes(ctx context.Contex return nil, NewCriticalError(fmt.Errorf("failed to create l1InfoTx: %w", err)) } - txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)+len(upgradeTxs)) + var afterForceIncludeTxs []hexutil.Bytes + // TODO I'm usure if this needs to be only done AFTER interop is enabled, but not on the same block. + // This is similar to what Proto mentioned about blocking the ExecutingMessages on the same block as the interop activation. + if ba.rollupCfg.IsInterop(nextL2Time) { + depositsCompleteTx, err := DepositsCompleteBytes(seqNumber, l1Info) + if err != nil { + return nil, NewCriticalError(fmt.Errorf("failed to create depositsCompleteTx: %w", err)) + } + afterForceIncludeTxs = append(afterForceIncludeTxs, depositsCompleteTx) + } + + txs := make([]hexutil.Bytes, 0, 1+len(depositTxs)+len(afterForceIncludeTxs)+len(upgradeTxs)) txs = append(txs, l1InfoTx) txs = append(txs, depositTxs...) + txs = append(txs, afterForceIncludeTxs...) txs = append(txs, upgradeTxs...) var withdrawals *types.Withdrawals diff --git a/op-node/rollup/derive/attributes_test.go b/op-node/rollup/derive/attributes_test.go index 68c7c71aa1e..e428d952e5b 100644 --- a/op-node/rollup/derive/attributes_test.go +++ b/op-node/rollup/derive/attributes_test.go @@ -195,6 +195,54 @@ func TestPreparePayloadAttributes(t *testing.T) { require.Equal(t, l1InfoTx, []byte(attrs.Transactions[0])) require.True(t, attrs.NoTxPool) }) + t.Run("same origin with deposits on post-Isthmus", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + l1Fetcher := &testutils.MockL1Source{} + defer l1Fetcher.AssertExpectations(t) + l2Parent := testutils.RandomL2BlockRef(rng) + l1CfgFetcher := &testutils.MockL2Client{} + l1CfgFetcher.ExpectSystemConfigByL2Hash(l2Parent.Hash, testSysCfg, nil) + defer l1CfgFetcher.AssertExpectations(t) + l1Info := testutils.RandomBlockInfo(rng) + l1Info.InfoParentHash = l2Parent.L1Origin.Hash + l1Info.InfoNum = l2Parent.L1Origin.Number + 1 + + receipts, depositTxs, err := makeReceipts(rng, l1Info.InfoHash, cfg.DepositContractAddress, []receiptData{ + {goodReceipt: true, DepositLogs: []bool{true, false}}, + {goodReceipt: true, DepositLogs: []bool{true}}, + {goodReceipt: false, DepositLogs: []bool{true}}, + {goodReceipt: false, DepositLogs: []bool{false}}, + }) + require.NoError(t, err) + usedDepositTxs, err := encodeDeposits(depositTxs) + require.NoError(t, err) + + // sets config to post-interop + cfg.AfterHardfork(rollup.Interop, 2) + + epoch := l1Info.ID() + l1InfoTx, err := L1InfoDepositBytes(cfg, testSysCfg, 0, l1Info, 0) + require.NoError(t, err) + + l2Txs := append(append(make([]eth.Data, 0), l1InfoTx), usedDepositTxs...) + + l1Fetcher.ExpectFetchReceipts(epoch.Hash, l1Info, receipts, nil) + attrBuilder := NewFetchingAttributesBuilder(cfg, l1Fetcher, l1CfgFetcher) + attrs, err := attrBuilder.PreparePayloadAttributes(context.Background(), l2Parent, epoch) + require.NoError(t, err) + require.NotNil(t, attrs) + require.Equal(t, l2Parent.Time+cfg.BlockTime, uint64(attrs.Timestamp)) + require.Equal(t, eth.Bytes32(l1Info.InfoMixDigest), attrs.PrevRandao) + require.Equal(t, predeploys.SequencerFeeVaultAddr, attrs.SuggestedFeeRecipient) + require.Equal(t, len(l2Txs), len(attrs.Transactions), "Expected txs to equal l1 info tx + user deposit txs + DepositsComplete") + require.Equal(t, l2Txs, attrs.Transactions) + require.Equal(t, DepositsCompleteBytes4, attrs.Transactions[1+len(depositTxs)][:4]) + require.True(t, attrs.NoTxPool) + depositsCompleteTx, err := DepositsCompleteBytes(l2Parent.SequenceNumber, l1Info) + require.NoError(t, err) + require.Equal(t, l2Txs, append(attrs.Transactions, depositsCompleteTx)) + }) + // Test that the payload attributes builder changes the deposit format based on L2-time-based regolith activation t.Run("regolith", func(t *testing.T) { testCases := []struct { diff --git a/op-node/rollup/derive/deposit_source.go b/op-node/rollup/derive/deposit_source.go index f7a9730ad02..0e663be95ab 100644 --- a/op-node/rollup/derive/deposit_source.go +++ b/op-node/rollup/derive/deposit_source.go @@ -13,9 +13,10 @@ type UserDepositSource struct { } const ( - UserDepositSourceDomain = 0 - L1InfoDepositSourceDomain = 1 - UpgradeDepositSourceDomain = 2 + UserDepositSourceDomain = 0 + L1InfoDepositSourceDomain = 1 + UpgradeDepositSourceDomain = 2 + AfterForceIncludeSourceDomain = 3 ) func (dep *UserDepositSource) SourceHash() common.Hash { @@ -63,3 +64,19 @@ func (dep *UpgradeDepositSource) SourceHash() common.Hash { copy(domainInput[32:], intentHash[:]) return crypto.Keccak256Hash(domainInput[:]) } + +// Used for DepositsComplete/ResetDeposits post-deposits transactions. +type AfterForceIncludeSource struct { + L1BlockHash common.Hash +} + +func (dep *AfterForceIncludeSource) SourceHash() common.Hash { + var input [32 * 2]byte + copy(input[:32], dep.L1BlockHash[:]) + depositIDHash := crypto.Keccak256Hash(input[:]) + + var domainInput [32 * 2]byte + binary.BigEndian.PutUint64(domainInput[32-8:32], AfterForceIncludeSourceDomain) + copy(domainInput[32:], depositIDHash[:]) + return crypto.Keccak256Hash(domainInput[:]) +} diff --git a/op-node/rollup/derive/deposit_source_test.go b/op-node/rollup/derive/deposit_source_test.go index 10fb7048a2a..d851325e370 100644 --- a/op-node/rollup/derive/deposit_source_test.go +++ b/op-node/rollup/derive/deposit_source_test.go @@ -3,6 +3,7 @@ package derive import ( "testing" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" ) @@ -34,3 +35,17 @@ func TestEcotone4788ContractSourceHash(t *testing.T) { assert.Equal(t, expected, actual.Hex()) } + +// TestAfterForceIncludeSourceHash +// cast keccak $(cast concat-hex 0x0000000000000000000000000000000000000000000000000000000000000003 $(cast keccak 0x01)) +// # 0x8afb1c4a581d0e71ab65334e3365ba5511fb15c13fa212776f9d4dafc6287845 +func TestAfterForceIncludeSource(t *testing.T) { + source := AfterForceIncludeSource{ + L1BlockHash: common.Hash{0x01}, + } + + actual := source.SourceHash() + expected := "0x8afb1c4a581d0e71ab65334e3365ba5511fb15c13fa212776f9d4dafc6287845" + + assert.Equal(t, expected, actual.Hex()) +} diff --git a/op-node/rollup/derive/fuzz_parsers_test.go b/op-node/rollup/derive/fuzz_parsers_test.go index 95ce94bc7cc..4f76c4ac742 100644 --- a/op-node/rollup/derive/fuzz_parsers_test.go +++ b/op-node/rollup/derive/fuzz_parsers_test.go @@ -83,15 +83,26 @@ func FuzzL1InfoEcotoneRoundTrip(f *testing.F) { } enc, err := in.marshalBinaryEcotone() if err != nil { - t.Fatalf("Failed to marshal binary: %v", err) + t.Fatalf("Failed to marshal Ecotone binary: %v", err) } var out L1BlockInfo err = out.unmarshalBinaryEcotone(enc) if err != nil { - t.Fatalf("Failed to unmarshal binary: %v", err) + t.Fatalf("Failed to unmarshal Ecotone binary: %v", err) } if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) { - t.Fatalf("The data did not round trip correctly. in: %v. out: %v", in, out) + t.Fatalf("The Ecotone data did not round trip correctly. in: %v. out: %v", in, out) + } + enc, err = in.marshalBinaryIsthmus() + if err != nil { + t.Fatalf("Failed to marshal Isthmus binary: %v", err) + } + err = out.unmarshalBinaryIsthmus(enc) + if err != nil { + t.Fatalf("Failed to unmarshal Isthmus binary: %v", err) + } + if !cmp.Equal(in, out, cmp.Comparer(testutils.BigEqual)) { + t.Fatalf("The Isthmus data did not round trip correctly. in: %v. out: %v", in, out) } }) diff --git a/op-node/rollup/derive/l1_block_info.go b/op-node/rollup/derive/l1_block_info.go index 26f3f6711f5..7631494e42c 100644 --- a/op-node/rollup/derive/l1_block_info.go +++ b/op-node/rollup/derive/l1_block_info.go @@ -20,14 +20,24 @@ import ( const ( L1InfoFuncBedrockSignature = "setL1BlockValues(uint64,uint64,uint256,bytes32,uint64,bytes32,uint256,uint256)" L1InfoFuncEcotoneSignature = "setL1BlockValuesEcotone()" + L1InfoFuncIsthmusSignature = "setL1BlockValuesIsthmus()" + DepositsCompleteSignature = "depositsComplete()" L1InfoArguments = 8 L1InfoBedrockLen = 4 + 32*L1InfoArguments L1InfoEcotoneLen = 4 + 32*5 // after Ecotone upgrade, args are packed into 5 32-byte slots + DepositsCompleteLen = 4 // only the selector + // We set the gas limit to 15k to ensure that the DepositsComplete Transaction does not run out of gas. + // GasBenchMark_L1BlockIsthmus_DepositsComplete:test_depositsComplete_benchmark() (gas: 7768) + // GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5768) + // see `test_depositsComplete_benchmark` at: `/packages/contracts-bedrock/test/BenchmarkTest.t.sol` + DepositsCompleteGas = uint64(15_000) ) var ( L1InfoFuncBedrockBytes4 = crypto.Keccak256([]byte(L1InfoFuncBedrockSignature))[:4] L1InfoFuncEcotoneBytes4 = crypto.Keccak256([]byte(L1InfoFuncEcotoneSignature))[:4] + L1InfoFuncIsthmusBytes4 = crypto.Keccak256([]byte(L1InfoFuncIsthmusSignature))[:4] + DepositsCompleteBytes4 = crypto.Keccak256([]byte(DepositsCompleteSignature))[:4] L1InfoDepositerAddress = common.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001") L1BlockAddress = predeploys.L1BlockAddr ErrInvalidFormat = errors.New("invalid ecotone l1 block info format") @@ -144,7 +154,7 @@ func (info *L1BlockInfo) unmarshalBinaryBedrock(data []byte) error { return nil } -// Ecotone Binary Format +// Isthmus & Ecotone Binary Format // +---------+--------------------------+ // | Bytes | Field | // +---------+--------------------------+ @@ -159,10 +169,24 @@ func (info *L1BlockInfo) unmarshalBinaryBedrock(data []byte) error { // | 32 | BlockHash | // | 32 | BatcherHash | // +---------+--------------------------+ - +// Marshal Ecotone and Isthmus func (info *L1BlockInfo) marshalBinaryEcotone() ([]byte, error) { - w := bytes.NewBuffer(make([]byte, 0, L1InfoEcotoneLen)) - if err := solabi.WriteSignature(w, L1InfoFuncEcotoneBytes4); err != nil { + out, err := marshalBinaryWithSignature(info, L1InfoFuncEcotoneBytes4) + if err != nil { + return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) + } + return out, nil +} +func (info *L1BlockInfo) marshalBinaryIsthmus() ([]byte, error) { + out, err := marshalBinaryWithSignature(info, L1InfoFuncIsthmusBytes4) + if err != nil { + return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err) + } + return out, nil +} +func marshalBinaryWithSignature(info *L1BlockInfo, signature []byte) ([]byte, error) { + w := bytes.NewBuffer(make([]byte, 0, L1InfoEcotoneLen)) // Ecotone and Isthmus have the same length + if err := solabi.WriteSignature(w, signature); err != nil { return nil, err } if err := binary.Write(w, binary.BigEndian, info.BaseFeeScalar); err != nil { @@ -200,14 +224,21 @@ func (info *L1BlockInfo) marshalBinaryEcotone() ([]byte, error) { return w.Bytes(), nil } +// Unmarshal Ecotone and Isthmus func (info *L1BlockInfo) unmarshalBinaryEcotone(data []byte) error { + return unmarshalBinaryWithSignatureAndData(info, L1InfoFuncEcotoneBytes4, data) +} +func (info *L1BlockInfo) unmarshalBinaryIsthmus(data []byte) error { + return unmarshalBinaryWithSignatureAndData(info, L1InfoFuncIsthmusBytes4, data) +} +func unmarshalBinaryWithSignatureAndData(info *L1BlockInfo, signature []byte, data []byte) error { if len(data) != L1InfoEcotoneLen { return fmt.Errorf("data is unexpected length: %d", len(data)) } r := bytes.NewReader(data) var err error - if _, err := solabi.ReadAndValidateSignature(r, L1InfoFuncEcotoneBytes4); err != nil { + if _, err := solabi.ReadAndValidateSignature(r, signature); err != nil { return err } if err := binary.Read(r, binary.BigEndian, &info.BaseFeeScalar); err != nil { @@ -250,9 +281,24 @@ func isEcotoneButNotFirstBlock(rollupCfg *rollup.Config, l2BlockTime uint64) boo return rollupCfg.IsEcotone(l2BlockTime) && !rollupCfg.IsEcotoneActivationBlock(l2BlockTime) } +// isInteropButNotFirstBlock returns whether the specified block is subject to the Isthmus upgrade, +// but is not the actiation block itself. +func isInteropButNotFirstBlock(rollupCfg *rollup.Config, l2BlockTime uint64) bool { + // note from Proto: + // Since we use the pre-interop L1 tx one last time during the upgrade block, + // we must disallow the deposit-txs from using the CrossL2Inbox during this block. + // If the CrossL2Inbox does not exist yet, then it is safe, + // but we have to ensure that the spec and code puts any Interop upgrade-txs after the user deposits. + return rollupCfg.IsInterop(l2BlockTime) && !rollupCfg.IsInteropActivationBlock(l2BlockTime) +} + // L1BlockInfoFromBytes is the inverse of L1InfoDeposit, to see where the L2 chain is derived from func L1BlockInfoFromBytes(rollupCfg *rollup.Config, l2BlockTime uint64, data []byte) (*L1BlockInfo, error) { var info L1BlockInfo + // Important, this should be ordered from most recent to oldest + if isInteropButNotFirstBlock(rollupCfg, l2BlockTime) { + return &info, info.unmarshalBinaryIsthmus(data) + } if isEcotoneButNotFirstBlock(rollupCfg, l2BlockTime) { return &info, info.unmarshalBinaryEcotone(data) } @@ -271,6 +317,7 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber BatcherAddr: sysCfg.BatcherAddr, } var data []byte + if isEcotoneButNotFirstBlock(rollupCfg, l2BlockTime) { l1BlockInfo.BlobBaseFee = block.BlobBaseFee() if l1BlockInfo.BlobBaseFee == nil { @@ -283,11 +330,19 @@ func L1InfoDeposit(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNumber } l1BlockInfo.BlobBaseFeeScalar = scalars.BlobBaseFeeScalar l1BlockInfo.BaseFeeScalar = scalars.BaseFeeScalar - out, err := l1BlockInfo.marshalBinaryEcotone() - if err != nil { - return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) + if isInteropButNotFirstBlock(rollupCfg, l2BlockTime) { + out, err := l1BlockInfo.marshalBinaryIsthmus() + if err != nil { + return nil, fmt.Errorf("failed to marshal Isthmus l1 block info: %w", err) + } + data = out + } else { + out, err := l1BlockInfo.marshalBinaryEcotone() + if err != nil { + return nil, fmt.Errorf("failed to marshal Ecotone l1 block info: %w", err) + } + data = out } - data = out } else { l1BlockInfo.L1FeeOverhead = sysCfg.Overhead l1BlockInfo.L1FeeScalar = sysCfg.Scalar @@ -335,3 +390,33 @@ func L1InfoDepositBytes(rollupCfg *rollup.Config, sysCfg eth.SystemConfig, seqNu } return opaqueL1Tx, nil } + +func DepositsCompleteDeposit(seqNumber uint64, block eth.BlockInfo) (*types.DepositTx, error) { + source := AfterForceIncludeSource{ + L1BlockHash: block.Hash(), + } + out := &types.DepositTx{ + SourceHash: source.SourceHash(), + From: L1InfoDepositerAddress, + To: &L1BlockAddress, + Mint: nil, + Value: big.NewInt(0), + Gas: DepositsCompleteGas, + IsSystemTransaction: false, + Data: DepositsCompleteBytes4, + } + return out, nil +} + +func DepositsCompleteBytes(seqNumber uint64, l1Info eth.BlockInfo) ([]byte, error) { + dep, err := DepositsCompleteDeposit(seqNumber, l1Info) + if err != nil { + return nil, fmt.Errorf("failed to create DepositsComplete tx: %w", err) + } + depositsCompleteTx := types.NewTx(dep) + opaqueDepositsCompleteTx, err := depositsCompleteTx.MarshalBinary() + if err != nil { + return nil, fmt.Errorf("failed to encode DepositsComplete tx: %w", err) + } + return opaqueDepositsCompleteTx, nil +} diff --git a/op-node/rollup/derive/l1_block_info_test.go b/op-node/rollup/derive/l1_block_info_test.go index e5c9253ce1c..0f847309995 100644 --- a/op-node/rollup/derive/l1_block_info_test.go +++ b/op-node/rollup/derive/l1_block_info_test.go @@ -41,6 +41,7 @@ func TestParseL1InfoDepositTxData(t *testing.T) { randomSeqNr := func(rng *rand.Rand) uint64 { return rng.Uint64() } + // Go 1.18 will have native fuzzing for us to use, until then, we cover just the below cases cases := []infoTest{ {"random", testutils.MakeBlockInfo(nil), randomL1Cfg, randomSeqNr}, @@ -109,10 +110,8 @@ func TestParseL1InfoDepositTxData(t *testing.T) { t.Run("regolith", func(t *testing.T) { rng := rand.New(rand.NewSource(1234)) info := testutils.MakeBlockInfo(nil)(rng) - zero := uint64(0) - rollupCfg := rollup.Config{ - RegolithTime: &zero, - } + rollupCfg := rollup.Config{} + rollupCfg.AtHardfork(rollup.Regolith) depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 0) require.NoError(t, err) require.False(t, depTx.IsSystemTransaction) @@ -121,11 +120,8 @@ func TestParseL1InfoDepositTxData(t *testing.T) { t.Run("ecotone", func(t *testing.T) { rng := rand.New(rand.NewSource(1234)) info := testutils.MakeBlockInfo(nil)(rng) - zero := uint64(0) - rollupCfg := rollup.Config{ - RegolithTime: &zero, - EcotoneTime: &zero, - } + rollupCfg := rollup.Config{} + rollupCfg.AtHardfork(rollup.Ecotone) depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 1) require.NoError(t, err) require.False(t, depTx.IsSystemTransaction) @@ -135,12 +131,8 @@ func TestParseL1InfoDepositTxData(t *testing.T) { t.Run("first-block ecotone", func(t *testing.T) { rng := rand.New(rand.NewSource(1234)) info := testutils.MakeBlockInfo(nil)(rng) - zero := uint64(2) - rollupCfg := rollup.Config{ - RegolithTime: &zero, - EcotoneTime: &zero, - BlockTime: 2, - } + rollupCfg := rollup.Config{} + rollupCfg.AfterHardfork(rollup.Ecotone, 2) depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 2) require.NoError(t, err) require.False(t, depTx.IsSystemTransaction) @@ -150,16 +142,75 @@ func TestParseL1InfoDepositTxData(t *testing.T) { t.Run("genesis-block ecotone", func(t *testing.T) { rng := rand.New(rand.NewSource(1234)) info := testutils.MakeBlockInfo(nil)(rng) - zero := uint64(0) - rollupCfg := rollup.Config{ - RegolithTime: &zero, - EcotoneTime: &zero, - BlockTime: 2, - } + rollupCfg := rollup.Config{} + rollupCfg.AfterHardfork(rollup.Ecotone, 2) + depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 0) + require.NoError(t, err) + require.False(t, depTx.IsSystemTransaction) + require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas)) + require.Equal(t, L1InfoEcotoneLen, len(depTx.Data)) + }) + t.Run("Isthmus", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + info := testutils.MakeBlockInfo(nil)(rng) + rollupCfg := rollup.Config{} + rollupCfg.AtHardfork(rollup.Interop) + depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 1) + require.NoError(t, err) + require.False(t, depTx.IsSystemTransaction) + require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas)) + require.Equal(t, L1InfoEcotoneLen, len(depTx.Data)) + require.Equal(t, L1InfoFuncEcotoneBytes4, depTx.Data[:4]) + // should still send Ecotone signature since upgrade happens after deposits + }) + t.Run("first-block isthmus", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + info := testutils.MakeBlockInfo(nil)(rng) + rollupCfg := rollup.Config{} + rollupCfg.AfterHardfork(rollup.Interop, 2) + depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 2) + require.NoError(t, err) + require.False(t, depTx.IsSystemTransaction) + require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas)) + require.Equal(t, L1InfoBedrockLen, len(depTx.Data)) + require.Equal(t, L1InfoFuncIsthmusBytes4, depTx.Data[:4]) + }) + t.Run("genesis-block isthmus", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + info := testutils.MakeBlockInfo(nil)(rng) + rollupCfg := rollup.Config{} + rollupCfg.AfterHardfork(rollup.Interop, 2) depTx, err := L1InfoDeposit(&rollupCfg, randomL1Cfg(rng, info), randomSeqNr(rng), info, 0) require.NoError(t, err) require.False(t, depTx.IsSystemTransaction) require.Equal(t, depTx.Gas, uint64(RegolithSystemTxGas)) require.Equal(t, L1InfoEcotoneLen, len(depTx.Data)) + require.Equal(t, L1InfoBedrockLen, len(depTx.Data)) + }) +} + +func TestDepositsCompleteBytes(t *testing.T) { + randomSeqNr := func(rng *rand.Rand) uint64 { + return rng.Uint64() + } + t.Run("valid return bytes", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + info := testutils.MakeBlockInfo(nil)(rng) + depTxByes, err := DepositsCompleteBytes(randomSeqNr(rng), info) + require.NoError(t, err) + require.Equal(t, depTxByes, DepositsCompleteBytes4) + require.Equal(t, DepositsCompleteLen, len(depTxByes)) + }) + t.Run("valid return Transaction", func(t *testing.T) { + rng := rand.New(rand.NewSource(1234)) + info := testutils.MakeBlockInfo(nil)(rng) + depTx, err := DepositsCompleteDeposit(randomSeqNr(rng), info) + require.NoError(t, err) + require.Equal(t, depTx.Data, DepositsCompleteBytes4) + require.Equal(t, DepositsCompleteLen, len(depTx.Data)) + require.Equal(t, DepositsCompleteGas, depTx.Gas) + require.False(t, depTx.IsSystemTransaction) + require.Equal(t, depTx.Value, big.NewInt(0)) + require.Equal(t, depTx.From, L1InfoDepositerAddress) }) } diff --git a/op-node/rollup/types.go b/op-node/rollup/types.go index fec118567c5..476a60fc32d 100644 --- a/op-node/rollup/types.go +++ b/op-node/rollup/types.go @@ -457,12 +457,51 @@ func (c *Config) IsHoloceneActivationBlock(l2BlockTime uint64) bool { !c.IsHolocene(l2BlockTime-c.BlockTime) } +// TODO rename to IsIsthmusActivationBlock (this will require quite a bit of renaming) func (c *Config) IsInteropActivationBlock(l2BlockTime uint64) bool { return c.IsInterop(l2BlockTime) && l2BlockTime >= c.BlockTime && !c.IsInterop(l2BlockTime-c.BlockTime) } +func (c *Config) AtHardfork(hardfork ForkName) { + zero := uint64(0) + // IMPORTANT! ordered from newest to oldest + switch hardfork { + case Interop: + c.InteropTime = &zero + fallthrough + case Holocene: + c.HoloceneTime = &zero + fallthrough + case Granite: + c.GraniteTime = &zero + fallthrough + case Fjord: + c.FjordTime = &zero + fallthrough + case Ecotone: + c.EcotoneTime = &zero + fallthrough + case Delta: + c.DeltaTime = &zero + fallthrough + case Canyon: + c.CanyonTime = &zero + fallthrough + case Regolith: + c.RegolithTime = &zero + fallthrough + case None: + break + } +} +func (c *Config) AfterHardfork(hardfork ForkName, timestamp uint64) { + c.AtHardfork(hardfork) + // required after hardfork time bump + c.BlockTime = timestamp +} + // ForkchoiceUpdatedVersion returns the EngineAPIMethod suitable for the chain hard fork version. func (c *Config) ForkchoiceUpdatedVersion(attr *eth.PayloadAttributes) eth.EngineAPIMethod { if attr == nil { diff --git a/packages/contracts-bedrock/.gas-snapshot b/packages/contracts-bedrock/.gas-snapshot index b3ea3b88545..c34ed57a169 100644 --- a/packages/contracts-bedrock/.gas-snapshot +++ b/packages/contracts-bedrock/.gas-snapshot @@ -1,3 +1,9 @@ +GasBenchMark_L1BlockIsthmus_DepositsComplete:test_depositsComplete_benchmark() (gas: 7768) +GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm:test_depositsComplete_benchmark() (gas: 5768) +GasBenchMark_L1BlockIsthmus_SetValuesIsthmus:test_setL1BlockValuesIsthmus_benchmark() (gas: 174050) +GasBenchMark_L1BlockIsthmus_SetValuesIsthmus_Warm:test_setL1BlockValuesIsthmus_benchmark() (gas: 5185) +GasBenchMark_L1Block_SetValuesEcotone:test_setL1BlockValuesEcotone_benchmark() (gas: 159033) +GasBenchMark_L1Block_SetValuesEcotone_Warm:test_setL1BlockValuesEcotone_benchmark() (gas: 7539) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_0() (gas: 369356) GasBenchMark_L1CrossDomainMessenger:test_sendMessage_benchmark_1() (gas: 2967496) GasBenchMark_L1StandardBridge_Deposit:test_depositERC20_benchmark_0() (gas: 564483) diff --git a/packages/contracts-bedrock/scripts/L2Genesis.s.sol b/packages/contracts-bedrock/scripts/L2Genesis.s.sol index 7d456865393..073b4e9af28 100644 --- a/packages/contracts-bedrock/scripts/L2Genesis.s.sol +++ b/packages/contracts-bedrock/scripts/L2Genesis.s.sol @@ -357,7 +357,7 @@ contract L2Genesis is Deployer { /// @notice This predeploy is following the safety invariant #1. function setL1Block() public { if (cfg.useInterop()) { - string memory cname = "L1BlockInterop"; + string memory cname = "L1BlockIsthmus"; address impl = Predeploys.predeployToCodeNamespace(Predeploys.L1_BLOCK_ATTRIBUTES); console.log("Setting %s implementation at: %s", cname, impl); vm.etch(impl, vm.getDeployedCode(string.concat(cname, ".sol:", cname))); diff --git a/packages/contracts-bedrock/scripts/periphery/drippie/DrippieConfig.s.sol b/packages/contracts-bedrock/scripts/periphery/drippie/DrippieConfig.s.sol index 3ab115ba326..1b03d53a929 100644 --- a/packages/contracts-bedrock/scripts/periphery/drippie/DrippieConfig.s.sol +++ b/packages/contracts-bedrock/scripts/periphery/drippie/DrippieConfig.s.sol @@ -126,7 +126,8 @@ contract DrippieConfig is Script { abi.decode(checkparams, (CheckSecrets.Params)); } else if (strcmp(dripcheck, "CheckTrue")) { // No parameters to decode. - } else { + } + else { console.log("ERROR: unknown drip configuration %s", dripcheck); revert UnknownDripCheck(dripcheck); } diff --git a/packages/contracts-bedrock/semver-lock.json b/packages/contracts-bedrock/semver-lock.json index 59def1ae86c..bc5df36ba3f 100644 --- a/packages/contracts-bedrock/semver-lock.json +++ b/packages/contracts-bedrock/semver-lock.json @@ -83,7 +83,7 @@ "initCodeHash": "0xfd099da051edf13b147f4382ab4bed9db546d0c48157736ba298fb7e178b20d9", "sourceCodeHash": "0x24db623574743432626ed0d7dd938bbd2149b570a00328c772debd7eb179ff1d" }, - "src/L2/L1BlockInterop.sol": { + "src/L2/L1BlockIsthmus.sol": { "initCodeHash": "0x6833a323934b3be1e5a5c7491c652b6e90bc5102416ddbb255b5f65aa6d5d4a1", "sourceCodeHash": "0xd8ec2f814690d1ffd55e5b8496bca5a179d6d1772d61f71cdf8296c9058dc2c6" }, diff --git a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol index 9fad94a0916..2b4bb3cd3e6 100644 --- a/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol +++ b/packages/contracts-bedrock/src/L1/OptimismPortalInterop.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import { OptimismPortal2 } from "src/L1/OptimismPortal2.sol"; -import { L1BlockInterop, ConfigType } from "src/L2/L1BlockInterop.sol"; +import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol"; import { Predeploys } from "src/libraries/Predeploys.sol"; import { Constants } from "src/libraries/Constants.sol"; @@ -47,7 +47,7 @@ contract OptimismPortalInterop is OptimismPortal2 { uint256(0), // value uint64(SYSTEM_DEPOSIT_GAS_LIMIT), // gasLimit false, // isCreation, - abi.encodeCall(L1BlockInterop.setConfig, (_type, _value)) + abi.encodeCall(L1BlockIsthmus.setConfig, (_type, _value)) ) ); } diff --git a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol index 098b59e1f9f..eb5ec7bbf12 100644 --- a/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol +++ b/packages/contracts-bedrock/src/L1/SystemConfigInterop.sol @@ -6,7 +6,7 @@ import { OptimismPortalInterop as OptimismPortal } from "src/L1/OptimismPortalIn import { GasPayingToken } from "src/libraries/GasPayingToken.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import { SystemConfig } from "src/L1/SystemConfig.sol"; -import { ConfigType } from "src/L2/L1BlockInterop.sol"; +import { ConfigType } from "src/L2/L1BlockIsthmus.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { Storage } from "src/libraries/Storage.sol"; diff --git a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol index cb0ccfa7714..d55f2ca80df 100644 --- a/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol +++ b/packages/contracts-bedrock/src/L2/CrossL2Inbox.sol @@ -17,6 +17,13 @@ interface IDependencySet { function isInDependencySet(uint256 _chainId) external view returns (bool); } +/// @title IL1Block +/// @notice Interface for L1Block with only `isDeposit()` method. +interface IL1Block { + /// @notice Returns whether the call was triggered from a a deposit or not. + /// @return True if the current call was triggered by a deposit transaction, and false otherwise. + function isDeposit() external view returns (bool); +} /// @notice Thrown when the caller is not DEPOSITOR_ACCOUNT when calling `setInteropStart()` error NotDepositor(); @@ -35,6 +42,9 @@ error InvalidChainId(); /// @notice Thrown when trying to execute a cross chain message and the target call fails. error TargetCallFailed(); +/// @notice Thrown when trying to execute a cross chain message on a deposit transaction. +error NoExecutingDeposits(); + /// @custom:proxied /// @custom:predeploy 0x4200000000000000000000000000000000000022 /// @title CrossL2Inbox @@ -71,7 +81,7 @@ contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware { /// @notice Semantic version. /// @custom:semver 1.0.0-beta.5 - string public constant version = "1.0.0-beta.5"; + string public constant version = "1.1.0-beta.5"; /// @notice Emitted when a cross chain message is being executed. /// @param msgHash Hash of message payload being executed. @@ -144,6 +154,9 @@ contract CrossL2Inbox is ICrossL2Inbox, ISemver, TransientReentrancyAware { payable reentrantAware { + // We need to know if this is being called on a depositTx + if (IL1Block(Predeploys.L1_BLOCK_ATTRIBUTES).isDeposit()) revert NoExecutingDeposits(); + // Check the Identifier. _checkIdentifier(_id); diff --git a/packages/contracts-bedrock/src/L2/L1Block.sol b/packages/contracts-bedrock/src/L2/L1Block.sol index 8cfbb391f0f..2476e686277 100644 --- a/packages/contracts-bedrock/src/L2/L1Block.sol +++ b/packages/contracts-bedrock/src/L2/L1Block.sol @@ -57,9 +57,9 @@ contract L1Block is ISemver, IGasToken { /// @notice The latest L1 blob base fee. uint256 public blobBaseFee; - /// @custom:semver 1.4.1-beta.1 + /// @custom:semver 1.5.1-beta.1 function version() public pure virtual returns (string memory) { - return "1.4.1-beta.1"; + return "1.5.1-beta.1"; } /// @notice Returns the gas paying token, its decimals, name and symbol. @@ -133,7 +133,34 @@ contract L1Block is ISemver, IGasToken { /// 7. _blobBaseFee L1 blob base fee. /// 8. _hash L1 blockhash. /// 9. _batcherHash Versioned hash to authenticate batcher by. - function setL1BlockValuesEcotone() external { + function setL1BlockValuesEcotone() public { + _setL1BlockValuesEcotone(); + } + + /// @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 }); + } + + /// @notice Updates the L1 block values for an Ecotone upgraded chain. + /// Params are packed and passed in as raw msg.data instead of ABI to reduce calldata size. + /// Params are expected to be in the following order: + /// 1. _baseFeeScalar L1 base fee scalar + /// 2. _blobBaseFeeScalar L1 blob base fee scalar + /// 3. _sequenceNumber Number of L2 blocks since epoch start. + /// 4. _timestamp L1 timestamp. + /// 5. _number L1 blocknumber. + /// 6. _basefee L1 base fee. + /// 7. _blobBaseFee L1 blob base fee. + /// 8. _hash L1 blockhash. + /// 9. _batcherHash Versioned hash to authenticate batcher by. + function _setL1BlockValuesEcotone() internal { address depositor = DEPOSITOR_ACCOUNT(); assembly { // Revert if the caller is not the depositor account. @@ -151,15 +178,4 @@ contract L1Block is ISemver, IGasToken { sstore(batcherHash.slot, calldataload(132)) // bytes32 } } - - /// @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/L1BlockIsthmus.sol similarity index 69% rename from packages/contracts-bedrock/src/L2/L1BlockInterop.sol rename to packages/contracts-bedrock/src/L2/L1BlockIsthmus.sol index 8dbf986fb7e..b64c44646ab 100644 --- a/packages/contracts-bedrock/src/L2/L1BlockInterop.sol +++ b/packages/contracts-bedrock/src/L2/L1BlockIsthmus.sol @@ -5,9 +5,10 @@ import { L1Block } from "src/L2/L1Block.sol"; 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 "src/libraries/L1BlockErrors.sol"; -/// @notice Enum representing different types of configurations that can be set on L1BlockInterop. +/// @notice Enum representing different types of configurations that can be set on L1BlockIsthmus. /// @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. @@ -19,9 +20,9 @@ enum ConfigType { /// @custom:proxied /// @custom:predeploy 0x4200000000000000000000000000000000000015 -/// @title L1BlockInterop -/// @notice Interop extenstions of L1Block. -contract L1BlockInterop is L1Block { +/// @title L1BlockIsthmus +/// @notice Isthmus extenstions of L1Block. +contract L1BlockIsthmus is L1Block { using EnumerableSet for EnumerableSet.UintSet; /// @notice Event emitted when a new dependency is added to the interop dependency set. @@ -33,9 +34,23 @@ contract L1BlockInterop is L1Block { /// @notice The interop dependency set, containing the chain IDs in it. EnumerableSet.UintSet dependencySet; - /// @custom:semver +interop + /// @notice Storage slot that the isDeposit is stored at. + /// This is a custom slot that is not part of the standard storage layout. + /// keccak256(abi.encode(uint256(keccak256("l1Block.identifier.isDeposit")) - 1)) & ~bytes32(uint256(0xff)) + uint256 internal constant IS_DEPOSIT_SLOT = 0x921bd3a089295c6e5540e8fba8195448d253efd6f2e3e495b499b627dc36a300; + + /// @custom:semver +isthmus function version() public pure override returns (string memory) { - return string.concat(super.version(), "+interop"); + return string.concat(super.version(), "+isthmus"); + } + + /// @notice Returns whether the call was triggered from a a deposit or not. + /// @notice This function is only callable by the CrossL2Inbox contract. + function isDeposit() external view returns (bool isDeposit_) { + if (msg.sender != Predeploys.CROSS_L2_INBOX) revert NotCrossL2Inbox(); + assembly { + isDeposit_ := sload(IS_DEPOSIT_SLOT) + } } /// @notice Returns true if a chain ID is in the interop dependency set and false otherwise. @@ -52,6 +67,29 @@ contract L1BlockInterop is L1Block { return uint8(dependencySet.length()); } + /// @notice Updates the `isDeposit` flag and sets the L1 block values for an Isthmus upgraded chain. + /// It updates the L1 block values through the `setL1BlockValuesEcotone` function. + /// It forwards the calldata to the internally-used `setL1BlockValuesEcotone` function. + function setL1BlockValuesIsthmus() external { + // Set the isDeposit flag to true. + assembly { + sstore(IS_DEPOSIT_SLOT, 1) + } + + _setL1BlockValuesEcotone(); + } + + /// @notice Resets the isDeposit flag. + /// Should only be called by the depositor account after the deposits are complete. + function depositsComplete() external { + if (msg.sender != DEPOSITOR_ACCOUNT()) revert NotDepositor(); + + // Set the isDeposit flag to false. + assembly { + sstore(IS_DEPOSIT_SLOT, 0) + } + } + /// @notice Sets static configuration options for the L2 system. Can only be called by the special /// depositor account. /// @param _type The type of configuration to set. diff --git a/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol b/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol index c9ef3903aeb..44e156e158c 100644 --- a/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol +++ b/packages/contracts-bedrock/src/libraries/L1BlockErrors.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.0; /// @notice Error returns when a non-depositor account tries to set L1 block values. error NotDepositor(); +/// @notice Error when a non-cross L2 Inbox sender tries to call the `isDeposit()` method. +error NotCrossL2Inbox(); + /// @notice Error when a chain ID is not in the interop dependency set. error NotDependency(); diff --git a/packages/contracts-bedrock/test/BenchmarkTest.t.sol b/packages/contracts-bedrock/test/BenchmarkTest.t.sol index 4722107a314..ca1f1c623ea 100644 --- a/packages/contracts-bedrock/test/BenchmarkTest.t.sol +++ b/packages/contracts-bedrock/test/BenchmarkTest.t.sol @@ -9,6 +9,7 @@ import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol"; import { CrossDomainMessenger } from "src/universal/CrossDomainMessenger.sol"; import { ResourceMetering } from "src/L1/ResourceMetering.sol"; import { Types } from "src/libraries/Types.sol"; +import { L1BlockIsthmus } from "src/L2/L1BlockIsthmus.sol"; // Free function for setting the prevBaseFee param in the OptimismPortal. function setPrevBaseFee(Vm _vm, address _op, uint128 _prevBaseFee) { @@ -209,3 +210,105 @@ contract GasBenchMark_L2OutputOracle is CommonTest { l2OutputOracle.proposeL2Output(nonZeroHash, nextBlockNumber, 0, 0); } } + +contract GasBenchMark_L1Block is CommonTest { + address depositor; + bytes setValuesEcotoneCalldata; + + function setUp() public virtual override { + super.setUp(); + depositor = l1Block.DEPOSITOR_ACCOUNT(); + setValuesEcotoneCalldata = abi.encodePacked( + bytes4(keccak256("setL1BlockValuesEcotone()")), + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + keccak256(abi.encode(1)), + bytes32(type(uint256).max) + ); + vm.startPrank(depositor); + } +} + +contract GasBenchMark_L1Block_SetValuesEcotone is GasBenchMark_L1Block { + function test_setL1BlockValuesEcotone_benchmark() external { + address(l1Block).call(abi.encodePacked(setValuesEcotoneCalldata)); + } +} + +contract GasBenchMark_L1Block_SetValuesEcotone_Warm is GasBenchMark_L1Block { + function setUp() public virtual override { + address(l1Block).call(abi.encodePacked(setValuesEcotoneCalldata)); + } + + function test_setL1BlockValuesEcotone_benchmark() external { + address(l1Block).call(abi.encodePacked(setValuesEcotoneCalldata)); + } +} + +contract GasBenchMark_L1BlockIsthmus is GasBenchMark_L1Block { + L1BlockIsthmus l1BlockIsthmus; + + function setUp() public virtual override { + super.setUp(); + l1BlockIsthmus = new L1BlockIsthmus(); + setValuesEcotoneCalldata = abi.encodePacked( + type(uint32).max, + type(uint32).max, + type(uint64).max, + type(uint64).max, + type(uint64).max, + type(uint256).max, + type(uint256).max, + keccak256(abi.encode(1)), + bytes32(type(uint256).max) + ); + } +} + +contract GasBenchMark_L1BlockIsthmus_SetValuesIsthmus is GasBenchMark_L1BlockIsthmus { + function test_setL1BlockValuesIsthmus_benchmark() external { + address(l1BlockIsthmus).call( + abi.encodePacked(l1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + } +} + +contract GasBenchMark_L1BlockIsthmus_SetValuesIsthmus_Warm is GasBenchMark_L1BlockIsthmus { + function setUp() public virtual override { + address(l1BlockIsthmus).call( + abi.encodePacked(l1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + } + + function test_setL1BlockValuesIsthmus_benchmark() external { + address(l1BlockIsthmus).call( + abi.encodePacked(l1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + } +} + +contract GasBenchMark_L1BlockIsthmus_DepositsComplete is GasBenchMark_L1BlockIsthmus { + function test_depositsComplete_benchmark() external { + address(l1BlockIsthmus).call(abi.encodeWithSelector(l1BlockIsthmus.depositsComplete.selector)); + } +} + +contract GasBenchMark_L1BlockIsthmus_DepositsComplete_Warm is GasBenchMark_L1BlockIsthmus { + function setUp() public virtual override { + super.setUp(); + // Set the isDeposit flag to true so then we can benchmark when it is reset. + address(l1BlockIsthmus).call( + abi.encodePacked(l1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + } + + function test_depositsComplete_benchmark() external { + address(l1BlockIsthmus).call(abi.encodeWithSelector(l1BlockIsthmus.depositsComplete.selector)); + } + +} diff --git a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol index 1d9c2b01a24..6fb9a1aab6e 100644 --- a/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/OptimismPortalInterop.t.sol @@ -11,7 +11,7 @@ import { Predeploys } from "src/libraries/Predeploys.sol"; // Target contract dependencies import "src/libraries/PortalErrors.sol"; import { OptimismPortalInterop } from "src/L1/OptimismPortalInterop.sol"; -import { L1BlockInterop, ConfigType } from "src/L2/L1BlockInterop.sol"; +import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol"; contract OptimismPortalInterop_Test is CommonTest { /// @notice Marked virtual to be overridden in @@ -31,7 +31,7 @@ contract OptimismPortalInterop_Test is CommonTest { _mint: 0, _gasLimit: 200_000, _isCreation: false, - _data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value)) + _data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.SET_GAS_PAYING_TOKEN, _value)) }); vm.prank(address(_optimismPortalInterop().systemConfig())); @@ -54,7 +54,7 @@ contract OptimismPortalInterop_Test is CommonTest { _mint: 0, _gasLimit: 200_000, _isCreation: false, - _data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.ADD_DEPENDENCY, _value)) + _data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.ADD_DEPENDENCY, _value)) }); vm.prank(address(_optimismPortalInterop().systemConfig())); @@ -77,7 +77,7 @@ contract OptimismPortalInterop_Test is CommonTest { _mint: 0, _gasLimit: 200_000, _isCreation: false, - _data: abi.encodeCall(L1BlockInterop.setConfig, (ConfigType.REMOVE_DEPENDENCY, _value)) + _data: abi.encodeCall(L1BlockIsthmus.setConfig, (ConfigType.REMOVE_DEPENDENCY, _value)) }); vm.prank(address(_optimismPortalInterop().systemConfig())); diff --git a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol index 36887fa3368..c861331b138 100644 --- a/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol +++ b/packages/contracts-bedrock/test/L1/SystemConfigInterop.t.sol @@ -14,7 +14,7 @@ import { SystemConfig } from "src/L1/SystemConfig.sol"; import { SystemConfigInterop } from "src/L1/SystemConfigInterop.sol"; import { OptimismPortalInterop } from "src/L1/OptimismPortalInterop.sol"; import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import { ConfigType } from "src/L2/L1BlockInterop.sol"; +import { ConfigType } from "src/L2/L1BlockIsthmus.sol"; contract SystemConfigInterop_Test is CommonTest { /// @notice Marked virtual to be overridden in diff --git a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol index 12b24ae1d47..ff4b4d80f03 100644 --- a/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol +++ b/packages/contracts-bedrock/test/L2/CrossL2Inbox.t.sol @@ -11,7 +11,9 @@ import { TransientContext } from "src/libraries/TransientContext.sol"; // Target contracts import { CrossL2Inbox, + IL1Block, NotEntered, + NoExecutingDeposits, InvalidTimestamp, InvalidChainId, TargetCallFailed, @@ -155,6 +157,13 @@ contract CrossL2InboxTest is Test { // Ensure that the target call is payable if value is sent if (_value > 0) assumePayable(_target); + // Ensure is not a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(false) + }); + // Ensure that the target call does not revert vm.mockCall({ callee: _target, msgValue: _value, data: _message, returnData: abi.encode(true) }); @@ -210,6 +219,13 @@ contract CrossL2InboxTest is Test { _id1.timestamp = bound(_id1.timestamp, interopStartTime + 1, block.timestamp); _id2.timestamp = bound(_id2.timestamp, interopStartTime + 1, block.timestamp); + // Ensure is not a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(false) + }); + // Ensure that id1's chain ID is in the dependency set vm.mockCall({ callee: Predeploys.L1_BLOCK_ATTRIBUTES, @@ -254,6 +270,32 @@ contract CrossL2InboxTest is Test { assertEq(crossL2Inbox.chainId(), _id2.chainId); } + /// @dev Tests that the `executeMessage` function reverts if the transaction comes from a deposit. + function testFuzz_executeMessage_isDeposit_reverts( + ICrossL2Inbox.Identifier calldata _id, + address _target, + bytes calldata _message, + uint256 _value + ) + external + { + // Ensure it is a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(true) + }); + + // Ensure that the contract has enough balance to send with value + vm.deal(address(this), _value); + + // Expect a revert with the NoExecutingDeposits selector + vm.expectRevert(NoExecutingDeposits.selector); + + // Call the executeMessage function + crossL2Inbox.executeMessage{ value: _value }({ _id: _id, _target: _target, _message: _message }); + } + /// @dev Tests that the `executeMessage` function reverts when called with an identifier with an invalid timestamp. function testFuzz_executeMessage_invalidTimestamp_reverts( ICrossL2Inbox.Identifier calldata _id, @@ -267,6 +309,13 @@ contract CrossL2InboxTest is Test { // Ensure that the id's timestamp is invalid (greater than the current block timestamp) vm.assume(_id.timestamp > block.timestamp); + // Ensure is not a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(false) + }); + // Ensure that the contract has enough balance to send with value vm.deal(address(this), _value); @@ -316,6 +365,13 @@ contract CrossL2InboxTest is Test { // interop start time) _id.timestamp = bound(_id.timestamp, interopStartTime + 1, block.timestamp); + // Ensure is not a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(false) + }); + // Ensure that the chain ID is NOT in the dependency set vm.mockCall({ callee: Predeploys.L1_BLOCK_ATTRIBUTES, @@ -353,6 +409,13 @@ contract CrossL2InboxTest is Test { // Ensure that the target call reverts vm.mockCallRevert({ callee: _target, msgValue: _value, data: _message, revertData: abi.encode(false) }); + // Ensure is not a deposit transaction + vm.mockCall({ + callee: Predeploys.L1_BLOCK_ATTRIBUTES, + data: abi.encodeWithSelector(IL1Block.isDeposit.selector), + returnData: abi.encode(false) + }); + // Ensure that the chain ID is in the dependency set vm.mockCall({ callee: Predeploys.L1_BLOCK_ATTRIBUTES, diff --git a/packages/contracts-bedrock/test/L2/L1Block.t.sol b/packages/contracts-bedrock/test/L2/L1Block.t.sol index ce67a669222..1c004a6d429 100644 --- a/packages/contracts-bedrock/test/L2/L1Block.t.sol +++ b/packages/contracts-bedrock/test/L2/L1Block.t.sol @@ -8,7 +8,7 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { Encoding } from "src/libraries/Encoding.sol"; import { Constants } from "src/libraries/Constants.sol"; -// Target contract +// Target contract dependencies import { L1Block } from "src/L2/L1Block.sol"; import "src/libraries/L1BlockErrors.sol"; diff --git a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol index 159021e77dc..1c2407dd73a 100644 --- a/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol +++ b/packages/contracts-bedrock/test/L2/L1BlockInterop.t.sol @@ -8,16 +8,17 @@ import { CommonTest } from "test/setup/CommonTest.sol"; import { StaticConfig } from "src/libraries/StaticConfig.sol"; // Target contract dependencies -import { L1BlockInterop, ConfigType } from "src/L2/L1BlockInterop.sol"; +import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; import "src/libraries/L1BlockErrors.sol"; -contract L1BlockInteropTest is CommonTest { +contract L1BlockIsthmusTest is CommonTest { event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); event DependencyAdded(uint256 indexed chainId); event DependencyRemoved(uint256 indexed chainId); modifier prankDepositor() { - vm.startPrank(l1Block.DEPOSITOR_ACCOUNT()); + vm.startPrank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); _; vm.stopPrank(); } @@ -33,14 +34,14 @@ contract L1BlockInteropTest is CommonTest { function testFuzz_isInDependencySet_succeeds(uint256 _chainId) public prankDepositor { vm.assume(_chainId != block.chainid); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); - assertTrue(_l1BlockInterop().isInDependencySet(_chainId)); + assertTrue(_l1BlockIsthmus().isInDependencySet(_chainId)); } /// @dev Tests that `isInDependencySet` returns true when the chain's chain ID is passed as the input. function test_isInDependencySet_chainChainId_succeeds() public view { - assertTrue(_l1BlockInterop().isInDependencySet(block.chainid)); + assertTrue(_l1BlockIsthmus().isInDependencySet(block.chainid)); } /// @dev Tests that `isInDependencySet` reverts when the input chain ID is not in the dependency set @@ -49,16 +50,16 @@ contract L1BlockInteropTest is CommonTest { vm.assume(_chainId != block.chainid); // Check that the chain ID is not in the dependency set - assertFalse(_l1BlockInterop().isInDependencySet(_chainId)); + assertFalse(_l1BlockIsthmus().isInDependencySet(_chainId)); } /// @dev Tests that `isInDependencySet` returns false when the dependency set is empty. function testFuzz_isInDependencySet_dependencySetEmpty_succeeds(uint256 _chainId) public view { vm.assume(_chainId != block.chainid); - assertEq(_l1BlockInterop().dependencySetSize(), 0); + assertEq(_l1BlockIsthmus().dependencySetSize(), 0); - assertFalse(_l1BlockInterop().isInDependencySet(_chainId)); + assertFalse(_l1BlockIsthmus().isInDependencySet(_chainId)); } /// @dev Tests that the dependency set size is correct when adding an arbitrary number of chain IDs. @@ -69,16 +70,16 @@ contract L1BlockInteropTest is CommonTest { for (uint256 i = 0; i < _dependencySetSize; i++) { if (i == block.chainid) continue; - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); uniqueCount++; } - assertEq(_l1BlockInterop().dependencySetSize(), uniqueCount); + assertEq(_l1BlockIsthmus().dependencySetSize(), uniqueCount); } /// @dev Tests that the dependency set size is correct when the dependency set is empty. function test_dependencySetSize_dependencySetEmpty_succeeds() public view { - assertEq(_l1BlockInterop().dependencySetSize(), 0); + assertEq(_l1BlockIsthmus().dependencySetSize(), 0); } /// @dev Tests that the config for setting the gas paying token succeeds. @@ -96,7 +97,7 @@ contract L1BlockInteropTest is CommonTest { vm.expectEmit(address(l1Block)); emit GasPayingTokenSet({ token: _token, decimals: _decimals, name: _name, symbol: _symbol }); - _l1BlockInterop().setConfig( + _l1BlockIsthmus().setConfig( ConfigType.SET_GAS_PAYING_TOKEN, StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) ); @@ -114,7 +115,7 @@ contract L1BlockInteropTest is CommonTest { vm.assume(_token != address(vm)); vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setConfig( + _l1BlockIsthmus().setConfig( ConfigType.SET_GAS_PAYING_TOKEN, StaticConfig.encodeSetGasPayingToken({ _token: _token, _decimals: _decimals, _name: _name, _symbol: _symbol }) ); @@ -127,41 +128,41 @@ contract L1BlockInteropTest is CommonTest { vm.expectEmit(address(l1Block)); emit DependencyAdded(_chainId); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); } /// @dev Tests that adding a dependency reverts if it's the chain's chain id function test_setConfig_addDependency_chainChainId_reverts() public prankDepositor { vm.expectRevert(AlreadyDependency.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(block.chainid)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(block.chainid)); } /// @dev Tests that adding a dependency already in the set reverts function test_setConfig_addDependency_alreadyDependency_reverts(uint256 _chainId) public prankDepositor { vm.assume(_chainId != block.chainid); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); vm.expectRevert(AlreadyDependency.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); } /// @dev Tests that setting the add dependency config as not the depositor reverts. function testFuzz_setConfig_addDependency_notDepositor_reverts(uint256 _chainId) public { vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); } /// @dev Tests that setting the add dependency config when the dependency set size is too large reverts. function test_setConfig_addDependency_dependencySetSizeTooLarge_reverts() public prankDepositor { for (uint256 i = 0; i < type(uint8).max; i++) { - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); } - assertEq(_l1BlockInterop().dependencySetSize(), type(uint8).max); + assertEq(_l1BlockIsthmus().dependencySetSize(), type(uint8).max); vm.expectRevert(DependencySetSizeTooLarge.selector); - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(1)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(1)); } /// @dev Tests that the config for removing a dependency can be set. @@ -169,24 +170,24 @@ contract L1BlockInteropTest is CommonTest { vm.assume(_chainId != block.chainid); // Add the chain ID to the dependency set before removing it - _l1BlockInterop().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); vm.expectEmit(address(l1Block)); emit DependencyRemoved(_chainId); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); } /// @dev Tests that setting the remove dependency config as not the depositor reverts. function testFuzz_setConfig_removeDependency_notDepositor_reverts(uint256 _chainId) public { vm.expectRevert(NotDepositor.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); } /// @dev Tests that setting the remove dependency config for the chain's chain ID reverts. function test_setConfig_removeDependency_chainChainId_reverts() public prankDepositor { vm.expectRevert(CantRemovedDependency.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(block.chainid)); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(block.chainid)); } /// @dev Tests that setting the remove dependency config for a chain ID that is not in the dependency set reverts. @@ -194,11 +195,118 @@ contract L1BlockInteropTest is CommonTest { vm.assume(_chainId != block.chainid); vm.expectRevert(NotDependency.selector); - _l1BlockInterop().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); } - /// @dev Returns the L1BlockInterop instance. - function _l1BlockInterop() internal view returns (L1BlockInterop) { - return L1BlockInterop(address(l1Block)); + /// @dev Returns the L1BlockIsthmus instance. + function _l1BlockIsthmus() internal view returns (L1BlockIsthmus) { + return L1BlockIsthmus(address(l1Block)); + } +} + +contract L1BlockIsthmusIsDeposit_Test is L1BlockIsthmusTest { + /// @dev Tests that `isDeposit` reverts if the caller is not the cross L2 inbox. + function test_isDeposit_notCrossL2Inbox_reverts(address _caller) external { + vm.assume(_caller != Predeploys.CROSS_L2_INBOX); + vm.expectRevert(NotCrossL2Inbox.selector); + _l1BlockIsthmus().isDeposit(); + } + + /// @dev Tests that `isDeposit` always returns the correct value. + function test_isDeposit_succeeds() external { + // Assert is false if the value is not updated + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); + + /// @dev Assuming that `setL1BlockValuesIsthmus` will set the proper value. That function is tested as well + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + + // Assert is true if the value is updated + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), true); + } +} + +contract L1BlockIsthmusSetL1BlockValuesIsthmus_Test is L1BlockIsthmusTest { + /// @dev Tests that `setL1BlockValuesIsthmus` reverts if sender address is not the depositor + function test_setL1BlockValuesIsthmus_notDepositor_reverts(address _caller) external { + vm.assume(_caller != _l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + vm.prank(_caller); + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + } + + /// @dev Tests that `setL1BlockValuesIsthmus` succeeds if sender address is the depositor + function test_setL1BlockValuesIsthmus_succeeds( + uint32 baseFeeScalar, + uint32 blobBaseFeeScalar, + uint64 sequenceNumber, + uint64 timestamp, + uint64 number, + uint256 baseFee, + uint256 blobBaseFee, + bytes32 hash, + bytes32 batcherHash + ) + external + { + // Ensure the `isDepositTransaction` flag is false before calling `setL1BlockValuesIsthmus` + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); + + bytes memory setValuesEcotoneCalldata = abi.encodePacked( + baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash + ); + + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + (bool success,) = address(l1Block).call( + abi.encodePacked(L1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + assertTrue(success, "function call failed"); + + // Assert that the `isDepositTransaction` flag was properly set to true + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), true); + + // Assert `setL1BlockValuesEcotone` was properly called, forwarding the calldata to it + assertEq(_l1BlockIsthmus().baseFeeScalar(), baseFeeScalar, "base fee scalar not properly set"); + assertEq(_l1BlockIsthmus().blobBaseFeeScalar(), blobBaseFeeScalar, "blob base fee scalar not properly set"); + assertEq(_l1BlockIsthmus().sequenceNumber(), sequenceNumber, "sequence number not properly set"); + assertEq(_l1BlockIsthmus().timestamp(), timestamp, "timestamp not properly set"); + assertEq(_l1BlockIsthmus().number(), number, "number not properly set"); + assertEq(_l1BlockIsthmus().basefee(), baseFee, "base fee not properly set"); + assertEq(_l1BlockIsthmus().blobBaseFee(), blobBaseFee, "blob base fee not properly set"); + assertEq(_l1BlockIsthmus().hash(), hash, "hash not properly set"); + assertEq(_l1BlockIsthmus().batcherHash(), batcherHash, "batcher hash not properly set"); + } +} + +contract L1BlockDepositsComplete_Test is L1BlockIsthmusTest { + // @dev Tests that `depositsComplete` reverts if the caller is not the depositor. + function test_deposits_is_depositor_reverts(address _caller) external { + vm.assume(_caller != _l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().depositsComplete(); + } + + // @dev Tests that `depositsComplete` succeeds if the caller is the depositor. + function test_depositsComplete_succeeds() external { + // Set the `isDeposit` flag to true + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + + // Assert that the `isDeposit` flag was properly set to true + vm.prank(Predeploys.CROSS_L2_INBOX); + assertTrue(_l1BlockIsthmus().isDeposit()); + + // Call `depositsComplete` + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().depositsComplete(); + + // Assert that the `isDeposit` flag was properly set to false + /// @dev Assuming that `isDeposit()` wil return the proper value. That function is tested as well + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); } } diff --git a/packages/contracts-bedrock/test/L2/L1BlockIsthmus.t.sol b/packages/contracts-bedrock/test/L2/L1BlockIsthmus.t.sol new file mode 100644 index 00000000000..1c2407dd73a --- /dev/null +++ b/packages/contracts-bedrock/test/L2/L1BlockIsthmus.t.sol @@ -0,0 +1,312 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// Testing utilities +import { CommonTest } from "test/setup/CommonTest.sol"; + +// Libraries +import { StaticConfig } from "src/libraries/StaticConfig.sol"; + +// Target contract dependencies +import { L1BlockIsthmus, ConfigType } from "src/L2/L1BlockIsthmus.sol"; +import { Predeploys } from "src/libraries/Predeploys.sol"; +import "src/libraries/L1BlockErrors.sol"; + +contract L1BlockIsthmusTest is CommonTest { + event GasPayingTokenSet(address indexed token, uint8 indexed decimals, bytes32 name, bytes32 symbol); + event DependencyAdded(uint256 indexed chainId); + event DependencyRemoved(uint256 indexed chainId); + + modifier prankDepositor() { + vm.startPrank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _; + vm.stopPrank(); + } + + /// @notice Marked virtual to be overridden in + /// test/kontrol/deployment/DeploymentSummary.t.sol + function setUp() public virtual override { + super.enableInterop(); + super.setUp(); + } + + /// @dev Tests that an arbitrary chain ID can be added to the dependency set. + function testFuzz_isInDependencySet_succeeds(uint256 _chainId) public prankDepositor { + vm.assume(_chainId != block.chainid); + + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + + assertTrue(_l1BlockIsthmus().isInDependencySet(_chainId)); + } + + /// @dev Tests that `isInDependencySet` returns true when the chain's chain ID is passed as the input. + function test_isInDependencySet_chainChainId_succeeds() public view { + assertTrue(_l1BlockIsthmus().isInDependencySet(block.chainid)); + } + + /// @dev Tests that `isInDependencySet` reverts when the input chain ID is not in the dependency set + /// and is not the chain's chain ID. + function testFuzz_isInDependencySet_notDependency_reverts(uint256 _chainId) public view { + vm.assume(_chainId != block.chainid); + + // Check that the chain ID is not in the dependency set + assertFalse(_l1BlockIsthmus().isInDependencySet(_chainId)); + } + + /// @dev Tests that `isInDependencySet` returns false when the dependency set is empty. + function testFuzz_isInDependencySet_dependencySetEmpty_succeeds(uint256 _chainId) public view { + vm.assume(_chainId != block.chainid); + + assertEq(_l1BlockIsthmus().dependencySetSize(), 0); + + assertFalse(_l1BlockIsthmus().isInDependencySet(_chainId)); + } + + /// @dev Tests that the dependency set size is correct when adding an arbitrary number of chain IDs. + function testFuzz_dependencySetSize_succeeds(uint8 _dependencySetSize) public prankDepositor { + vm.assume(_dependencySetSize <= type(uint8).max); + + uint256 uniqueCount = 0; + + for (uint256 i = 0; i < _dependencySetSize; i++) { + if (i == block.chainid) continue; + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); + uniqueCount++; + } + + assertEq(_l1BlockIsthmus().dependencySetSize(), uniqueCount); + } + + /// @dev Tests that the dependency set size is correct when the dependency set is empty. + function test_dependencySetSize_dependencySetEmpty_succeeds() public view { + assertEq(_l1BlockIsthmus().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 }); + + _l1BlockIsthmus().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_gasPayingToken_notDepositor_reverts( + address _token, + uint8 _decimals, + bytes32 _name, + bytes32 _symbol + ) + public + { + vm.assume(_token != address(vm)); + + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().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); + + vm.expectEmit(address(l1Block)); + emit DependencyAdded(_chainId); + + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + } + + /// @dev Tests that adding a dependency reverts if it's the chain's chain id + function test_setConfig_addDependency_chainChainId_reverts() public prankDepositor { + vm.expectRevert(AlreadyDependency.selector); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(block.chainid)); + } + + /// @dev Tests that adding a dependency already in the set reverts + function test_setConfig_addDependency_alreadyDependency_reverts(uint256 _chainId) public prankDepositor { + vm.assume(_chainId != block.chainid); + + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + + vm.expectRevert(AlreadyDependency.selector); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + } + + /// @dev Tests that setting the add dependency config as not the depositor reverts. + function testFuzz_setConfig_addDependency_notDepositor_reverts(uint256 _chainId) public { + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + } + + /// @dev Tests that setting the add dependency config when the dependency set size is too large reverts. + function test_setConfig_addDependency_dependencySetSizeTooLarge_reverts() public prankDepositor { + for (uint256 i = 0; i < type(uint8).max; i++) { + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(i)); + } + + assertEq(_l1BlockIsthmus().dependencySetSize(), type(uint8).max); + + vm.expectRevert(DependencySetSizeTooLarge.selector); + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(1)); + } + + /// @dev Tests that the config for removing a dependency can be set. + function testFuzz_setConfig_removeDependency_succeeds(uint256 _chainId) public prankDepositor { + vm.assume(_chainId != block.chainid); + + // Add the chain ID to the dependency set before removing it + _l1BlockIsthmus().setConfig(ConfigType.ADD_DEPENDENCY, StaticConfig.encodeAddDependency(_chainId)); + + vm.expectEmit(address(l1Block)); + emit DependencyRemoved(_chainId); + + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + } + + /// @dev Tests that setting the remove dependency config as not the depositor reverts. + function testFuzz_setConfig_removeDependency_notDepositor_reverts(uint256 _chainId) public { + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + } + + /// @dev Tests that setting the remove dependency config for the chain's chain ID reverts. + function test_setConfig_removeDependency_chainChainId_reverts() public prankDepositor { + vm.expectRevert(CantRemovedDependency.selector); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(block.chainid)); + } + + /// @dev Tests that setting the remove dependency config for a chain ID that is not in the dependency set reverts. + function testFuzz_setConfig_removeDependency_notDependency_reverts(uint256 _chainId) public prankDepositor { + vm.assume(_chainId != block.chainid); + + vm.expectRevert(NotDependency.selector); + _l1BlockIsthmus().setConfig(ConfigType.REMOVE_DEPENDENCY, StaticConfig.encodeRemoveDependency(_chainId)); + } + + /// @dev Returns the L1BlockIsthmus instance. + function _l1BlockIsthmus() internal view returns (L1BlockIsthmus) { + return L1BlockIsthmus(address(l1Block)); + } +} + +contract L1BlockIsthmusIsDeposit_Test is L1BlockIsthmusTest { + /// @dev Tests that `isDeposit` reverts if the caller is not the cross L2 inbox. + function test_isDeposit_notCrossL2Inbox_reverts(address _caller) external { + vm.assume(_caller != Predeploys.CROSS_L2_INBOX); + vm.expectRevert(NotCrossL2Inbox.selector); + _l1BlockIsthmus().isDeposit(); + } + + /// @dev Tests that `isDeposit` always returns the correct value. + function test_isDeposit_succeeds() external { + // Assert is false if the value is not updated + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); + + /// @dev Assuming that `setL1BlockValuesIsthmus` will set the proper value. That function is tested as well + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + + // Assert is true if the value is updated + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), true); + } +} + +contract L1BlockIsthmusSetL1BlockValuesIsthmus_Test is L1BlockIsthmusTest { + /// @dev Tests that `setL1BlockValuesIsthmus` reverts if sender address is not the depositor + function test_setL1BlockValuesIsthmus_notDepositor_reverts(address _caller) external { + vm.assume(_caller != _l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + vm.prank(_caller); + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + } + + /// @dev Tests that `setL1BlockValuesIsthmus` succeeds if sender address is the depositor + function test_setL1BlockValuesIsthmus_succeeds( + uint32 baseFeeScalar, + uint32 blobBaseFeeScalar, + uint64 sequenceNumber, + uint64 timestamp, + uint64 number, + uint256 baseFee, + uint256 blobBaseFee, + bytes32 hash, + bytes32 batcherHash + ) + external + { + // Ensure the `isDepositTransaction` flag is false before calling `setL1BlockValuesIsthmus` + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); + + bytes memory setValuesEcotoneCalldata = abi.encodePacked( + baseFeeScalar, blobBaseFeeScalar, sequenceNumber, timestamp, number, baseFee, blobBaseFee, hash, batcherHash + ); + + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + (bool success,) = address(l1Block).call( + abi.encodePacked(L1BlockIsthmus.setL1BlockValuesIsthmus.selector, setValuesEcotoneCalldata) + ); + assertTrue(success, "function call failed"); + + // Assert that the `isDepositTransaction` flag was properly set to true + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), true); + + // Assert `setL1BlockValuesEcotone` was properly called, forwarding the calldata to it + assertEq(_l1BlockIsthmus().baseFeeScalar(), baseFeeScalar, "base fee scalar not properly set"); + assertEq(_l1BlockIsthmus().blobBaseFeeScalar(), blobBaseFeeScalar, "blob base fee scalar not properly set"); + assertEq(_l1BlockIsthmus().sequenceNumber(), sequenceNumber, "sequence number not properly set"); + assertEq(_l1BlockIsthmus().timestamp(), timestamp, "timestamp not properly set"); + assertEq(_l1BlockIsthmus().number(), number, "number not properly set"); + assertEq(_l1BlockIsthmus().basefee(), baseFee, "base fee not properly set"); + assertEq(_l1BlockIsthmus().blobBaseFee(), blobBaseFee, "blob base fee not properly set"); + assertEq(_l1BlockIsthmus().hash(), hash, "hash not properly set"); + assertEq(_l1BlockIsthmus().batcherHash(), batcherHash, "batcher hash not properly set"); + } +} + +contract L1BlockDepositsComplete_Test is L1BlockIsthmusTest { + // @dev Tests that `depositsComplete` reverts if the caller is not the depositor. + function test_deposits_is_depositor_reverts(address _caller) external { + vm.assume(_caller != _l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + vm.expectRevert(NotDepositor.selector); + _l1BlockIsthmus().depositsComplete(); + } + + // @dev Tests that `depositsComplete` succeeds if the caller is the depositor. + function test_depositsComplete_succeeds() external { + // Set the `isDeposit` flag to true + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().setL1BlockValuesIsthmus(); + + // Assert that the `isDeposit` flag was properly set to true + vm.prank(Predeploys.CROSS_L2_INBOX); + assertTrue(_l1BlockIsthmus().isDeposit()); + + // Call `depositsComplete` + vm.prank(_l1BlockIsthmus().DEPOSITOR_ACCOUNT()); + _l1BlockIsthmus().depositsComplete(); + + // Assert that the `isDeposit` flag was properly set to false + /// @dev Assuming that `isDeposit()` wil return the proper value. That function is tested as well + vm.prank(Predeploys.CROSS_L2_INBOX); + assertEq(_l1BlockIsthmus().isDeposit(), false); + } +} diff --git a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol index 4de72c87257..15e806d3e52 100644 --- a/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol +++ b/packages/contracts-bedrock/test/universal/ProxyAdmin.t.sol @@ -164,9 +164,8 @@ contract ProxyAdmin_Test is Test { admin.getProxyAdmin(_proxy); } else if (proxyType == ProxyAdmin.ProxyType.RESOLVED) { // Just an empty block to show that all cases are covered - } else { - vm.expectRevert("ProxyAdmin: unknown proxy type"); } + else vm.expectRevert("ProxyAdmin: unknown proxy type"); // Call the proxy contract directly to get the admin. // Different proxy types have different interfaces.