-
Notifications
You must be signed in to change notification settings - Fork 954
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
1.4.1 Feature: Backport migration contracts to 1.4.1 (#795)
This PR implements #781. It migrates the migration contracts from: - #685 - #793 I had to refactor tests quite a bit because a lot has changed since the release of 1.4.1 like we added typechain types, migrated to ethers v6, etc --------- Co-authored-by: Akshay <[email protected]> Co-authored-by: Nicholas Rodrigues Lordello <[email protected]> Co-authored-by: Shebin John <[email protected]> Co-authored-by: Uxío <[email protected]>
- Loading branch information
1 parent
a1f6da9
commit ab1e24e
Showing
9 changed files
with
1,041 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {Safe} from "../Safe.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(); | ||
Safe(payable(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(); | ||
Safe(payable(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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// 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 { | ||
// solhint-disable-next-line | ||
function VERSION() external view returns (string memory); | ||
|
||
function setFallbackHandler(address handler) external; | ||
|
||
function getOwners() external view returns (address[] memory); | ||
|
||
function getThreshold() external view returns (uint256); | ||
} | ||
|
||
/** | ||
* @title Migration Contract for updating a Safe from 1.1.1/1.3.0/1.4.1 versions 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.1.1 to 1.3.0/1.4.1 L2, 1.3.0 to 1.3.0L2 or from 1.4.1 to 1.4.1L2 | ||
* Other 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 SafeSetup(address indexed initiator, address[] owners, uint256 threshold, address initializer, address fallbackHandler); | ||
|
||
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 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 Modifier to prevent using initialized Safes. | ||
* If Safe has a nonce higher than 0, it will revert | ||
*/ | ||
modifier onlyNonceZero() { | ||
// 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"); | ||
_; | ||
} | ||
|
||
/** | ||
* @dev Internal function with common migration steps, changes the singleton and emits SafeMultiSigTransaction event | ||
*/ | ||
function migrate(address l2Singleton, bytes memory functionData) private { | ||
singleton = l2Singleton; | ||
|
||
// Encode nonce, sender, threshold | ||
bytes memory additionalInfo = abi.encode(0, msg.sender, threshold); | ||
|
||
// Simulate a L2 transaction so Safe Tx Service indexer picks up the Safe | ||
emit SafeMultiSigTransaction( | ||
MIGRATION_SINGLETON, | ||
0, | ||
functionData, | ||
Enum.Operation.DelegateCall, | ||
0, | ||
0, | ||
0, | ||
address(0), | ||
address(0), | ||
"", // We cannot detect signatures | ||
additionalInfo | ||
); | ||
emit ChangedMasterCopy(singleton); | ||
} | ||
|
||
/** | ||
* @notice Migrate from Safe 1.3.0/1.4.1 Singleton (L1) to the same version provided L2 singleton | ||
* Safe is required to have nonce 0 so backend can support it after the migration | ||
* @dev This function should only be called via a delegatecall to perform the upgrade. | ||
* Singletons versions will be compared, so it implies that contracts exist | ||
*/ | ||
function migrateToL2(address l2Singleton) public onlyDelegateCall onlyNonceZero { | ||
require(address(singleton) != l2Singleton, "Safe is already using the singleton"); | ||
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 configure 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" | ||
); | ||
|
||
// 0xef2624ae - keccak("migrateToL2(address)") | ||
bytes memory functionData = abi.encodeWithSelector(0xef2624ae, l2Singleton); | ||
migrate(l2Singleton, functionData); | ||
} | ||
|
||
/** | ||
* @notice Migrate from Safe 1.1.1 Singleton to 1.3.0 or 1.4.1 L2 | ||
* Safe is required to have nonce 0 so backend can support it after the migration | ||
* @dev This function should only be called via a delegatecall to perform the upgrade. | ||
* Singletons version will be checked, so it implies that contracts exist. | ||
* A valid and compatible fallbackHandler needs to be provided, only existance will be checked. | ||
*/ | ||
function migrateFromV111(address l2Singleton, address fallbackHandler) public onlyDelegateCall onlyNonceZero { | ||
require(isContract(fallbackHandler), "fallbackHandler is not a contract"); | ||
|
||
bytes32 oldSingletonVersion = keccak256(abi.encodePacked(ISafe(singleton).VERSION())); | ||
require(oldSingletonVersion == keccak256(abi.encodePacked("1.1.1")), "Provided singleton version is not supported"); | ||
|
||
bytes32 newSingletonVersion = keccak256(abi.encodePacked(ISafe(l2Singleton).VERSION())); | ||
require( | ||
newSingletonVersion == keccak256(abi.encodePacked("1.3.0")) || newSingletonVersion == keccak256(abi.encodePacked("1.4.1")), | ||
"Provided singleton version is not supported" | ||
); | ||
|
||
ISafe safe = ISafe(address(this)); | ||
safe.setFallbackHandler(fallbackHandler); | ||
|
||
// Safes < 1.3.0 did not emit SafeSetup, so Safe Tx Service backend needs the event to index the Safe | ||
emit SafeSetup(MIGRATION_SINGLETON, safe.getOwners(), safe.getThreshold(), address(0), fallbackHandler); | ||
|
||
// 0xd9a20812 - keccak("migrateFromV111(address,address)") | ||
bytes memory functionData = abi.encodeWithSelector(0xd9a20812, l2Singleton, fallbackHandler); | ||
migrate(l2Singleton, functionData); | ||
} | ||
|
||
/** | ||
* @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 (see documentation for details). | ||
* 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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
pragma solidity >=0.7.0 <0.9.0; | ||
|
||
// solhint-disable-next-line no-unused-import | ||
import {UpgradeableProxy} from "@openzeppelin/contracts/proxy/UpgradeableProxy.sol"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { DeployFunction } from "hardhat-deploy/types"; | ||
import { HardhatRuntimeEnvironment } from "hardhat/types"; | ||
|
||
const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { | ||
const { deployments, getNamedAccounts } = hre; | ||
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; |
Oops, something went wrong.