Skip to content

Commit

Permalink
feat(protocol-kit): predict safe address util (safe-global#415)
Browse files Browse the repository at this point in the history
* add predictSafeAddress util function

* remove chainId from predictSafeAddress

* add predictSafeAddress tests

* fix validateSafeDeploymentConfig

---------

Co-authored-by: Germán Martínez <6764315+germartinez@users.noreply.github.com>
  • Loading branch information
DaniSomoza and germartinez authored Apr 28, 2023
1 parent 827fab3 commit f7cd9f6
Show file tree
Hide file tree
Showing 10 changed files with 696 additions and 262 deletions.
54 changes: 31 additions & 23 deletions packages/account-abstraction-kit/src/AccountAbstraction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@ import {
OperationType
} from '@safe-global/account-abstraction-kit-poc/types'
import Safe, {
calculateProxyAddress,
encodeCreateProxyWithNonce,
predictSafeAddress,
encodeMultiSendData,
EthersAdapter,
getMultiSendCallOnlyContract,
getProxyFactoryContract,
getSafeContract,
getSafeInitializer,
PredictedSafeProps,
PREDETERMINED_SALT_NONCE,
PredictedSafeProps
SafeDeploymentConfig,
encodeCreateProxyWithNonce,
SafeAccountConfig,
encodeSetupCallData
} from '@safe-global/protocol-kit'
import { RelayPack } from '@safe-global/relay-kit'
import {
Expand Down Expand Up @@ -49,26 +51,31 @@ class AccountAbstraction {
const { relayPack } = options
this.setRelayPack(relayPack)

const predictedSafe: PredictedSafeProps = {
safeAccountConfig: {
owners: [await this.getSignerAddress()],
threshold: 1
},
safeDeploymentConfig: {
saltNonce: PREDETERMINED_SALT_NONCE
}
const signer = await this.getSignerAddress()
const owners = [signer]
const threshold = 1
const saltNonce = PREDETERMINED_SALT_NONCE

const safeAccountConfig: SafeAccountConfig = {
owners,
threshold
}
const safeDeploymentConfig: SafeDeploymentConfig = {
saltNonce,
safeVersion
}

this.#safeProxyFactoryContract = await getProxyFactoryContract({
ethAdapter: this.#ethAdapter,
safeVersion
})
const safeAddress = await calculateProxyAddress(
this.#ethAdapter,
safeVersion,
this.#safeProxyFactoryContract,
predictedSafe
)

const safeAddress = await predictSafeAddress({
ethAdapter: this.#ethAdapter,
safeAccountConfig,
safeDeploymentConfig
})

this.#safeContract = await getSafeContract({
ethAdapter: this.#ethAdapter,
safeVersion,
Expand Down Expand Up @@ -162,11 +169,12 @@ class AccountAbstraction {
saltNonce: PREDETERMINED_SALT_NONCE
}
}
const initializer = await getSafeInitializer(
this.#ethAdapter,
this.#safeContract,
predictedSafe
)

const initializer = await encodeSetupCallData({
ethAdapter: this.#ethAdapter,
safeContract: this.#safeContract,
safeAccountConfig: predictedSafe.safeAccountConfig
})

const safeDeploymentTransaction: MetaTransactionData = {
to: this.#safeProxyFactoryContract.getAddress(),
Expand Down
1 change: 1 addition & 0 deletions packages/protocol-kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"yargs": "^17.6.2"
},
"dependencies": {
"@ethersproject/address": "^5.7.0",
"@ethersproject/bignumber": "^5.7.0",
"@ethersproject/solidity": "^5.7.0",
"@safe-global/safe-deployments": "^1.22.0",
Expand Down
17 changes: 4 additions & 13 deletions packages/protocol-kit/src/Safe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ import {
TransactionResult
} from '@safe-global/safe-core-sdk-types'
import { SAFE_LAST_VERSION } from './contracts/config'
import { getProxyFactoryContract } from './contracts/safeDeploymentContracts'
import { calculateProxyAddress } from './contracts/utils'
import { predictSafeAddress } from './contracts/utils'
import ContractManager from './managers/contractManager'
import FallbackHandlerManager from './managers/fallbackHandlerManager'
import GuardManager from './managers/guardManager'
Expand Down Expand Up @@ -176,19 +175,11 @@ class Safe {
}

const chainId = await this.#ethAdapter.getChainId()
const safeProxyFactoryContract = await getProxyFactoryContract({
return predictSafeAddress({
ethAdapter: this.#ethAdapter,
safeVersion,
customContracts: this.#contractManager.contractNetworks?.[chainId]
customContracts: this.#contractManager.contractNetworks?.[chainId],
...this.#predictedSafe
})

return calculateProxyAddress(
this.#ethAdapter,
safeVersion,
safeProxyFactoryContract,
this.#predictedSafe,
this.#contractManager.contractNetworks?.[chainId]
)
}

if (!this.#contractManager.safeContract) {
Expand Down
195 changes: 118 additions & 77 deletions packages/protocol-kit/src/contracts/utils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
import { PredictedSafeProps } from '@safe-global/protocol-kit/index'
import {
EthAdapter,
GnosisSafeContract,
GnosisSafeProxyFactoryContract,
SafeVersion
GnosisSafeProxyFactoryContract
} from '@safe-global/safe-core-sdk-types'
import { BigNumberish, BytesLike, ethers } from 'ethers'
import { generateAddress2, keccak256, toBuffer } from 'ethereumjs-util'
import { isAddress } from '@ethersproject/address'
import { BigNumber } from '@ethersproject/bignumber'
import semverSatisfies from 'semver/functions/satisfies'
import {
getCompatibilityFallbackHandlerContract,
getProxyFactoryContract,
getSafeContract
} from '../contracts/safeDeploymentContracts'
import { ContractNetworkConfig, SafeAccountConfig } from '../types'
import { ZERO_ADDRESS } from '../utils/constants'
import { ContractNetworkConfig, SafeAccountConfig, SafeDeploymentConfig } from '../types'
import { EMPTY_DATA, ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants'
import { SAFE_LAST_VERSION } from '@safe-global/protocol-kit/contracts/config'

// keccak256(toUtf8Bytes('Safe Account Abstraction'))
export const PREDETERMINED_SALT_NONCE =
'0xb1073742015cbcf5a3a4d9d1ae33ecf619439710b89475f92e2abd2117e90f90'

export interface PredictSafeProps {
ethAdapter: EthAdapter
safeAccountConfig: SafeAccountConfig
safeDeploymentConfig: SafeDeploymentConfig
isL1SafeMasterCopy?: boolean
customContracts?: ContractNetworkConfig
}

export interface encodeSetupCallDataProps {
ethAdapter: EthAdapter
safeAccountConfig: SafeAccountConfig
safeContract: GnosisSafeContract
customContracts?: ContractNetworkConfig
}

export function encodeCreateProxyWithNonce(
safeProxyFactoryContract: GnosisSafeProxyFactoryContract,
safeSingletonAddress: string,
Expand All @@ -30,105 +47,129 @@ export function encodeCreateProxyWithNonce(
])
}

// TO-DO: Merge with encodeSetupCallData from the SafeFactory class
export async function encodeDefaultSetupCallData(
ethAdapter: EthAdapter,
safeVersion: SafeVersion,
safeContract: GnosisSafeContract,
safeAccountConfig: SafeAccountConfig,
customContracts?: ContractNetworkConfig
): Promise<string> {
export async function encodeSetupCallData({
ethAdapter,
safeAccountConfig,
safeContract,
customContracts
}: encodeSetupCallDataProps): Promise<string> {
const {
owners,
threshold,
to = ZERO_ADDRESS,
data = EMPTY_DATA,
fallbackHandler,
paymentToken = ZERO_ADDRESS,
payment = 0,
paymentReceiver = ZERO_ADDRESS
} = safeAccountConfig

const safeVersion = await safeContract.getVersion()

if (semverSatisfies(safeVersion, '<=1.0.0')) {
return safeContract.encode('setup', [
safeAccountConfig.owners, // required
safeAccountConfig.threshold as BigNumberish, // required
safeAccountConfig.to || ZERO_ADDRESS,
(safeAccountConfig.data as BytesLike) || '0x',
safeAccountConfig.paymentToken || ZERO_ADDRESS,
(safeAccountConfig.payment as BigNumberish) || 0,
safeAccountConfig.paymentReceiver || ZERO_ADDRESS
owners,
threshold,
to,
data,
paymentToken,
payment,
paymentReceiver
])
}
let fallbackHandlerAddress = safeAccountConfig.fallbackHandler
if (!fallbackHandlerAddress) {

let fallbackHandlerAddress = fallbackHandler
const isValidAddress = fallbackHandlerAddress !== undefined && isAddress(fallbackHandlerAddress)
if (!isValidAddress) {
const fallbackHandlerContract = await getCompatibilityFallbackHandlerContract({
ethAdapter,
safeVersion,
customContracts
})

fallbackHandlerAddress = fallbackHandlerContract.getAddress()
}

return safeContract.encode('setup', [
safeAccountConfig.owners, // required
safeAccountConfig.threshold as BigNumberish, // required
safeAccountConfig.to || ZERO_ADDRESS,
(safeAccountConfig.data as BytesLike) || '0x',
owners,
threshold,
to,
data,
fallbackHandlerAddress,
safeAccountConfig.paymentToken || ZERO_ADDRESS,
(safeAccountConfig.payment as BigNumberish) || 0,
safeAccountConfig.paymentReceiver || ZERO_ADDRESS
paymentToken,
payment,
paymentReceiver
])
}

export async function getSafeInitializer(
ethAdapter: EthAdapter,
safeContract: GnosisSafeContract,
predictedSafe: PredictedSafeProps,
customContracts?: ContractNetworkConfig
): Promise<string> {
const safeVersion = await safeContract.getVersion()
const initializer = await encodeDefaultSetupCallData(
export async function predictSafeAddress({
ethAdapter,
safeAccountConfig,
safeDeploymentConfig,
isL1SafeMasterCopy = false,
customContracts
}: PredictSafeProps): Promise<string> {
validateSafeAccountConfig(safeAccountConfig)
validateSafeDeploymentConfig(safeDeploymentConfig)

const { safeVersion = SAFE_LAST_VERSION, saltNonce = PREDETERMINED_SALT_NONCE } =
safeDeploymentConfig

const safeProxyFactoryContract = await getProxyFactoryContract({
ethAdapter,
safeVersion,
safeContract,
predictedSafe.safeAccountConfig,
customContracts
)
})

return initializer
}
const proxyCreationCode = await safeProxyFactoryContract.proxyCreationCode()

export async function calculateProxyAddress(
ethAdapter: EthAdapter,
safeVersion: SafeVersion,
safeProxyFactoryContract: GnosisSafeProxyFactoryContract,
predictedSafe: PredictedSafeProps,
customContracts?: ContractNetworkConfig
): Promise<string> {
const safeSingletonContract = await getSafeContract({
const safeContract = await getSafeContract({
ethAdapter,
safeVersion,
isL1SafeMasterCopy,
customContracts
})
const deployer = safeProxyFactoryContract.getAddress()

const deploymentCode = ethers.utils.solidityPack(
['bytes', 'uint256'],
[await safeProxyFactoryContract.proxyCreationCode(), safeSingletonContract.getAddress()]
)
const salt = ethers.utils.solidityKeccak256(
['bytes32', 'uint256'],
[
ethers.utils.solidityKeccak256(
['bytes'],
[
await getSafeInitializer(
ethAdapter,
safeSingletonContract,
predictedSafe,
customContracts
)
]
),
predictedSafe.safeDeploymentConfig.saltNonce || PREDETERMINED_SALT_NONCE
]
const initializer = await encodeSetupCallData({
ethAdapter,
safeAccountConfig,
safeContract,
customContracts
})

const encodedNonce = toBuffer(ethAdapter.encodeParameters(['uint256'], [saltNonce])).toString(
'hex'
)

const derivedAddress = ethers.utils.getCreate2Address(
deployer,
salt,
ethers.utils.keccak256(deploymentCode)
const salt = keccak256(
toBuffer('0x' + keccak256(toBuffer(initializer)).toString('hex') + encodedNonce)
)
return derivedAddress

const constructorData = toBuffer(
ethAdapter.encodeParameters(['address'], [safeContract.getAddress()])
).toString('hex')

const initCode = proxyCreationCode + constructorData

const proxyAddress =
'0x' +
generateAddress2(
toBuffer(safeProxyFactoryContract.getAddress()),
toBuffer(salt),
toBuffer(initCode)
).toString('hex')

return ethAdapter.getChecksummedAddress(proxyAddress)
}

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 && BigNumber.from(saltNonce).lt(0))
throw new Error('saltNonce must be greater than or equal to 0')
}
8 changes: 4 additions & 4 deletions packages/protocol-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import {
getSignMessageLibContract
} from './contracts/safeDeploymentContracts'
import {
calculateProxyAddress,
predictSafeAddress,
encodeSetupCallData,
encodeCreateProxyWithNonce,
getSafeInitializer,
PREDETERMINED_SALT_NONCE
} from './contracts/utils'
import ContractManager from './managers/contractManager'
Expand Down Expand Up @@ -115,8 +115,8 @@ export {
getMultiSendCallOnlyContract,
getSignMessageLibContract,
getCreateCallContract,
getSafeInitializer,
calculateProxyAddress,
predictSafeAddress,
encodeSetupCallData,
PREDETERMINED_SALT_NONCE,
encodeCreateProxyWithNonce,
EthSafeSignature
Expand Down
Loading

0 comments on commit f7cd9f6

Please sign in to comment.