Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate counterfactual Safe #229

Merged
merged 8 commits into from
Jul 29, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TransactionOptions } from '../types'
import { TransactionOptions } from '../types';

export interface CreateProxyProps {
safeMasterCopyAddress: string
Expand All @@ -10,6 +10,7 @@ export interface CreateProxyProps {

export interface GnosisSafeProxyFactoryContract {
getAddress(): string
proxyCreationCode(): Promise<string>
createProxy(options: CreateProxyProps): Promise<string>
encode(methodName: string, params: any[]): string
estimateGas(methodName: string, params: any[], options: TransactionOptions): Promise<number>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface EthAdapter {
getEip3770Address(fullAddress: string): Promise<Eip3770Address>
getBalance(address: string): Promise<BigNumber>
getChainId(): Promise<number>
getChecksummedAddress(address: string): string
getSafeContract({
safeVersion,
chainId,
Expand Down
2 changes: 2 additions & 0 deletions packages/safe-core-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@nomiclabs/hardhat-web3": "^2.0.0",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/ethereumjs-abi": "^0.6.3",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.23",
"@types/semver": "^7.3.9",
Expand Down Expand Up @@ -91,6 +92,7 @@
"@ethersproject/solidity": "^5.6.0",
"@gnosis.pm/safe-core-sdk-types": "^1.2.1",
"@gnosis.pm/safe-deployments": "1.15.0",
"ethereumjs-abi": "^0.6.8",
"ethereumjs-util": "^7.1.4",
"semver": "^7.3.5",
"web3-utils": "^1.7.1"
Expand Down
2 changes: 2 additions & 0 deletions packages/safe-core-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Safe, {
} from './Safe'
import SafeFactory, {
DeploySafeProps,
PredictSafeProps,
SafeAccountConfig,
SafeDeploymentConfig,
SafeFactoryConfig
Expand All @@ -24,6 +25,7 @@ export {
SafeFactoryConfig,
SafeAccountConfig,
SafeDeploymentConfig,
PredictSafeProps,
DeploySafeProps,
SafeConfig,
ConnectSafeConfig,
Expand Down
41 changes: 40 additions & 1 deletion packages/safe-core-sdk/src/safeFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
SafeVersion,
TransactionOptions
} from '@gnosis.pm/safe-core-sdk-types'
import abi from 'ethereumjs-abi'
import { generateAddress2, keccak256, toBuffer } from 'ethereumjs-util'
import { SAFE_LAST_VERSION } from '../contracts/config'
import { getProxyFactoryContract, getSafeContract } from '../contracts/safeDeploymentContracts'
import Safe from '../Safe'
import { ContractNetworksConfig } from '../types'
import { EMPTY_DATA, ZERO_ADDRESS } from '../utils/constants'
import { validateSafeAccountConfig } from './utils'
import { validateSafeAccountConfig, validateSafeDeploymentConfig } from './utils'

export interface SafeAccountConfig {
owners: string[]
Expand All @@ -27,6 +29,11 @@ export interface SafeDeploymentConfig {
saltNonce: number
}

export interface PredictSafeProps {
safeAccountConfig: SafeAccountConfig
safeDeploymentConfig: SafeDeploymentConfig
}

export interface DeploySafeProps {
safeAccountConfig: SafeAccountConfig
safeDeploymentConfig?: SafeDeploymentConfig
Expand Down Expand Up @@ -140,13 +147,45 @@ class SafeFactory {
])
}

async predictSafeAddress({
safeAccountConfig,
safeDeploymentConfig
}: PredictSafeProps): Promise<string> {
validateSafeAccountConfig(safeAccountConfig)
if (safeDeploymentConfig) {
validateSafeDeploymentConfig(safeDeploymentConfig)
}

const from = this.#safeProxyFactoryContract.getAddress()

const initializer = await this.encodeSetupCallData(safeAccountConfig)
const saltNonce = safeDeploymentConfig.saltNonce.toString()
const encodedNonce = abi.rawEncode(['uint256'], [saltNonce]).toString('hex')
const salt = keccak256(
toBuffer('0x' + keccak256(toBuffer(initializer)).toString('hex') + encodedNonce)
)

const proxyCreationCode = await this.#safeProxyFactoryContract.proxyCreationCode()
const constructorData = abi
.rawEncode(['address'], [this.#gnosisSafeContract.getAddress()])
.toString('hex')
const initCode = proxyCreationCode + constructorData

const proxyAddress =
'0x' + generateAddress2(toBuffer(from), toBuffer(salt), toBuffer(initCode)).toString('hex')
return this.#ethAdapter.getChecksummedAddress(proxyAddress)
}

async deploySafe({
safeAccountConfig,
safeDeploymentConfig,
options,
callback
}: DeploySafeProps): Promise<Safe> {
validateSafeAccountConfig(safeAccountConfig)
if (safeDeploymentConfig) {
validateSafeDeploymentConfig(safeDeploymentConfig)
}
const signerAddress = await this.#ethAdapter.getSignerAddress()
const initializer = await this.encodeSetupCallData(safeAccountConfig)
const saltNonce =
Expand Down
6 changes: 5 additions & 1 deletion packages/safe-core-sdk/src/safeFactory/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { SafeAccountConfig } from './'
import { SafeAccountConfig, SafeDeploymentConfig } from './'

export const validateSafeAccountConfig = ({ owners, threshold }: SafeAccountConfig): void => {
if (owners.length <= 0) throw new Error('Owner list must have at least one owner')
if (threshold <= 0) throw new Error('Threshold must be greater than or equal to 1')
if (threshold > owners.length)
throw new Error('Threshold must be lower than or equal to owners length')
}

export const validateSafeDeploymentConfig = ({ saltNonce }: SafeDeploymentConfig): void => {
if (saltNonce < 0) throw new Error('saltNonce must be greater than 0')
}
88 changes: 88 additions & 0 deletions packages/safe-core-sdk/tests/safeFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { safeVersionDeployed } from '../hardhat/deploy/deploy-contracts'
import {
ContractNetworksConfig,
DeploySafeProps,
PredictSafeProps,
SafeAccountConfig,
SafeDeploymentConfig,
SafeFactory
Expand Down Expand Up @@ -95,6 +96,93 @@ describe('Safe Proxy Factory', () => {
})
})

describe('predictSafeAddress', async () => {
it('should fail if there are no owners', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeFactory = await SafeFactory.create({ ethAdapter, contractNetworks })
const owners: string[] = []
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: 1 }
const predictSafeProps: PredictSafeProps = { safeAccountConfig, safeDeploymentConfig }
chai
.expect(safeFactory.predictSafeAddress(predictSafeProps))
.rejectedWith('Owner list must have at least one owner')
})

it('should fail if the threshold is lower than 0', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1, account2] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeFactory = await SafeFactory.create({ ethAdapter, contractNetworks })
const owners = [account1.address, account2.address]
const threshold = 0
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: 1 }
const predictSafeProps: PredictSafeProps = { safeAccountConfig, safeDeploymentConfig }
chai
.expect(safeFactory.predictSafeAddress(predictSafeProps))
.rejectedWith('Threshold must be greater than or equal to 1')
})

it('should fail if the threshold is higher than the threshold', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1, account2] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeFactory = await SafeFactory.create({ ethAdapter, contractNetworks })
const owners = [account1.address, account2.address]
const threshold = 3
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: 1 }
const predictSafeProps: PredictSafeProps = { safeAccountConfig, safeDeploymentConfig }
chai
.expect(safeFactory.predictSafeAddress(predictSafeProps))
.rejectedWith('Threshold must be lower than or equal to owners length')
})

it('should fail if the saltNonce is lower than 0', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1, account2] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeFactory = await SafeFactory.create({
ethAdapter,
safeVersion: safeVersionDeployed,
contractNetworks
})
const owners = [account1.address, account2.address]
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: -1 }
const predictSafeProps: PredictSafeProps = { safeAccountConfig, safeDeploymentConfig }
chai
.expect(safeFactory.predictSafeAddress(predictSafeProps))
.rejectedWith('saltNonce must be greater than 0')
})

