Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions sepolia/2026-06-23-transfer-solana-bridge-ownership/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
BASE_CONTRACTS_COMMIT=be7c7a642e430fa64b04b63203839f8c81f48466

# Signer: must be an owner EOA of OWNER_SAFE. Facilitator sets this to their own signer
# address before running `make gen-validation`. Default is the first owner of OWNER_SAFE.
SENDER=0x6e427c3212C0b63BE0C382F97715D49b011bFF33

# Old CB L1 multisig (issuer; current owner via its L2 alias).
OWNER_SAFE=0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f

# New CB L1 multisig (new owner). The script derives its L2 alias via applyL1ToL2Alias.
NEW_OWNER_SAFE=0x646132A1667ca7aD00d36616AFBA1A28116C770A

# OptimismPortal2 on Sepolia L1.
OPTIMISM_PORTAL_ADDR=0x49f53e41452C74589E85cA1677426Ba426459e85

# Shared Solady ERC1967Factory (holds the proxy admins). Same address on every chain.
ERC1967_FACTORY_ADDR=0x0000000000006396FF2a80c067f99B3d2Ab4Df24

# Target contracts on Base Sepolia L2.
BRIDGE_ADDR=0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B
TWIN_BEACON_ADDR=0x11bF22cFf007C46C725Dc59A919383326E3cdefB
CROSS_CHAIN_ERC20_BEACON_ADDR=0xc039781ccb3cb281f69f8509bfb17163993dd6d1
CROSS_CHAIN_ERC20_FACTORY_ADDR=0x488EB7F7cb2568e31595D48cb26F63963Cc7565D
BRIDGE_VALIDATOR_ADDR=0x863Bed3E344035253CC44C75612Ad5fDF5904aEE
RELAYER_ORCHESTRATOR_ADDR=0x1e0842b2E6FA06A59b05a9c1d36a6480730012CE

# Gas limit for each L2 deposit transaction.
L2_GAS_LIMIT=100000

# Required for the task signer tool.
RECORD_STATE_DIFF=true
86 changes: 86 additions & 0 deletions sepolia/2026-06-23-transfer-solana-bridge-ownership/FACILITATOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Facilitator Guide

Guide for facilitators managing the Base Sepolia Solana bridge owner transfer.

## 1. Generate the validation file

Run this after any change to [.env](./.env) or
[script/TransferSolanaBridgeOwnership.s.sol](./script/TransferSolanaBridgeOwnership.s.sol).

```bash
cd contract-deployments
git pull
cd sepolia/2026-06-23-transfer-solana-bridge-ownership
make deps
make gen-validation
```

This produces `validations/base-signer.json`. Check that the `cmd` field uses:

```text
--sender 0x6e427c3212C0b63BE0C382F97715D49b011bFF33
```

(or whichever owner of `OWNER_SAFE` is set as `SENDER` in `.env`).

### Disable task-origin validation

This task does not ship task-origin signatures. After generating the validation
file, ensure `validations/base-signer.json` carries the following field at the JSON
root (add it if the signer-tool did not emit it automatically):

```json
"skipTaskOriginValidation": true
```

Commit the file only after that field is set; otherwise signers' UI will demand
task-origin attestations that do not exist for this task.

## 2. Collect signatures

Ask signers to follow [README.md](./README.md). They run `make sign-task` from the repo root and
select `sepolia/2026-06-23-transfer-solana-bridge-ownership` in the signing UI. The old multisig
has a threshold of 3, so collect at least 3 signatures.

## 3. Execute

This task uses direct signatures from `OWNER_SAFE`, so there are no separate nested Safe approval
transactions. Concatenate the collected signatures and pass them to `make execute`.

```bash
cd contract-deployments
git pull
cd sepolia/2026-06-23-transfer-solana-bridge-ownership
make deps
SIGNATURES=AAABBBCCC make execute
```

Replace `AAABBBCCC` with the concatenated signatures collected from signers.

## 4. Verify on-chain

After the L1 deposits are relayed to L2, verify on Base Sepolia that every contract is now controlled
by the new owner alias `0x757232A1667ca7aD00d36616AFBA1A28116C881B`:

