Skip to content

Commit

Permalink
Add contract to migrate a Safe from not L2 to L2
Browse files Browse the repository at this point in the history
  • Loading branch information
BinaryBard-0604 committed Oct 24, 2023
1 parent 9baf315 commit e476df3
Show file tree
Hide file tree
Showing 2 changed files with 326 additions and 0 deletions.
131 changes: 131 additions & 0 deletions contracts/libraries/SafeToL2Migration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: LGPL-3.0-only
/* solhint-disable one-contract-per-file */
pragma solidity >=0.7.0 <0.9.0;

import {SafeStorage} from "../libraries/SafeStorage.sol";
import {Enum} from "../common/Enum.sol";

interface ISafe {
function VERSION() external view returns (string memory);
}



/**
* @title Migration Contract for updating a Safe from 1.3.0/1.4.1 version to a L2 version. Useful when replaying a Safe from a non L2 network in a L2 network.
* @notice This contract facilitates the migration of a Safe contract from version 1.3.0 to 1.3.0L2 or from 1.4.1 to 1.4.1L2
* Older versions are not supported
* @dev IMPORTANT: The migration will only work with proxies that store the implementation address in the storage slot 0.
*/
contract SafeToL2Migration is SafeStorage {
// Address of this contract
address public immutable MIGRATION_SINGLETON;

/**
* @notice Constructor
* @dev Initializes the migrationSingleton with the contract's own address.
*/
constructor() {
MIGRATION_SINGLETON = address(this);
}

/**
* @notice Event indicating a change of master copy address.
* @param singleton New master copy address
*/
event ChangedMasterCopy(address singleton);

event SafeMultiSigTransaction(
address to,
uint256 value,
bytes data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes signatures,
// We combine nonce, sender and threshold into one to avoid stack too deep
// Dev note: additionalInfo should not contain `bytes`, as this complicates decoding
bytes additionalInfo
);

/**
* @notice Migrate from Safe 1.3.0/1.4.1 Singleton (L1) to the same version provided L2 singleton
* @dev This function should only be called via a delegatecall to perform the upgrade.
*/
function migrateToL2(address l2Singleton) public {
require(address(this) != MIGRATION_SINGLETON, "Migration should only be called via delegatecall");
require(address(singleton) != l2Singleton, "Safe is already using the singleton");
// Nonce is increased before executing a tx, so first executed tx will have nonce=1
require(nonce == 1, "Safe must have not executed any tx");
bytes32 oldSingletonVersion = keccak256(abi.encodePacked(ISafe(singleton).VERSION()));
bytes32 newSingletonVersion = keccak256(abi.encodePacked(ISafe(l2Singleton).VERSION()));

require(oldSingletonVersion == newSingletonVersion, "L2 singleton must match current version singleton");
// There's no way to make sure if address is a valid singleton, unless we cofigure the contract for every chain
require(newSingletonVersion == keccak256(abi.encodePacked("1.3.0")) || newSingletonVersion == keccak256(abi.encodePacked("1.4.1")), "Provided singleton version is not supported");

singleton = l2Singleton;

// Simulate a L2 transaction so indexer picks up the Safe
// 0xef2624ae - keccack("migrateToL2(address)")
bytes memory data = abi.encodeWithSelector(0xef2624ae, l2Singleton);
bytes memory additionalInfo;
{
// nonce, sender, threshold
additionalInfo = abi.encode(nonce, msg.sender, threshold);
}
emit SafeMultiSigTransaction(
MIGRATION_SINGLETON,
0,
data,
Enum.Operation.DelegateCall,
0,
0,
0,
address(0),
address(0),
"", // We cannot detect signatures
additionalInfo
);
emit ChangedMasterCopy(singleton);
}

/**
* @notice Checks whether an Ethereum address corresponds to a contract or an externally owned account (EOA).
*
* @param account The Ethereum address to be checked.
*
* @return A boolean value indicating whether the address is associated with a contract (true) or an EOA (false).
*
* @dev This function relies on the `extcodesize` assembly opcode to determine whether an address is a contract.
* It may return incorrect results in some edge cases:
*
* - During the contract deployment process, including the constructor, this function may incorrectly identify the
* contract's own address as an EOA, as the code is not yet deployed.
*
* - If a contract performs a self-destruct operation (using `selfdestruct`) after deployment, this function may
* incorrectly identify the address as an EOA once the contract is destroyed, as its code will be removed.
*
* - When interacting with external contracts that use delegatecall or other mechanisms to execute code from
* different contracts, this function may not accurately distinguish between a contract and an EOA, as it only
* checks the code size at the specified address.
*
* - Contracts that are created using the CREATE2 opcode may not be accurately identified as contracts by this
* function, especially if the code is not deployed until after the creation.
*
* Developers should use caution when relying on the results of this function for critical decision-making.
*/
function isContract(address account) internal view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly {
size := extcodesize(account)
}

// If the code size is greater than 0, it is a contract; otherwise, it is an EOA.
return size > 0;
}
}
195 changes: 195 additions & 0 deletions test/libraries/SafeToL2Migration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { expect } from "chai";
import hre, { ethers, deployments } from "hardhat";
import { AddressZero } from "@ethersproject/constants";
import { getSafeWithSingleton, migrationContractTo150, getSafeSingletonAt, getMock } from "../utils/setup";
import deploymentData from "../json/safeDeployment.json";
import safeRuntimeBytecode from "../json/safeRuntimeBytecode.json";
import { buildSafeTransaction, executeContractCallWithSigners, executeTxWithSigners } from "../../src/utils/execution";

