-
Notifications
You must be signed in to change notification settings - Fork 718
Tx Filtering additional system tests #4503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,4 +123,47 @@ contract AddressFilterTest { | |
| ) external { | ||
| emit UnfilteredEvent(some); | ||
| } | ||
|
|
||
| /// @notice Makes a DELEGATECALL to the target address | ||
| function delegatecallTarget( | ||
| address target | ||
| ) external returns (bool success) { | ||
| (success,) = target.delegatecall(""); | ||
| emit CallResult(success); | ||
| } | ||
|
|
||
| /// @notice Makes a CALLCODE to the target address (requires inline assembly) | ||
| function callcodeTarget( | ||
| address target | ||
| ) external returns (bool success) { | ||
| assembly { | ||
| success := callcode(gas(), target, 0, 0, 0, 0, 0) | ||
| } | ||
| emit CallResult(success); | ||
| } | ||
|
|
||
| /// @notice Calls an intermediary contract's callTarget function with the final target. | ||
| /// This creates a two-hop call chain: this -> intermediary.callTarget(target) | ||
| function callVia(address intermediary, address target) external returns (bool success) { | ||
| bytes memory data = abi.encodeWithSignature("callTarget(address)", target); | ||
| (success,) = intermediary.call(data); | ||
| emit CallResult(success); | ||
| } | ||
|
|
||
| /// @notice Accept ETH transfers | ||
| receive() external payable {} | ||
|
|
||
| /// @notice Send this contract's entire balance to the target address | ||
| function payTo(address payable target) external { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't |
||
| (bool success,) = target.call{value: address(this).balance}(""); | ||
| require(success, "payTo failed"); | ||
| } | ||
|
|
||
| /// @notice Forward msg.value to intermediary and have it pay the final target. | ||
| /// This creates a two-hop payment: caller -> intermediary -> target | ||
| function payVia(address intermediary, address payable target) external payable { | ||
| bytes memory data = abi.encodeWithSignature("payTo(address)", target); | ||
| (bool success,) = intermediary.call{value: msg.value}(data); | ||
| require(success, "payVia failed"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -393,6 +393,279 @@ func TestAddressFilterSelfdestructOnConstruct(t *testing.T) { | |
| Require(t, err) | ||
| } | ||
|
|
||
| func TestAddressFilterIndirectPayment(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test seems very valuable, but it fails now. payTo should be payable — right now the intermediary reverts before the filtered address is ever reached. There could also be a deeper gap in the filter (value transfers to filtered EOAs via internal CALLs may not trigger TouchAddress), but probably worth merging latest master first to investigate |
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Deploy payer contract | ||
| _, payer := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Deploy intermediary contract and fund it so it can forward payments | ||
| intermediaryAddr, _ := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Create filtered destination | ||
| builder.L2Info.GenerateAccount("FilteredDest") | ||
| filteredAddr := builder.L2Info.GetAddress("FilteredDest") | ||
|
|
||
| // Create clean destination for the positive test | ||
| builder.L2Info.GenerateAccount("CleanDest") | ||
| cleanAddr := builder.L2Info.GetAddress("CleanDest") | ||
|
|
||
| // Set up filter to block FilteredDest | ||
| filter := newHashedChecker([]common.Address{filteredAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // Test 1: Indirect payment payer -> intermediary -> filtered address should fail | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| auth.Value = big.NewInt(1e15) | ||
| _, err := payer.PayVia(&auth, intermediaryAddr, filteredAddr) | ||
| if err == nil { | ||
| t.Fatal("expected indirect payment to filtered address to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
|
|
||
| // Test 2: Indirect payment payer -> intermediary -> clean address should succeed | ||
| auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| auth.Value = big.NewInt(1e15) | ||
| tx, err := payer.PayVia(&auth, intermediaryAddr, cleanAddr) | ||
| Require(t, err) | ||
| _, err = builder.L2.EnsureTxSucceeded(tx) | ||
| Require(t, err) | ||
|
|
||
| // Verify the clean destination received the funds | ||
| balance := builder.L2.GetBalance(t, cleanAddr) | ||
| if balance.Cmp(big.NewInt(1e15)) != 0 { | ||
| t.Fatalf("expected clean destination balance of 1e15, got %s", balance.String()) | ||
| } | ||
| } | ||
|
|
||
| // TestAddressFilterDelegateCall verifies that DELEGATECALL to a filtered address | ||
| // does NOT trigger filtering. DELEGATECALL loads code from the target but executes | ||
| // in the caller's context, so the target address is never "entered" from the | ||
| // filter's perspective (PushContract sees caller, not the code source). | ||
| func TestAddressFilterDelegateCall(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Deploy caller contract (not filtered) | ||
| _, caller := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Deploy target contract (will be filtered) | ||
| targetAddr, _ := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Set up filter to block the target contract | ||
| filter := newHashedChecker([]common.Address{targetAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // DELEGATECALL to filtered address should succeed because the target is | ||
| // only a code source - execution stays in the caller's context. | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| tx, err := caller.DelegatecallTarget(&auth, targetAddr) | ||
| Require(t, err) | ||
| _, err = builder.L2.EnsureTxSucceeded(tx) | ||
| Require(t, err) | ||
|
|
||
| // Sanity check: a regular CALL to the same filtered address should still fail | ||
| auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| _, err = caller.CallTarget(&auth, targetAddr) | ||
| if err == nil { | ||
| t.Fatal("expected CALL to filtered address to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // TestAddressFilterCallCode verifies that CALLCODE to a filtered address | ||
| // does NOT trigger filtering, for the same reason as DELEGATECALL: the target | ||
| // address is only used as a code source. | ||
| func TestAddressFilterCallCode(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Deploy caller contract (not filtered) | ||
| _, caller := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Deploy target contract (will be filtered) | ||
| targetAddr, _ := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Set up filter to block the target contract | ||
| filter := newHashedChecker([]common.Address{targetAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // CALLCODE to filtered address should succeed | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| tx, err := caller.CallcodeTarget(&auth, targetAddr) | ||
| Require(t, err) | ||
| _, err = builder.L2.EnsureTxSucceeded(tx) | ||
| Require(t, err) | ||
| } | ||
|
|
||
| // TestAddressFilterCallViaFilteredIntermediary verifies that when a non-filtered | ||
| // contract CALLs a filtered intermediary, the transaction is rejected even though | ||
| // the final target is not filtered. | ||
| func TestAddressFilterCallViaFilteredIntermediary(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seem to be similar to existing |
||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Deploy payer contract (not filtered) | ||
| _, payer := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Deploy intermediary contract (will be filtered) | ||
| intermediaryAddr, _ := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Deploy final target contract (not filtered) | ||
| targetAddr, _ := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Filter only the intermediary | ||
| filter := newHashedChecker([]common.Address{intermediaryAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // Test: payer -> filtered intermediary -> target should fail at the intermediary | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| _, err := payer.CallVia(&auth, intermediaryAddr, targetAddr) | ||
| if err == nil { | ||
| t.Fatal("expected call via filtered intermediary to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
| } | ||
|
|
||
| // TestAddressFilterContractDeploy verifies that deploying a contract from an EOA | ||
| // is rejected when the resulting contract address is filtered. | ||
| func TestAddressFilterContractDeploy(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Fund a deployer account | ||
| builder.L2Info.GenerateAccount("Deployer") | ||
| builder.L2.TransferBalance(t, "Owner", "Deployer", big.NewInt(1e18), builder.L2Info) | ||
|
|
||
| // Compute the address that the next deployment from Deployer will create | ||
| deployerAddr := builder.L2Info.GetAddress("Deployer") | ||
| nonce, err := builder.L2.Client.NonceAt(ctx, deployerAddr, nil) | ||
| Require(t, err) | ||
| futureAddr := crypto.CreateAddress(deployerAddr, nonce) | ||
|
|
||
| // Filter that future contract address | ||
| filter := newHashedChecker([]common.Address{futureAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // Deploy a contract (tx with no To address) - should be rejected | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Deployer", ctx) | ||
| _, _, _, err = localgen.DeployAddressFilterTest(&auth, builder.L2.Client) | ||
| if err == nil { | ||
| t.Fatal("expected deployment to filtered address to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
|
|
||
| // Clear filter and verify deployment succeeds (nonce didn't increment, same address) | ||
| emptyChecker := newHashedChecker([]common.Address{}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(emptyChecker) | ||
|
|
||
| auth = builder.L2Info.GetDefaultTransactOpts("Deployer", ctx) | ||
| _, tx, _, err := localgen.DeployAddressFilterTest(&auth, builder.L2.Client) | ||
| Require(t, err) | ||
| _, err = builder.L2.EnsureTxSucceeded(tx) | ||
| Require(t, err) | ||
| } | ||
|
|
||
| func TestAddressFilterEventBypassRule(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| transferEvent := "Transfer(address,address,uint256)" | ||
| selector, _, err := eventfilter.CanonicalSelectorFromEvent(transferEvent) | ||
| Require(t, err) | ||
|
|
||
| // Create rules with bypass: skip filtering when topic[1] (from) matches bypassAddr | ||
| bypassAddr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") | ||
| rules := []eventfilter.EventRule{ | ||
| { | ||
| Event: transferEvent, | ||
| Selector: selector, | ||
| TopicAddresses: []int{1, 2}, | ||
| Bypass: &eventfilter.BypassRule{ | ||
| TopicIndex: 1, | ||
| Equals: bypassAddr, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| builder := NewNodeBuilder(ctx).DefaultConfig(t, false).WithEventFilterRules(rules) | ||
| builder.isSequencer = true | ||
| cleanup := builder.Build(t) | ||
| defer cleanup() | ||
|
|
||
| // Deploy test contract | ||
| _, contract := deployAddressFilterTestContract(t, ctx, builder) | ||
|
|
||
| // Create a filtered address | ||
| builder.L2Info.GenerateAccount("FilteredUser") | ||
| filteredAddr := builder.L2Info.GetAddress("FilteredUser") | ||
|
|
||
| filter := newHashedChecker([]common.Address{filteredAddr}) | ||
| builder.L2.ExecNode.ExecEngine.SetAddressChecker(filter) | ||
|
|
||
| // Test 1: Transfer from random address to filtered address should be rejected | ||
| auth := builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| _, err = contract.EmitTransfer(&auth, auth.From, filteredAddr) | ||
| if err == nil { | ||
| t.Fatal("expected Transfer to filtered address to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
|
|
||
| // Test 2: Transfer FROM the bypass address TO the filtered address should succeed | ||
| // because the bypass rule skips filtering when from == bypassAddr | ||
| auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| tx, err := contract.EmitTransfer(&auth, bypassAddr, filteredAddr) | ||
| Require(t, err) | ||
| _, err = builder.L2.EnsureTxSucceeded(tx) | ||
| Require(t, err) | ||
|
|
||
| // Test 3: Transfer FROM filtered address (not bypass) should still be rejected | ||
| auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) | ||
| _, err = contract.EmitTransfer(&auth, filteredAddr, auth.From) | ||
| if err == nil { | ||
| t.Fatal("expected Transfer from filtered address to be rejected") | ||
| } | ||
| if !isFilteredError(err) { | ||
| t.Fatalf("expected filtered error, got: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestAddressFilterWithFilteredEvents(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as i know, CALLCODE is considered deprecated, I'm not sure we need to support it