Skip to content

Commit

Permalink
Add Safe to L2 Setup Contract (#759)
Browse files Browse the repository at this point in the history
This PR introduces a setup contract that can be called from the `Safe`
setup function in order to automatically promote a Safe at setup time if
the code is executing on an L2. Namely, this allows the Safe Proxy
factory to use a single singleton and initializer for all chains, but
end up with different `singleton`s depending on the chain ID.

The expected use of this contract is to use the standard proxy factory:

```solidity
Safe l1Singleton;
SafeL2 l2Singleton;
SafeToL2Setup l2Setup;

proxyFactory.createProxyWithNonce(
    address(l1Singleton),
    abi.encodeCall(
        l1Singleton.setup,
        (
            owners,
            threshold,
            address(l2Setup),
            abi.encodeCall(l2Setup.setupToL2, address(l2Singleton)),
            fallbackHandler,
            paymentToken,
            payment,
            paymentReceiver
        )
    ),
    saltNonce
)
```

On L1 (i.e. Ethereum Mainnet where `chainId == 1`), you would end up
with a Safe where `safe.singleton == l1Singleton` and on any other
chains, you would end up with `safe.singleton == l2Singleton`. This
would happen _before_ the first transaction.

---------

Co-authored-by: Mikhail <16622558+mmv08@users.noreply.github.com>
  • Loading branch information
nlordell and mmv08 authored Jul 3, 2024
1 parent 13c0494 commit 8f80a83
Show file tree
Hide file tree
Showing 16 changed files with 550 additions and 194 deletions.
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ SAFE_CONTRACT_UNDER_TEST="Safe"
SOLIDITY_VERSION= # Example: '0.8.19'
# For running coverage tests, `details` section of solidity settings are required, else could be removed.
SOLIDITY_SETTINGS= # Example: '{"viaIR":true,"optimizer":{"enabled":true, "details": {"yul": true, "yulDetails": { "optimizerSteps": ""}}}}'
# Sets hardhat chain id. In general, you don't need this, it's only used for testing the SafeToL2Setup contract.
HARDHAT_CHAIN_ID=31337
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
solidity: ["0.7.6", "0.8.24"]
include:
- solidity: "0.8.24"
settings: '{"viaIR":true,"optimizer":{"enabled":true,"runs":1000000}}'
settings: '{"viaIR":false,"optimizer":{"enabled":true,"runs":1000000}}'
env:
SOLIDITY_VERSION: ${{ matrix.solidity }}
SOLIDITY_SETTINGS: ${{ matrix.settings }}
Expand Down
2 changes: 2 additions & 0 deletions benchmark/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const generateTarget = async (owners: number, threshold: number, guardAddress: s
const safe = await getSafeWithOwners(
signers.map((owner) => owner.address),
threshold,
ethers.ZeroAddress,
"0x",
fallbackHandlerAddress,
logGasUsage,
saltNumber,
Expand Down
94 changes: 94 additions & 0 deletions contracts/libraries/SafeToL2Setup.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;

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

/**
* @title Safe to L2 Setup Contract
* @dev This contract expects the singleton to be the {Safe} by default. Even if there are more
* {SafeL2} proxies deployed, the average gas cost on L2s is significantly lower, making the
* current design more economically efficient overall.
* @notice This contract facilitates the deployment of a Safe to the same address on all networks by
* automatically changing the singleton to the L2 version when not on chain ID 1.
*/
contract SafeToL2Setup is SafeStorage {
/**
* @notice Address of the contract.
* @dev This is used to ensure that the contract is only ever `DELEGATECALL`-ed.
*/
address public immutable _SELF;

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

/**
* @notice Initializes a new {SafeToL2Setup} instance.
*/
constructor() {
_SELF = address(this);
}

/**
* @notice Modifier ensure a function is only called via `DELEGATECALL`. Will revert otherwise.
*/
modifier onlyDelegateCall() {
require(address(this) != _SELF, "SafeToL2Setup should only be called via delegatecall");
_;
}

/**
* @notice Modifier to prevent using initialized Safes.
*/
modifier onlyNonceZero() {
require(nonce == 0, "Safe must have not executed any tx");
_;
}

/**
* @notice Modifier to ensure that the specified account is a contract.
*
*/
modifier onlyContract(address account) {
require(_codeSize(account) != 0, "Account doesn't contain code");
_;
}

/**
* @notice Setup the Safe with the provided L2 singleton if needed.
* @dev This function checks that the chain ID is not 1, and if it isn't updates the singleton
* to the provided L2 singleton.
*/
function setupToL2(address l2Singleton) public onlyDelegateCall onlyNonceZero onlyContract(l2Singleton) {
if (_chainId() != 1) {
singleton = l2Singleton;
emit ChangedMasterCopy(l2Singleton);
}
}

/**
* @notice Returns the current chain ID.
*/
function _chainId() private view returns (uint256 result) {
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
result := chainid()
}
/* solhint-enable no-inline-assembly */
}

/**
* @notice Returns the code size of the specified account.
*/
function _codeSize(address account) internal view returns (uint256 result) {
/* solhint-disable no-inline-assembly */
/// @solidity memory-safe-assembly
assembly {
result := extcodesize(account)
}
/* solhint-enable no-inline-assembly */
}
}
3 changes: 2 additions & 1 deletion hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const argv = yargs

// Load environment variables.
dotenv.config();
const { NODE_URL, INFURA_KEY, MNEMONIC, ETHERSCAN_API_KEY, PK, SOLIDITY_VERSION, SOLIDITY_SETTINGS } = process.env;
const { NODE_URL, INFURA_KEY, MNEMONIC, ETHERSCAN_API_KEY, PK, SOLIDITY_VERSION, SOLIDITY_SETTINGS, HARDHAT_CHAIN_ID } = process.env;

const DEFAULT_MNEMONIC = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat";

Expand Down Expand Up @@ -78,6 +78,7 @@ const userConfig: HardhatUserConfig = {
allowUnlimitedContractSize: true,
blockGasLimit: 100000000,
gas: 100000000,
chainId: typeof HARDHAT_CHAIN_ID === "string" && !Number.isNaN(parseInt(HARDHAT_CHAIN_ID)) ? parseInt(HARDHAT_CHAIN_ID) : 31337,
},
mainnet: {
...sharedNetworkConfig,
Expand Down
Loading

0 comments on commit 8f80a83

Please sign in to comment.