Skip to content

Commit

Permalink
Merge pull request #230 from safe-global/development
Browse files Browse the repository at this point in the history
v2.3.0
  • Loading branch information
germartinez authored Jul 29, 2022
2 parents d038b4f + 5348aa5 commit 57f7a0a
Show file tree
Hide file tree
Showing 14 changed files with 191 additions and 27 deletions.
2 changes: 1 addition & 1 deletion packages/safe-core-sdk-types/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gnosis.pm/safe-core-sdk-types",
"version": "1.2.1",
"version": "1.3.0",
"description": "Safe Core SDK types",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { TransactionOptions } from '../types'
export interface CreateProxyProps {
safeMasterCopyAddress: string
initializer: string
saltNonce: number
saltNonce: string
options?: TransactionOptions
callback?: (txHash: string) => void
}

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
2 changes: 2 additions & 0 deletions packages/safe-core-sdk-types/src/ethereumLibs/EthAdapter.ts
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 Expand Up @@ -63,4 +64,5 @@ export interface EthAdapter {
callback?: (error: Error, gas: number) => void
): Promise<number>
call(transaction: EthAdapterTransaction): Promise<string>
encodeParameters(types: string[], values: any[]): string
}
4 changes: 2 additions & 2 deletions packages/safe-core-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gnosis.pm/safe-core-sdk",
"version": "2.2.1",
"version": "2.3.0",
"description": "Safe Core SDK",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
Expand Down Expand Up @@ -89,7 +89,7 @@
},
"dependencies": {
"@ethersproject/solidity": "^5.6.0",
"@gnosis.pm/safe-core-sdk-types": "^1.2.1",
"@gnosis.pm/safe-core-sdk-types": "^1.3.0",
"@gnosis.pm/safe-deployments": "1.15.0",
"ethereumjs-util": "^7.1.4",
"semver": "^7.3.5",
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
46 changes: 43 additions & 3 deletions packages/safe-core-sdk/src/safeFactory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import {
SafeVersion,
TransactionOptions
} from '@gnosis.pm/safe-core-sdk-types'
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 @@ -24,7 +25,12 @@ export interface SafeAccountConfig {
}

export interface SafeDeploymentConfig {
saltNonce: number
saltNonce: string
}

export interface PredictSafeProps {
safeAccountConfig: SafeAccountConfig
safeDeploymentConfig: SafeDeploymentConfig
}

export interface DeploySafeProps {
Expand Down Expand Up @@ -140,17 +146,51 @@ class SafeFactory {
])
}

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

const from = this.#safeProxyFactoryContract.getAddress()

const initializer = await this.encodeSetupCallData(safeAccountConfig)
const saltNonce = safeDeploymentConfig.saltNonce
const encodedNonce = toBuffer(this.#ethAdapter.encodeParameters(['uint256'], [saltNonce])).toString(
'hex'
)

const salt = keccak256(
toBuffer('0x' + keccak256(toBuffer(initializer)).toString('hex') + encodedNonce)
)

const proxyCreationCode = await this.#safeProxyFactoryContract.proxyCreationCode()
const constructorData = toBuffer(
this.#ethAdapter.encodeParameters(['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 =
safeDeploymentConfig?.saltNonce ?? Date.now() * 1000 + Math.floor(Math.random() * 1000)
safeDeploymentConfig?.saltNonce ??
(Date.now() * 1000 + Math.floor(Math.random() * 1000)).toString()

if (options?.gas && options?.gasLimit) {
throw new Error('Cannot specify gas and gasLimit together in transaction options')
Expand Down
8 changes: 7 additions & 1 deletion packages/safe-core-sdk/src/safeFactory/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { SafeAccountConfig } from './'
import { BigNumber } from '@ethersproject/bignumber'
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 (BigNumber.from(saltNonce).lt(0))
throw new Error('saltNonce must be greater than or equal to 0')
}
102 changes: 95 additions & 7 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 }
await 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 }
await 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 }
await 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 }
await chai
.expect(safeFactory.predictSafeAddress(predictSafeProps))
.rejectedWith('saltNonce must be greater than or equal to 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 All @@ -105,7 +193,7 @@ describe('Safe Proxy Factory', () => {
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeployProps: DeploySafeProps = { safeAccountConfig }
chai
await chai
.expect(safeFactory.deploySafe(safeDeployProps))
.rejectedWith('Owner list must have at least one owner')
})
Expand All @@ -119,7 +207,7 @@ describe('Safe Proxy Factory', () => {
const threshold = 0
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeployProps: DeploySafeProps = { safeAccountConfig }
chai
await chai
.expect(safeFactory.deploySafe(safeDeployProps))
.rejectedWith('Threshold must be greater than or equal to 1')
})
Expand All @@ -133,7 +221,7 @@ describe('Safe Proxy Factory', () => {
const threshold = 3
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const deploySafeProps: DeploySafeProps = { safeAccountConfig }
chai
await chai
.expect(safeFactory.deploySafe(deploySafeProps))
.rejectedWith('Threshold must be lower than or equal to owners length')
})
Expand All @@ -150,11 +238,11 @@ describe('Safe Proxy Factory', () => {
const owners = [account1.address, account2.address]
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: -1 }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: '-1' }
const safeDeployProps: DeploySafeProps = { safeAccountConfig, safeDeploymentConfig }
chai
await chai
.expect(safeFactory.deploySafe(safeDeployProps))
.rejectedWith('saltNonce must be greater than 0')
.rejectedWith('saltNonce must be greater than or equal to 0')
})

