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

v2.3.0 #230

Merged
merged 10 commits into from
Jul 29, 2022
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