├── .gas-snapshot ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── scripts └── ExampleDeploy.s.sol ├── src └── SafeSingletonDeployer.sol └── test ├── Mock.sol ├── MockReverting.sol └── SafeSingletonDeployer.t.sol /.gas-snapshot: -------------------------------------------------------------------------------- 1 | SafeSingletonDeployerTest:test_deploy_createsAtExpectedAddress() (gas: 110747) 2 | SafeSingletonDeployerTest:test_deploy_createsContractCorrectly() (gas: 100784) 3 | SafeSingletonDeployerTest:test_deploy_reverts() (gas: 41137) -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Forge CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | forge-test: 13 | name: Run Forge Tests and Checks 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | 36 | - name: Check formatting 37 | run: | 38 | forge fmt --check 39 | id: fmt 40 | 41 | forge-coverage: 42 | name: Run Coverage Reporting 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v3 46 | with: 47 | submodules: recursive 48 | 49 | - name: Install Foundry 50 | uses: foundry-rs/foundry-toolchain@v1 51 | with: 52 | version: nightly 53 | 54 | - name: Install forge dependencies 55 | run: forge install 56 | 57 | - name: Install lcov 58 | run: | 59 | sudo apt-get install lcov 60 | id: lcov 61 | 62 | - name: Run coverage 63 | run: | 64 | forge coverage --report summary --report lcov 65 | 66 | - name: Prune coverage 67 | run: | 68 | lcov --remove ./lcov.info -o ./lcov-filtered.info 'test/*' 'script/*' 'src/utils/*' 69 | 70 | - name: Submit coverage to Coveralls 71 | uses: coverallsapp/github-action@master 72 | with: 73 | github-token: ${{ secrets.GITHUB_TOKEN }} 74 | path-to-lcov: ./lcov-filtered.info 75 | flag-name: foundry 76 | parallel: true 77 | 78 | finish: 79 | needs: forge-coverage 80 | if: ${{ always() }} 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Coveralls Finished 84 | uses: coverallsapp/github-action@v2 85 | with: 86 | parallel-finished: true 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Solidity Safe Singleton Factory Deployer 3 | This is a Solidity library for using [Safe Singleton Factory](https://github.com/safe-global/safe-singleton-factory). At the code level, this factory matches [Arachnid's Determinstic Deployment Proxy](https://github.com/Arachnid/deterministic-deployment-proxy). It is different from other deterministic deployment factories in that Safe holds the private key and has NOT shared any presigned transactions. This can help avoid accidental or malicious nonce increments via presigned transactions with invalid gas values for a certain network. 4 | 5 | Safe has currently deployed this factory to 252 chains and it has the same address on 248. 6 | 7 | I made this library so that this factory would be easiser to use with Forge deployment scripts, as there seem to only be existing tools for Hardhat. 8 | 9 | Example usage in [`ExampleDeploy.s.sol`](https://github.com/wilsoncusack/safe-singleton-deployer-sol/blob/main/scripts/ExampleDeploy.s.sol) 10 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /scripts/ExampleDeploy.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.13; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | 6 | import {SafeSingletonDeployer} from "../src/SafeSingletonDeployer.sol"; 7 | import {Mock} from "../test/Mock.sol"; 8 | 9 | contract ExampleDeployScript is Script { 10 | function run() public { 11 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); 12 | 13 | SafeSingletonDeployer.broadcastDeploy({ 14 | deployerPrivateKey: deployerPrivateKey, 15 | creationCode: type(Mock).creationCode, 16 | args: abi.encode(1), 17 | salt: bytes32("0x1234") 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SafeSingletonDeployer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {VmSafe} from "forge-std/Vm.sol"; 5 | 6 | /// @notice Library for deploying contracts using Safe's Singleton Factory 7 | /// https://github.com/safe-global/safe-singleton-factory 8 | library SafeSingletonDeployer { 9 | error DeployFailed(); 10 | 11 | address constant SAFE_SINGLETON_FACTORY = 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7; 12 | VmSafe private constant VM = VmSafe(address(uint160(uint256(keccak256("hevm cheat code"))))); 13 | 14 | function computeAddress(bytes memory creationCode, bytes32 salt) public pure returns (address) { 15 | return computeAddress(creationCode, "", salt); 16 | } 17 | 18 | function computeAddress(bytes memory creationCode, bytes memory args, bytes32 salt) public pure returns (address) { 19 | return VM.computeCreate2Address({ 20 | salt: salt, 21 | initCodeHash: _hashInitCode(creationCode, args), 22 | deployer: SAFE_SINGLETON_FACTORY 23 | }); 24 | } 25 | 26 | function broadcastDeploy(bytes memory creationCode, bytes memory args, bytes32 salt) internal returns (address) { 27 | VM.broadcast(); 28 | return _deploy(creationCode, args, salt); 29 | } 30 | 31 | function broadcastDeploy(bytes memory creationCode, bytes32 salt) internal returns (address) { 32 | VM.broadcast(); 33 | return _deploy(creationCode, "", salt); 34 | } 35 | 36 | function broadcastDeploy(address deployer, bytes memory creationCode, bytes memory args, bytes32 salt) 37 | internal 38 | returns (address) 39 | { 40 | VM.broadcast(deployer); 41 | return _deploy(creationCode, args, salt); 42 | } 43 | 44 | function broadcastDeploy(address deployer, bytes memory creationCode, bytes32 salt) internal returns (address) { 45 | VM.broadcast(deployer); 46 | return _deploy(creationCode, "", salt); 47 | } 48 | 49 | function broadcastDeploy(uint256 deployerPrivateKey, bytes memory creationCode, bytes memory args, bytes32 salt) 50 | internal 51 | returns (address) 52 | { 53 | VM.broadcast(deployerPrivateKey); 54 | return _deploy(creationCode, args, salt); 55 | } 56 | 57 | function broadcastDeploy(uint256 deployerPrivateKey, bytes memory creationCode, bytes32 salt) 58 | internal 59 | returns (address) 60 | { 61 | VM.broadcast(deployerPrivateKey); 62 | return _deploy(creationCode, "", salt); 63 | } 64 | 65 | /// @dev Allows calling without Forge broadcast 66 | function deploy(bytes memory creationCode, bytes memory args, bytes32 salt) internal returns (address) { 67 | return _deploy(creationCode, args, salt); 68 | } 69 | 70 | /// @dev Allows calling without Forge broadcast 71 | function deploy(bytes memory creationCode, bytes32 salt) internal returns (address) { 72 | return _deploy(creationCode, "", salt); 73 | } 74 | 75 | function _deploy(bytes memory creationCode, bytes memory args, bytes32 salt) private returns (address) { 76 | bytes memory callData = abi.encodePacked(salt, creationCode, args); 77 | 78 | (bool success, bytes memory result) = SAFE_SINGLETON_FACTORY.call(callData); 79 | 80 | if (!success) { 81 | // contract does not pass on revert reason 82 | // https://github.com/Arachnid/deterministic-deployment-proxy/blob/master/source/deterministic-deployment-proxy.yul#L13 83 | revert DeployFailed(); 84 | } 85 | 86 | return address(bytes20(result)); 87 | } 88 | 89 | function _hashInitCode(bytes memory creationCode, bytes memory args) private pure returns (bytes32) { 90 | return keccak256(abi.encodePacked(creationCode, args)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract Mock { 5 | uint256 public value; 6 | 7 | constructor(uint256 startValue) { 8 | value = startValue; 9 | } 10 | 11 | function setValue(uint256 value_) public returns (uint256) { 12 | return value = value_; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/MockReverting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | contract MockReverting { 5 | error MyError(); 6 | 7 | constructor() { 8 | revert MyError(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/SafeSingletonDeployer.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | 6 | import {SafeSingletonDeployer} from "../src/SafeSingletonDeployer.sol"; 7 | 8 | import {Mock} from "./Mock.sol"; 9 | import {MockReverting} from "./MockReverting.sol"; 10 | 11 | contract SafeSingletonDeployerTest is Test { 12 | // cast code 0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7 --rpc-url https://mainnet.base.org 13 | bytes factoryCode = 14 | hex"7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3"; 15 | 16 | function setUp() public { 17 | vm.etch(SafeSingletonDeployer.SAFE_SINGLETON_FACTORY, factoryCode); 18 | } 19 | 20 | function test_deploy_createsAtExpectedAddress() public { 21 | address expectedAddress = 22 | SafeSingletonDeployer.computeAddress(type(Mock).creationCode, abi.encode(1), bytes32("0x1234")); 23 | assertEq(expectedAddress.code, ""); 24 | address returnAddress = SafeSingletonDeployer.deploy({ 25 | creationCode: type(Mock).creationCode, 26 | args: abi.encode(1), 27 | salt: bytes32("0x1234") 28 | }); 29 | assertEq(returnAddress, expectedAddress); 30 | assertNotEq(expectedAddress.code, ""); 31 | } 32 | 33 | function test_deploy_createsContractCorrectly() public { 34 | uint256 startValue = 1; 35 | address mock = SafeSingletonDeployer.deploy({ 36 | creationCode: type(Mock).creationCode, 37 | args: abi.encode(1), 38 | salt: bytes32("0x1234") 39 | }); 40 | assertEq(startValue, Mock(mock).value()); 41 | uint256 newValue = 2; 42 | Mock(mock).setValue(newValue); 43 | assertEq(newValue, Mock(mock).value()); 44 | } 45 | 46 | function test_deploy_reverts() public { 47 | vm.expectRevert(); 48 | SafeSingletonDeployer.deploy({ 49 | creationCode: type(MockReverting).creationCode, 50 | args: abi.encode(1), 51 | salt: bytes32("0x1234") 52 | }); 53 | } 54 | } 55 | --------------------------------------------------------------------------------