it('should deploy a new Safe without saltNonce', async () => {
Expand Down Expand Up @@ -189,7 +277,7 @@ describe('Safe Proxy Factory', () => {
const owners = [account1.address, account2.address]
const threshold = 2
const safeAccountConfig: SafeAccountConfig = { owners, threshold }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: 1 }
const safeDeploymentConfig: SafeDeploymentConfig = { saltNonce: '1' }
const deploySafeProps: DeploySafeProps = { safeAccountConfig, safeDeploymentConfig }
const safe = await safeFactory.deploySafe(deploySafeProps)
const deployedSafeOwners = await safe.getOwners()
Expand Down
4 changes: 2 additions & 2 deletions packages/safe-ethers-lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gnosis.pm/safe-ethers-lib",
"version": "1.2.1",
"version": "1.3.0",
"description": "Ethers library adapter to be used by Safe Core SDK",
"main": "dist/src/index.js",
"typings": "dist/src/index.d.ts",
Expand Down Expand Up @@ -50,7 +50,7 @@
"typescript": "^4.6.3"
},
"dependencies": {
"@gnosis.pm/safe-core-sdk-types": "^1.2.1",
"@gnosis.pm/safe-core-sdk-types": "^1.3.0",
"@gnosis.pm/safe-core-sdk-utils": "^1.2.1"
},
"peerDependencies": {
Expand Down
8 changes: 8 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 Expand Up @@ -159,6 +163,10 @@ class EthersAdapter implements EthAdapter {
call(transaction: EthAdapterTransaction): Promise<string> {
return this.#provider.call(transaction)
}

encodeParameters(types: string[], values: any[]) {
return new this.#ethers.utils.AbiCoder().encode(types, values)
}
}

export default EthersAdapter
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Event } from '@ethersproject/contracts'
import { GnosisSafeProxyFactoryContract } from '@gnosis.pm/safe-core-sdk-types'
import { ProxyFactory as ProxyFactory_V1_1_1 } from '../../../typechain/src/ethers-v5/v1.1.1/ProxyFactory'
Expand All @@ -7,7 +8,7 @@ import { EthersTransactionOptions } from '../../types'
export interface CreateProxyProps {
safeMasterCopyAddress: string
initializer: string
saltNonce: number
saltNonce: string
options?: EthersTransactionOptions
callback?: (txHash: string) => void
}
Expand All @@ -19,16 +20,19 @@ class GnosisSafeProxyFactoryEthersContract implements GnosisSafeProxyFactoryCont
return this.contract.address
}

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

async createProxy({
safeMasterCopyAddress,
initializer,
saltNonce,
options,
callback
}: CreateProxyProps): Promise<string> {
if (saltNonce < 0) {
throw new Error('saltNonce must be greater than 0')
}
if (BigNumber.from(saltNonce).lt(0))
throw new Error('saltNonce must be greater than or equal to 0')
if (options && !options.gasLimit) {
options.gasLimit = await this.estimateGas(
'createProxyWithNonce',
Expand Down
Loading

0 comments on commit 57f7a0a

Please sign in to comment.