```bash
export RPC=https://sepolia.base.org
export FACTORY=0x0000000000006396FF2a80c067f99B3d2Ab4Df24

# Bridge: both the functional owner and the proxy admin must move.
cast call 0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B "owner()(address)" --rpc-url $RPC
cast call $FACTORY "adminOf(address)(address)" 0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B --rpc-url $RPC

# Beacons.
cast call 0x11bF22cFf007C46C725Dc59A919383326E3cdefB "owner()(address)" --rpc-url $RPC
cast call 0xc039781ccb3cb281f69f8509bfb17163993dd6d1 "owner()(address)" --rpc-url $RPC

# Proxies (admin held in the factory).
cast call $FACTORY "adminOf(address)(address)" 0x488EB7F7cb2568e31595D48cb26F63963Cc7565D --rpc-url $RPC
cast call $FACTORY "adminOf(address)(address)" 0x863Bed3E344035253CC44C75612Ad5fDF5904aEE --rpc-url $RPC
cast call $FACTORY "adminOf(address)(address)" 0x1e0842b2E6FA06A59b05a9c1d36a6480730012CE --rpc-url $RPC
```

Each call must return `0x757232A1667ca7aD00d36616AFBA1A28116C881B`.

Then update [README.md](./README.md) status to `EXECUTED` with the transaction link and check in any
generated execution records.
15 changes: 15 additions & 0 deletions sepolia/2026-06-23-transfer-solana-bridge-ownership/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
include ../../Makefile
include ../../Multisig.mk
include ../.env
include .env

RPC_URL = $(L1_RPC_URL)
SCRIPT_NAME = TransferSolanaBridgeOwnership

.PHONY: gen-validation
gen-validation: deps-signer-tool
$(call GEN_VALIDATION,$(SCRIPT_NAME),,$(SENDER),base-signer.json,)

.PHONY: execute
execute:
$(call MULTISIG_EXECUTE,$(SIGNATURES))
57 changes: 57 additions & 0 deletions sepolia/2026-06-23-transfer-solana-bridge-ownership/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Transfer Base Sepolia Solana Bridge Owner