it('should predict a new Safe with saltNonce', async () => {
const { accounts, contractNetworks } = await setupTests()
const [account1, account2] = accounts
const ethAdapter = await getEthAdapter(account1.signer)
const safeFactory = await SafeFactory.create({
ethAdapter,
safeVersion: safeVersionDeployed,
contractNetworks
})
const owners = [account1.address, account2.address]
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: 12345 }
const predictSafeProps: PredictSafeProps = { safeAccountConfig, safeDeploymentConfig }
const counterfactualSafeAddress = await safeFactory.predictSafeAddress(predictSafeProps)
const deploySafeProps: DeploySafeProps = { safeAccountConfig, safeDeploymentConfig }
const safe = await safeFactory.deploySafe(deploySafeProps)
const safeAddress = await safe.getAddress()
chai.expect(counterfactualSafeAddress).to.be.eq(safeAddress)
})
})

describe('deploySafe', async () => {
it('should fail if there are no owners', async () => {
const { accounts, contractNetworks } = await setupTests()
Expand Down
4 changes: 4 additions & 0 deletions packages/safe-ethers-lib/src/EthersAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ class EthersAdapter implements EthAdapter {
return (await this.#provider.getNetwork()).chainId
}

getChecksummedAddress(address: string): string {
return this.#ethers.utils.getAddress(address)
}

getSafeContract({
safeVersion,
chainId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class GnosisSafeProxyFactoryEthersContract implements GnosisSafeProxyFactoryCont
return this.contract.address
}

async proxyCreationCode(): Promise<string> {
return this.contract.proxyCreationCode()
}

async createProxy({
safeMasterCopyAddress,
initializer,
Expand Down
4 changes: 4 additions & 0 deletions packages/safe-web3-lib/src/Web3Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class Web3Adapter implements EthAdapter {
return this.#web3.eth.getChainId()
}

getChecksummedAddress(address: string): string {
return this.#web3.utils.toChecksumAddress(address)
}

getSafeContract({
safeVersion,
chainId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class GnosisSafeProxyFactoryWeb3Contract implements GnosisSafeProxyFactoryContra
return this.contract.options.address
}

async proxyCreationCode(): Promise<string> {
return this.contract.methods.proxyCreationCode().call()
}

async createProxy({
safeMasterCopyAddress,
initializer,
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2538,6 +2538,13 @@
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==

"@types/ethereumjs-abi@^0.6.3":
version "0.6.3"
resolved "https://registry.yarnpkg.com/@types/ethereumjs-abi/-/ethereumjs-abi-0.6.3.tgz#eb5ed09fd86b9e2b1c0eb75d1e9bc29c50715c86"
integrity sha512-DnHvqPkrJS5w4yZexTa5bdPNb8IyKPYciou0+zZCIg5fpzvGtyptTvshy0uZKzti2/k/markwjlxWRBWt7Mjuw==
dependencies:
"@types/node" "*"

"@types/[email protected]", "@types/express-serve-static-core@^4.17.18":
version "4.17.29"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.29.tgz#2a1795ea8e9e9c91b4a4bbe475034b20c1ec711c"
Expand Down