Skip to content

Commit

Permalink
Generic migration contract (#793)
Browse files Browse the repository at this point in the history
Fixes: #787 

## Summary

This PR adds a general migration contract that takes address of the
Safe, SafeL2 and fallback handler contracts during deployment. The
contract allows Safe to update the Singleton at `address(0)`.

- Uses error strings rather than error types because Solidity version
0.7.6 doesn't support it.

As of now tests cover below migration paths: 
- 1.3.0 to 1.5.0
- 1.3.0 to 1.4.1
- 14.1 to 1.5.0

See `SafeMigration.spec.ts` to see how tests are organised. Do share if
you any thoughts to better run same tests on different migration paths.

The migration contract stores address of the Safe singletons and
fallback handler rather than using code hash and requiring the user to
provide singleton address as described in the issue. The reason being as
follows:
 
Checking codehash of the target singleton means user has to provide the
address of the target singleton. Also, checking code hash has higher gas
costs.

The only argument for using code hash for upgrades is that it also
allows unofficial singletons to be used for migration using official
migration contract. But, users/projects can also deploy their own
version of migration contract by providing singleton addresses in the
constructor and have similar security guarantees as the official
migration contract.

## Changes in PR
- Create a general migration contract which is not tightly bound to any
specific Safe version
- Update tests
- Remove other migration contract as this PR supersedes it
 
Unlike `Safe150Migration.sol`, this new contract does not check if
slot(0) of the contract stores an address having some non-empty code. I
think this check is not need because this contract is not intended to be
used in general by other proxy contracts and checking slot(0) value is
only a partially correct way. Would like to know thought of others.

---------

Co-authored-by: Nicholas Rodrigues Lordello <[email protected]>
Co-authored-by: Shebin John <[email protected]>
Co-authored-by: Mikhail <[email protected]>
  • Loading branch information
4 people authored Jul 19, 2024
1 parent 8f80a83 commit b6692f9
Show file tree
Hide file tree
Showing 10 changed files with 503 additions and 866 deletions.
135 changes: 0 additions & 135 deletions contracts/libraries/Safe130To141Migration.sol

This file was deleted.

109 changes: 0 additions & 109 deletions contracts/libraries/Safe150Migration.sol

This file was deleted.

120 changes: 120 additions & 0 deletions contracts/libraries/SafeMigration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

import {SafeStorage} from "../libraries/SafeStorage.sol";
import {ISafe} from "../interfaces/ISafe.sol";

/**
* @title Migration Contract for Safe Upgrade
* @notice This is a generic contract that facilitates Safe and SafeL2 proxy contracts to migrate their singleton address.
* The supported target Safe version is immutable and set in the constructor during the deployment of the contract.
* This contract also supports migration with fallback handler update.
* @author @safe-global/safe-protocol
* @dev IMPORTANT: The library is intended to be used with the Safe standard proxy that stores the singleton address
* at the storage slot 0. Use at your own risk with custom proxy implementations. The contract will allow invocations
* to the migration functions only via delegatecall.
*/
contract SafeMigration is SafeStorage {
/**
* @notice Address of this contract
*/
address public immutable MIGRATION_SINGLETON;
/**
* @notice Address of the Safe Singleton implementation
*/
address public immutable SAFE_SINGLETON;
/**
* @notice Address of the Safe Singleton (L2) implementation
*/
address public immutable SAFE_L2_SINGLETON;
/**
* @notice Addresss of the Fallback Handler
*/
address public immutable SAFE_FALLBACK_HANDLER;

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

/**
* @notice Modifier to make a function callable via delegatecall only.
* If the function is called via a regular call, it will revert.
*/
modifier onlyDelegateCall() {
require(address(this) != MIGRATION_SINGLETON, "Migration should only be called via delegatecall");
_;
}

/**
* @notice Constructor
* @param safeSingleton Address of the Safe Singleton implementation
* @param safeL2Singleton Address of the SafeL2 Singleton implementation
* @param fallbackHandler Address of the fallback handler implementation
*/
constructor(address safeSingleton, address safeL2Singleton, address fallbackHandler) {
MIGRATION_SINGLETON = address(this);

require(hasCode(safeSingleton), "Safe Singleton is not deployed");
require(hasCode(safeL2Singleton), "Safe Singleton (L2) is not deployed");
require(hasCode(fallbackHandler), "fallback handler is not deployed");

SAFE_SINGLETON = safeSingleton;
SAFE_L2_SINGLETON = safeL2Singleton;
SAFE_FALLBACK_HANDLER = fallbackHandler;
}

/**
* @notice Migrate the Safe contract to a new Safe Singleton implementation.
*/
function migrateSingleton() public onlyDelegateCall {
singleton = SAFE_SINGLETON;
emit ChangedMasterCopy(SAFE_SINGLETON);
}

/**
* @notice Migrate to Safe Singleton and set the fallback handler. This function is intended to be used when migrating
* a Safe to a version which also requires updating fallback handler.
*/
function migrateWithFallbackHandler() public onlyDelegateCall {
migrateSingleton();
ISafe(address(this)).setFallbackHandler(SAFE_FALLBACK_HANDLER);
}

/**
* @notice Migrate the Safe contract to a new Safe Singleton (L2) implementation.
*/
function migrateL2Singleton() public onlyDelegateCall {
singleton = SAFE_L2_SINGLETON;
emit ChangedMasterCopy(SAFE_L2_SINGLETON);
}

/**
* @notice Migrate to Safe Singleton (L2) and set the fallback handler. This function is intended to be used when migrating
* a Safe to a version which also requires updating fallback handler.
*/
function migrateL2WithFallbackHandler() public onlyDelegateCall {
migrateL2Singleton();
ISafe(address(this)).setFallbackHandler(SAFE_FALLBACK_HANDLER);
}

/**
* @notice Checks whether an account has code.
* @param account The address of the account to be checked.
* @return A boolean value indicating whether the address has code (true) or not (false).
* @dev This function relies on the `extcodesize` assembly opcode to determine whether an address has code.
* It does not reliably determine whether or not an address is a smart contract or an EOA.
*/
function hasCode(address account) internal view returns (bool) {
uint256 size;
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
size := extcodesize(account)
}
/* solhint-enable no-inline-assembly */

return size > 0;
}
}
12 changes: 12 additions & 0 deletions src/deploy/deploy_migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,25 @@ const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deployer } = await getNamedAccounts();
const { deploy } = deployments;

const Safe = await deployments.get("Safe");
const SafeL2 = await deployments.get("SafeL2");
const CompatibilityFallbackHandler = await deployments.get("CompatibilityFallbackHandler");

await deploy("SafeToL2Migration", {
from: deployer,
args: [],
log: true,
deterministicDeployment: true,
});

await deploy("SafeMigration", {
from: deployer,
args: [Safe.address, SafeL2.address, CompatibilityFallbackHandler.address],
log: true,
deterministicDeployment: true,
});
};

deploy.tags = ["not-l2-to-l2-migration", "migration"];
deploy.dependencies = ["singleton", "l2", "handlers"];
export default deploy;
Loading

0 comments on commit b6692f9

Please sign in to comment.