Status: [EXECUTED](https://sepolia.etherscan.io/tx/0xe7e9efe67323840d61bfd4517f3604c0ccde0b1a1f99f305f507c5934593e486)

## Description

This task transfers control of the Base Sepolia Solana bridge contracts from the aliased old
Coinbase L1 multisig to the aliased new Coinbase L1 multisig.

Superchain separation migrated Coinbase upgrade signers to a new multisig address, but these bridge
contracts are still controlled by the old multisig's L2 alias.

| Role | L1 address | L2 alias |
| -- | -- | -- |
| Current owner | [`0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f`](https://sepolia.etherscan.io/address/0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f) | [`0x6f0fB066334B67355A15dc9b67317fD2a2e20890`](https://sepolia.basescan.org/address/0x6f0fB066334B67355A15dc9b67317fD2a2e20890) |
| New owner | [`0x646132A1667ca7aD00d36616AFBA1A28116C770A`](https://sepolia.etherscan.io/address/0x646132A1667ca7aD00d36616AFBA1A28116C770A) | [`0x757232A1667ca7aD00d36616AFBA1A28116C881B`](https://sepolia.basescan.org/address/0x757232A1667ca7aD00d36616AFBA1A28116C881B) |

The calls are executed from Sepolia L1 through `OptimismPortal2`, which makes the old L1 multisig's
aliased address transfer ownership of each contract on Base Sepolia. Two ownership mechanisms are
involved:

| Contract | Address | Mechanism |
| -- | -- | -- |
| Bridge | [`0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B`](https://sepolia.basescan.org/address/0x01824a90d32A69022DdAEcC6C5C14Ed08dB4EB9B) | `transferOwnership` + `ERC1967Factory.changeAdmin` |
| TwinBeacon | [`0x11bF22cFf007C46C725Dc59A919383326E3cdefB`](https://sepolia.basescan.org/address/0x11bF22cFf007C46C725Dc59A919383326E3cdefB) | `transferOwnership` |
| CrossChainERC20Beacon | [`0xc039781ccb3cb281f69f8509bfb17163993dd6d1`](https://sepolia.basescan.org/address/0xc039781ccb3cb281f69f8509bfb17163993dd6d1) | `transferOwnership` |
| CrossChainERC20Factory | [`0x488EB7F7cb2568e31595D48cb26F63963Cc7565D`](https://sepolia.basescan.org/address/0x488EB7F7cb2568e31595D48cb26F63963Cc7565D) | `ERC1967Factory.changeAdmin` |
| BridgeValidator | [`0x863Bed3E344035253CC44C75612Ad5fDF5904aEE`](https://sepolia.basescan.org/address/0x863Bed3E344035253CC44C75612Ad5fDF5904aEE) | `ERC1967Factory.changeAdmin` |
| RelayerOrchestrator | [`0x1e0842b2E6FA06A59b05a9c1d36a6480730012CE`](https://sepolia.basescan.org/address/0x1e0842b2E6FA06A59b05a9c1d36a6480730012CE) | `ERC1967Factory.changeAdmin` |

## Approving the transaction

### 1. Update repo

```bash
cd contract-deployments
git pull
```

### 2. Run the signing tool

Run this command from the repo root. Do not enter the task directory.

```bash
make sign-task
```

### 3. Sign

Open [http://localhost:3000](http://localhost:3000) and select:

```text
sepolia/2026-06-23-transfer-solana-bridge-ownership
```

After signing, copy the signature and send it to the facilitator. You may then close the signer
tool with `Ctrl + C`.
22 changes: 22 additions & 0 deletions sepolia/2026-06-23-transfer-solana-bridge-ownership/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
broadcast = 'records'
fs_permissions = [{ access = "read-write", path = "./" }]
optimizer = true
optimizer_runs = 999999
solc_version = "0.8.15"
via-ir = false
evm_version = "shanghai"
remappings = [
'@base-contracts/=lib/contracts',
'@lib-keccak/=lib/lib-keccak/contracts/lib/',
'@solady/=lib/solady/src/',
'src/=lib/contracts/src/'
]

[lint]
lint_on_build = false

# See more config options https://github.com/foundry-rs/foundry/tree/master/config

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import {Vm} from "forge-std/Vm.sol";

import {MultisigScript, Enum} from "@base-contracts/script/universal/MultisigScript.sol";
import {Simulation} from "@base-contracts/script/universal/Simulation.sol";
import {IOptimismPortal2} from "@base-contracts/interfaces/L1/IOptimismPortal2.sol";
import {AddressAliasHelper} from "@base-contracts/src/vendor/AddressAliasHelper.sol";

/// @notice Minimal interface for Solady Ownable / OwnableRoles / UpgradeableBeacon ownership transfer.
interface IOwnable {
function transferOwnership(address newOwner) external;
}

/// @notice Minimal interface for the shared Solady ERC1967Factory proxy-admin transfer.
interface IERC1967Factory {
function changeAdmin(address proxy, address admin) external;
}

/// @title TransferSolanaBridgeOwnership
/// @notice Transfers ownership/admin of the Base Sepolia Solana bridge contracts from the alias of
/// the old Coinbase L1 multisig to the alias of the new Coinbase L1 multisig.
contract TransferSolanaBridgeOwnership is MultisigScript {
using AddressAliasHelper for address;

// Task config from .env.
address internal ownerSafeEnv;
address internal newOwnerSafeEnv;
address internal optimismPortalEnv;
address internal erc1967FactoryEnv;
address internal bridgeEnv;
address internal twinBeaconEnv;
address internal crossChainErc20BeaconEnv;
address internal crossChainErc20FactoryEnv;
address internal bridgeValidatorEnv;
address internal relayerOrchestratorEnv;
uint64 internal l2GasLimitEnv;

function setUp() public {
ownerSafeEnv = vm.envAddress("OWNER_SAFE");
newOwnerSafeEnv = vm.envAddress("NEW_OWNER_SAFE");
optimismPortalEnv = vm.envAddress("OPTIMISM_PORTAL_ADDR");
erc1967FactoryEnv = vm.envAddress("ERC1967_FACTORY_ADDR");
bridgeEnv = vm.envAddress("BRIDGE_ADDR");
twinBeaconEnv = vm.envAddress("TWIN_BEACON_ADDR");
crossChainErc20BeaconEnv = vm.envAddress("CROSS_CHAIN_ERC20_BEACON_ADDR");
crossChainErc20FactoryEnv = vm.envAddress("CROSS_CHAIN_ERC20_FACTORY_ADDR");
bridgeValidatorEnv = vm.envAddress("BRIDGE_VALIDATOR_ADDR");
relayerOrchestratorEnv = vm.envAddress("RELAYER_ORCHESTRATOR_ADDR");

uint256 gasLimit = vm.envUint("L2_GAS_LIMIT");
require(gasLimit <= type(uint64).max, "TransferSolanaBridgeOwnership: L2_GAS_LIMIT too large");
l2GasLimitEnv = uint64(gasLimit);
}

/// @notice Post-check is a no-op because the L1 simulation cannot verify post-deposit L2 state.
function _postCheck(Vm.AccountAccess[] memory, Simulation.Payload memory) internal override {}

/// @notice Builds the L1 deposit transactions that transfer ownership of each bridge contract on L2.
/// @dev Each deposit targets the contract (or the ERC1967Factory) directly rather than routing
/// through an L2 CBMulticall, so that the L2 msg.sender is the owner safe's alias and the
/// onlyOwner / adminOf checks pass.
function _buildCalls() internal view override returns (Call[] memory) {
Call[] memory calls = new Call[](7);

address newOwnerAlias = newOwnerSafeEnv.applyL1ToL2Alias();
bytes memory transferOwnerCalldata = abi.encodeCall(IOwnable.transferOwnership, (newOwnerAlias));

// Bridge: OwnableRoles functional owner + ERC1967 proxy admin (both currently the old alias).
calls[0] = _deposit({l2Target: bridgeEnv, l2Calldata: transferOwnerCalldata});
calls[1] = _deposit({
l2Target: erc1967FactoryEnv,
l2Calldata: abi.encodeCall(IERC1967Factory.changeAdmin, (bridgeEnv, newOwnerAlias))
});

// Beacons: Solady UpgradeableBeacon owner.
calls[2] = _deposit({l2Target: twinBeaconEnv, l2Calldata: transferOwnerCalldata});
calls[3] = _deposit({l2Target: crossChainErc20BeaconEnv, l2Calldata: transferOwnerCalldata});

// Proxies with no Ownable owner: ERC1967 proxy admin only.
calls[4] = _deposit({
l2Target: erc1967FactoryEnv,
l2Calldata: abi.encodeCall(IERC1967Factory.changeAdmin, (crossChainErc20FactoryEnv, newOwnerAlias))
});
calls[5] = _deposit({
l2Target: erc1967FactoryEnv,
l2Calldata: abi.encodeCall(IERC1967Factory.changeAdmin, (bridgeValidatorEnv, newOwnerAlias))
});
calls[6] = _deposit({
l2Target: erc1967FactoryEnv,
l2Calldata: abi.encodeCall(IERC1967Factory.changeAdmin, (relayerOrchestratorEnv, newOwnerAlias))
});

return calls;
}

/// @notice Wraps an L2 call into an L1 OptimismPortal deposit transaction.
function _deposit(address l2Target, bytes memory l2Calldata) internal view returns (Call memory) {
return Call({
operation: Enum.Operation.Call,
target: optimismPortalEnv,
data: abi.encodeCall(IOptimismPortal2.depositTransaction, (l2Target, 0, l2GasLimitEnv, false, l2Calldata)),
value: 0
});
}

function _ownerSafe() internal view override returns (address) {
return ownerSafeEnv;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"cmd": "mise exec -- forge script --rpc-url https://ethereum-full-sepolia-k8s-dev.cbhq.net TransferSolanaBridgeOwnership --sig sign(address[]) [] --sender 0x6e427c3212C0b63BE0C382F97715D49b011bFF33",
"ledgerId": 1,
"rpcUrl": "https://ethereum-full-sepolia-k8s-dev.cbhq.net",
"expectedDomainAndMessageHashes": {
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"domainHash": "0x0127bbb910536860a0757a9c0ffcdf9e4452220f566ed83af1f27f9e833f0e23",
"messageHash": "0xbfd48921a63fbdeb2f13a0ed969eded82ed5f39f93a1ef1da59a3df68ef6aa2a"
},
"stateOverrides": [
{
"name": "CB Signer Safe - Sepolia",
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"overrides": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000004",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Override the threshold to 1 so the transaction simulation can occur.",
"allowDifference": false
},
{
"key": "0x6583b5c8426e42b454adeb4ecf58ab89a2433b8764fccccc72647ffdc55eb78e",
"value": "0x0000000000000000000000000000000000000000000000000000000000000001",
"description": "Simulates an approval from msg.sender in order for the task simulation to succeed.",
"allowDifference": false
}
]
}
],
"stateChanges": [
{
"name": "OptimismPortal - Sepolia",
"address": "0x49f53e41452C74589E85cA1677426Ba426459e85",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000001",
"before": "0x0000000000a9b9120000000000143e110000000000000000000000003b9aca00",
"after": "0x0000000000a9b91d00000000000aae600000000000000000000000003b9aca00",
"description": "Updates the ResourceMetering parameters (prevBlockNum, prevBoughtGas, prevBaseFee).",
"allowDifference": true
}
]
},
{
"name": "CB Signer Safe - Sepolia",
"address": "0x5dfEB066334B67355A15dc9b67317fD2a2e1f77f",
"changes": [
{
"key": "0x0000000000000000000000000000000000000000000000000000000000000005",
"before": "0x0000000000000000000000000000000000000000000000000000000000000018",
"after": "0x0000000000000000000000000000000000000000000000000000000000000019",
"description": "Increments the nonce",
"allowDifference": false
}
]
}
],
"balanceChanges": [],
"skipTaskOriginValidation": true
}
Loading