const SAFE_SINGLETON_141_ADDRESS = "0x3E5c63644E683549055b9Be8653de26E0B4CD36E";

const SAFE_SINGLETON_141_L2_ADDRESS = "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA";

const SAFE_SINGLETON_150_ADDRESS = "0x88627c8904eCd9DF96A572Ef32A7ff13b199Ed8D";

const SAFE_SINGLETON_150_L2_ADDRESS = "0x0Ee37514644683f7EB9745a5726C722DeBa77e52";

const FALLBACK_HANDLER_STORAGE_SLOT = "0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5";

const GUARD_STORAGE_SLOT = "0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8";

describe("SafeToL2Migration library", () => {
const migratedInterface = new ethers.Interface(["function masterCopy() view returns(address)"]);

const setupTests = deployments.createFixture(async ({ deployments }) => {
await deployments.fixture();

// Set the runtime code for hardcoded addresses, so the expected events are emitted
await hre.network.provider.send("hardhat_setCode", [SAFE_SINGLETON_141_ADDRESS, safeRuntimeBytecode.safe141]);
await hre.network.provider.send("hardhat_setCode", [SAFE_SINGLETON_141_L2_ADDRESS, safeRuntimeBytecode.safe141l2]);
await hre.network.provider.send("hardhat_setCode", [SAFE_SINGLETON_150_ADDRESS, safeRuntimeBytecode.safe150]);
await hre.network.provider.send("hardhat_setCode", [SAFE_SINGLETON_150_L2_ADDRESS, safeRuntimeBytecode.safe150l2]);

const signers = await ethers.getSigners();
const [user1] = signers;
const singleton130Address = (await (await user1.sendTransaction({ data: deploymentData.safe130 })).wait())?.contractAddress;
const singleton130L2Address = (await (await user1.sendTransaction({ data: deploymentData.safe130l2 })).wait())?.contractAddress;

if (!singleton130Address || !singleton130L2Address) {
throw new Error("Could not deploy Safe130 or Safe130L2");
}
const singleton130 = await getSafeSingletonAt(singleton130Address);
const singleton130L2 = await getSafeSingletonAt(singleton130L2Address);

const guardContract = await hre.ethers.getContractAt("Guard", AddressZero);
const guardEip165Calldata = guardContract.interface.encodeFunctionData("supportsInterface", ["0x945b8148"]);
const validGuardMock = await getMock();
await validGuardMock.givenCalldataReturnBool(guardEip165Calldata, true);

const invalidGuardMock = await getMock();
await invalidGuardMock.givenCalldataReturnBool(guardEip165Calldata, false);

const safeWith1967Proxy = await getSafeSingletonAt(
await hre.ethers
.getContractFactory("UpgradeableProxy")
.then((factory) =>
factory.deploy(
singleton130Address,
singleton130.interface.encodeFunctionData("setup", [
[user1.address],
1,
AddressZero,
"0x",
AddressZero,
AddressZero,
0,
AddressZero,
]),
),
)
.then((proxy) => proxy.getAddress()),
);
const safeToL2MigrationContract = await hre.ethers.getContractFactory("SafeToL2Migration");
const migration = await safeToL2MigrationContract.deploy();
return {
safe130: await getSafeWithSingleton(singleton130, [user1.address]),
safe130l2: await getSafeWithSingleton(singleton130L2, [user1.address]),
safeWith1967Proxy,
migration,
signers,
validGuardMock,
invalidGuardMock,
singleton130Address,
singleton130L2Address,
};
});

describe("migrateToL2", () => {
it("reverts if the singleton is not set", async () => {
const {
migration,
safeWith1967Proxy,
signers: [user1],
singleton130L2Address,
} = await setupTests();

await expect(
executeContractCallWithSigners(safeWith1967Proxy, migration, "migrateToL2", [singleton130L2Address], [user1], true),
).to.be.revertedWith("GS013");
});

it("reverts if new singleton is the same as the old one", async () => {
const {
safe130,
migration,
signers: [user1],
singleton130Address,
} = await setupTests();
await expect(
executeContractCallWithSigners(safe130, migration, "migrateToL2", [singleton130Address], [user1], true),
).to.be.revertedWith("GS013");
});

it("reverts if new singleton is not supported", async () => {
const {
safe130,
migration,
signers: [user1],
} = await setupTests();
await expect(
executeContractCallWithSigners(safe130, migration, "migrateToL2", [SAFE_SINGLETON_150_L2_ADDRESS], [user1], true),
).to.be.revertedWith("GS013");
});

it("reverts if nonce > 0", async () => {
const {
safe130,
migration,
signers: [user1],
singleton130Address,
singleton130L2Address,
} = await setupTests();
const safeAddress = await safe130.getAddress();
// The emit matcher checks the address, which is the Safe as delegatecall is used
const migrationSafe = migration.attach(safeAddress);

// Increase nonce by sending eth
await user1.sendTransaction({ to: safeAddress, value: ethers.parseEther("1") });
const nonce = 0;
const safeTx = buildSafeTransaction({ to: user1.address, value: ethers.parseEther("1"), nonce });
await executeTxWithSigners(safe130, safeTx, [user1]);

await expect(
executeContractCallWithSigners(safe130, migration, "migrateToL2", [singleton130L2Address], [user1], true),
).to.be.revertedWith("GS013");

const singletonResp = await user1.call({ to: safeAddress, data: migratedInterface.encodeFunctionData("masterCopy") });
expect(migratedInterface.decodeFunctionResult("masterCopy", singletonResp)[0]).to.eq(singleton130Address);
});

it("migrates from singleton 1.3.0 to 1.3.0L2", async () => {
const {
safe130,
migration,
signers: [user1],
singleton130L2Address,
} = await setupTests();
const safeAddress = await safe130.getAddress();
// The emit matcher checks the address, which is the Safe as delegatecall is used
const migrationSafe = migration.attach(safeAddress);

await expect(executeContractCallWithSigners(safe130, migration, "migrateToL2", [singleton130L2Address], [user1], true))
.to.emit(migrationSafe, "ChangedMasterCopy")
.withArgs(singleton130L2Address);

const singletonResp = await user1.call({ to: safeAddress, data: migratedInterface.encodeFunctionData("masterCopy") });
expect(migratedInterface.decodeFunctionResult("masterCopy", singletonResp)[0]).to.eq(singleton130L2Address);
});

it("doesn't touch important storage slots", async () => {
const {
safe130,
migration,
signers: [user1],
singleton130L2Address,
} = await setupTests();
const safeAddress = await safe130.getAddress();

const ownerCountBeforeMigration = await hre.ethers.provider.getStorage(safeAddress, 3);
const thresholdBeforeMigration = await hre.ethers.provider.getStorage(safeAddress, 4);
const nonceBeforeMigration = await hre.ethers.provider.getStorage(safeAddress, 5);
const guardBeforeMigration = await hre.ethers.provider.getStorage(safeAddress, GUARD_STORAGE_SLOT);
const fallbackHandlerBeforeMigration = await hre.ethers.provider.getStorage(safeAddress, FALLBACK_HANDLER_STORAGE_SLOT);

await expect(executeContractCallWithSigners(safe130, migration, "migrateToL2", [singleton130L2Address], [user1], true));

expect(await hre.ethers.provider.getStorage(safeAddress, 3)).to.be.eq(ownerCountBeforeMigration);
expect(await hre.ethers.provider.getStorage(safeAddress, 4)).to.be.eq(thresholdBeforeMigration);
expect(await hre.ethers.provider.getStorage(safeAddress, 5)).to.be.eq(nonceBeforeMigration);
expect(await hre.ethers.provider.getStorage(safeAddress, GUARD_STORAGE_SLOT)).to.be.eq(guardBeforeMigration);
expect(await hre.ethers.provider.getStorage(safeAddress, FALLBACK_HANDLER_STORAGE_SLOT)).to.be.eq(
fallbackHandlerBeforeMigration,
);
});
});
});

0 comments on commit e476df3

Please sign in to comment.