Migrate to v7
Overview
This release adds support for Safe v1.5.0 smart contracts across the SDK suite (types-kit, protocol-kit, api-kit, and sdk-starter-kit). It includes breaking changes that require attention when upgrading.
Breaking Changes
1. DEFAULT_SAFE_VERSION changed from 1.3.0 to 1.4.1
Affected package: protocol-kit
The default Safe version used when no explicit version is provided has changed from 1.3.0 to 1.4.1. This affects Safe creation, deployment, and address prediction.
Before:
_10// Implicitly deployed a Safe v1.3.0_10const protocolKit = await Safe.init({_10 provider,_10 signer,_10 predictedSafe: {_10 safeAccountConfig: { owners, threshold }_10 }_10})
After:
_10// Now implicitly deploys a Safe v1.4.1_10const protocolKit = await Safe.init({_10 provider,_10 signer,_10 predictedSafe: {_10 safeAccountConfig: { owners, threshold }_10 }_10})
⚠️ Warning: Predicted Safe addresses will differ if you were relying on the previous default. Make sure to pin the version explicitly in production environments.
How to migrate:
If your application depends on deploying Safe v1.3.0 contracts, explicitly set the version:
_10const protocolKit = await Safe.init({_10 provider,_10 signer,_10 predictedSafe: {_10 safeAccountConfig: { owners, threshold },_10 safeDeploymentConfig: {_10 safeVersion: '1.3.0'_10 }_10 }_10})
2. encodeTransactionData removed from the Safe contract in v1.5.0
Affected packages: protocol-kit, types-kit
In Safe v1.5.0, the encodeTransactionData function has been removed from the Safe contract and relocated to the CompatibilityFallbackHandler.
Before (≤ v1.4.1):
_13// Called directly on the Safe contract_13const data = await safeContract.encodeTransactionData(_13 to,_13 value,_13 data,_13 operation,_13 safeTxGas,_13 baseGas,_13 gasPrice,_13 gasToken,_13 refundReceiver,_13 nonce_13)
After (v1.5.0):
Calling encodeTransactionData on a Safe v1.5.0 contract will revert on-chain.
How to migrate:
For Safe v1.5.0, use the CompatibilityFallbackHandler contract instead:
_19import { getCompatibilityFallbackHandlerContract } from '@safe-global/protocol-kit'_19_19const fallbackHandler = await getCompatibilityFallbackHandlerContract({_19 safeProvider,_19 safeVersion: '1.5.0'_19})_19_19const data = await fallbackHandler.encodeTransactionData(_19 to,_19 value,_19 data,_19 operation,_19 safeTxGas,_19 baseGas,_19 gasPrice,_19 gasToken,_19 refundReceiver,_19 nonce_19)
If you need to support multiple Safe versions, add a version check:
_10import { semverSatisfies } from '@safe-global/protocol-kit'_10_10if (semverSatisfies(safeVersion, '>=1.5.0')) {_10 // Use CompatibilityFallbackHandler_10} else {_10 // Use Safe contract directly_10}
3. Legacy isValidSignature(bytes, bytes) removed in v1.5.0
Affected package: protocol-kit
The CompatibilityFallbackHandler in Safe v1.5.0 no longer exposes the legacy isValidSignature(bytes, bytes) overload. Only the modern EIP-1271 overload isValidSignature(bytes32, bytes) is supported.
Before (≤ v1.4.1):
The SDK called both overloads in parallel during signature validation:
isValidSignature(bytes32, bytes): EIP-1271 standardisValidSignature(bytes, bytes): Legacy overload
After (v1.5.0):
Only the modern overload is called. The legacy call would revert or return invalid data against a v1.5.0 Safe.
How to migrate:
If you are calling isValidSignature directly, ensure you use the bytes32 overload:
_10// ✅ Correct for all versions_10const result = await safeContract.isValidSignature(messageHash, signature)_10// where messageHash is bytes32_10_10// ❌ Will fail on v1.5.0_10const result = await safeContract.isValidSignature(messageBytes, signature)_10// where messageBytes is raw bytes
Note: The SDK handles this internally. If you rely on the SDK's
isValidSignaturemethod in theSafeclass, no changes are needed, the SDK now conditionally skips the legacy call for v1.5.0+.
4. SafeVersion type extended with '1.5.0'
Affected package: types-kit
The SafeVersion union type now includes '1.5.0' as a valid value.
Before:
_10type SafeVersion = '1.0.0' | '1.1.1' | '1.2.0' | '1.3.0' | '1.4.1'
After:
_10type SafeVersion = '1.0.0' | '1.1.1' | '1.2.0' | '1.3.0' | '1.4.1' | '1.5.0'
How to migrate:
If your code performs exhaustive checks on SafeVersion, add a case for '1.5.0':
_22// Before — will cause a TypeScript compilation error_22switch (version) {_22 case '1.0.0': // ..._22 case '1.1.1': // ..._22 case '1.2.0': // ..._22 case '1.3.0': // ..._22 case '1.4.1': // ..._22 default:_22 const \_exhaustive: never = version // ❌ TS error_22}_22_22// After — add the new version_22switch (version) {_22 case '1.0.0': // ..._22 case '1.1.1': // ..._22 case '1.2.0': // ..._22 case '1.3.0': // ..._22 case '1.4.1': // ..._22 case '1.5.0': // ..._22 default:_22 const \_exhaustive: never = version // ✅_22}
5. Passkey Verifier Changes
The FCLP256Verifier contract used as the default P256 signature verifier for passkey signers has been deprecated. This release removes the silent default and migrates to DaimoP256Verifier as the new recommended verifier.
As part of this change, the passkey API has been cleaned up to make the verifier address explicit and required at setup time. This prevents a class of bugs where existing passkeys could silently resolve to a wrong signer address if the default ever changed.
PasskeyArgType now requires verifierAddress
The optional customVerifierAddress?: string field has been replaced by a required verifierAddress: string.
_13// ❌ Before_13const passkey: PasskeyArgType = {_13 rawId: '...',_13 coordinates: { x: '...', y: '...' },_13 customVerifierAddress: '0x...' // optional, SDK filled in a default if omitted_13}_13_13// ✅ After_13const passkey: PasskeyArgType = {_13 rawId: '...',_13 coordinates: { x: '...', y: '...' },_13 verifierAddress: '0x...' // required, no default_13}
New type ExtractedPasskeyData
A new type has been introduced to represent the raw output of credential extraction, just rawId and coordinates. PasskeyArgType now extends this type.
_11// ExtractedPasskeyData — what you get from a WebAuthn credential alone_11type ExtractedPasskeyData = {_11 rawId: string_11 coordinates: PasskeyCoordinates_11}_11_11// PasskeyArgType — what you need to use the passkey as a Safe signer_11type PasskeyArgType = ExtractedPasskeyData & {_11 verifierAddress: string_11 getFn?: GetPasskeyCredentialFn_11}
Safe.createPasskeySigner() return type changed
This method now returns ExtractedPasskeyData instead of PasskeyArgType. You must combine the result with a verifierAddress before passing it to Safe.init().
_10// ❌ Before — result was directly usable as a signer_10const signer = await Safe.createPasskeySigner(credential)_10// signer was PasskeyArgType (verifier defaulted silently)_10_10// ✅ After — must add verifierAddress explicitly_10const extracted = await Safe.createPasskeySigner(credential)_10const signer: PasskeyArgType = {_10 ...extracted,_10 verifierAddress: getP256VerifierAddress(chainId)_10}
getDefaultFCLP256VerifierAddress() removed
This internal function has been replaced by the new public export getP256VerifierAddress(chainId), which returns the DaimoP256Verifier address for a given chain.
_10// ❌ Before (internal, not recommended for direct use)_10import { getDefaultFCLP256VerifierAddress } from '@safe-global/protocol-kit'_10const address = getDefaultFCLP256VerifierAddress(chainId)_10_10// ✅ After_10import { getP256VerifierAddress } from '@safe-global/protocol-kit'_10const address = getP256VerifierAddress(chainId)
chainId parameter removed from createPasskeyClient and isSharedSigner
Both functions no longer accept a chainId argument, as it was only used internally to resolve the default verifier.
We changed the default verifier from FCLP256Verifier to DaimoP256Verifier. If you were relying on the old default, you must now explicitly provide the new verifier address when constructing your passkey signer. You can use @safe-global/safe-deployments (opens in a new tab) v2 to resolve the correct address for your chain, or hard-code it if you prefer.
_12// ❌ Before_12await createPasskeyClient(_12 passkey,_12 contract,_12 provider,_12 safeAddress,_12 owners,_12 chainId_12)_12_12// ✅ After_12await createPasskeyClient(passkey, contract, provider, safeAddress, owners)
Full Migration Example
Setting up a new passkey signer
_26import Safe, {_26 getP256VerifierAddress,_26 type PasskeyArgType_26} from '@safe-global/protocol-kit'_26_26// 1. Create a WebAuthn credential (for example, via navigator.credentials.create)_26const credential = await navigator.credentials.create({ publicKey: { ... } })_26_26// 2. Extract the passkey data from the credential_26const extracted = await Safe.createPasskeySigner(credential)_26_26// 3. Get the recommended verifier address for your chain_26const verifierAddress = getP256VerifierAddress(chainId)_26_26// 4. Build the full PasskeyArgType_26const passkeySigner: PasskeyArgType = {_26 ...extracted,_26 verifierAddress_26}_26_26// 5. Use it to initialize a Safe_26const protocolKit = await Safe.init({_26 provider,_26 signer: passkeySigner,_26 safeAddress: '0x...'_26})
Using a custom verifier
If your deployment uses a custom P256 verifier (for example, for testing or a non-standard chain), pass its address directly:
_10const passkeySigner: PasskeyArgType = {_10 ...extracted,_10 verifierAddress: '0xYourCustomVerifierAddress'_10}
Reconnecting an existing passkey (stored credentials)
If you store passkey data and reload it across sessions, make sure to persist and restore verifierAddress:
_17// When saving_17localStorage.setItem(_17 'passkey',_17 JSON.stringify({_17 rawId: passkeySigner.rawId,_17 coordinates: passkeySigner.coordinates,_17 verifierAddress: passkeySigner.verifierAddress // ← persist this!_17 })_17)_17_17// When loading_17const stored = JSON.parse(localStorage.getItem('passkey'))_17const passkeySigner: PasskeyArgType = {_17 rawId: stored.rawId,_17 coordinates: stored.coordinates,_17 verifierAddress: stored.verifierAddress // ← restore it_17}
⚠️ Important: The
verifierAddressis baked into the Safe's on-chain state at passkey setup time. Always restore the original verifier address that was used when the passkey owner was added to the Safe. Using a different address will cause the signer resolution to fail silently.
New Features (non-breaking)
ExtensibleFallbackHandler contract
A new ExtensibleFallbackHandler contract is available for Safe v1.5.0 with read and write methods:
_16import { getExtensibleFallbackHandlerContract } from '@safe-global/protocol-kit'_16_16const handler = await getExtensibleFallbackHandlerContract({_16 safeProvider,_16 safeVersion: '1.5.0'_16})_16_16// Read methods_16await handler.domainVerifiers(safe, domainSeparator)_16await handler.safeMethods(safe, selector)_16await handler.safeInterfaces(safe, interfaceId)_16_16// Write methods (executed as Safe transactions)_16await handler.setSafeMethod(selector, newMethod)_16await handler.setDomainVerifier(domainSeparator, verifier)_16await handler.setSupportedInterface(interfaceId, supported)
checkNSignatures with executor parameter
Safe v1.5.0 adds a new overload of checkNSignatures and checkSignatures that accepts an explicit executor address:
_10// Original (all versions)_10await safeContract.checkNSignatures(dataHash, signatures, requiredSignatures)_10_10// New overload (v1.5.0 only)_10await safeContract.checkNSignaturesWithExecutor(_10 executor,_10 dataHash,_10 signatures,_10 requiredSignatures_10)
Module Guard support
Safe v1.5.0 introduces module guards. New methods are available in the Safe class:
_10// Enable a module guard_10const tx = await protocolKit.createEnableModuleGuardTx(moduleGuardAddress)_10_10// Disable the module guard_10const tx = await protocolKit.createDisableModuleGuardTx()
Note: Module guard functionality is only available for Safe v1.5.0+. Calling these methods on earlier versions will throw an error.
Deprecations
zkSync EraVM support in predictSafeAddress
The zkSync EraVM-specific logic in predictSafeAddress has been deprecated for Safe v1.5.0. If you are predicting Safe addresses on zkSync with v1.5.0 contracts, be aware that this path is no longer supported.
Quick Reference
| Change | Action Required | Packages |
|---|---|---|
Default version → 1.4.1 | Pin safeVersion explicitly if you need 1.3.0 | protocol-kit |
encodeTransactionData moved | Use CompatibilityFallbackHandler for v1.5.0 | protocol-kit, types-kit |
Legacy isValidSignature removed | Use bytes32 overload only for v1.5.0 | protocol-kit |
SafeVersion type extended | Add '1.5.0' case to exhaustive switches | types-kit |
PasskeyArgType.customVerifierAddress removed | Use verifierAddress (required) instead | protocol-kit |
PasskeyArgType.verifierAddress added | Provide verifierAddress when constructing passkey arguments | protocol-kit |
ExtractedPasskeyData type added | Use new type { rawId, coordinates } where applicable | protocol-kit |
Safe.createPasskeySigner() return type changed | Expect ExtractedPasskeyData instead of PasskeyArgType | protocol-kit |
getDefaultFCLP256VerifierAddress() removed | Use getP256VerifierAddress(chainId) instead | protocol-kit |
getP256VerifierAddress(chainId) added | New public export to resolve the P256 verifier address | protocol-kit |
createPasskeyClient signature changed | Remove chainId argument from call sites | protocol-kit |
isSharedSigner signature changed | Remove chainId argument from call sites | protocol-kit |