-
Notifications
You must be signed in to change notification settings - Fork 278
feat(protocol-kit): Add mastercopy bytecode matching for L2 deployments #1271
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
Closed
Copilot
wants to merge
10
commits into
development
from
copilot/fix-f31fb0b7-80b9-45b5-935a-c6950afea8c6
Closed
Changes from 7 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
79e1a44
chore: set release versions
dasanra 201c50e
chore(deps): bump @safe-global/safe-deployments to version 1.37.43 (#…
Fbartoli d7486b1
Initial plan
Copilot d22c1e2
Add mastercopy codehash matching utilities
Copilot 7ee4f86
Add unit tests for mastercopy matching functionality
Copilot ad103e6
Add documentation for mastercopy code matching feature
Copilot fd040a0
Add user guide for custom mastercopy support
Copilot e4d052f
Remove MASTERCOPY_MATCHING.md and update references
Copilot d4b7370
Limit mastercopy matching to 1.1.1 L2 and 1.3.0 L2 only
Copilot fc2ca88
Change supported versions to 1.3.0 L2 and 1.4.1 L2
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Mastercopy Code Matching Implementation | ||
|
||
## Overview | ||
|
||
This implementation adds support for Safe contracts whose mastercopy address is not listed in the official `safe-deployments` package. Instead of refusing to initialize, the SDK now attempts to match the mastercopy's bytecode hash against all known Safe versions. | ||
|
||
## Implementation Details | ||
|
||
### 1. Mastercopy Matcher Utility (`src/utils/mastercopyMatcher.ts`) | ||
|
||
Three main functions are provided: | ||
|
||
#### `getMasterCopyAddressFromProxy(safeProvider, safeAddress)` | ||
- Reads the mastercopy address from storage slot 0 of the Safe proxy | ||
- Returns the checksummed mastercopy address | ||
|
||
#### `matchContractCodeToSafeVersion(safeProvider, contractAddress, chainId, isL1SafeSingleton)` | ||
- Fetches the bytecode of the contract at the given address | ||
- Computes the keccak256 hash of the bytecode | ||
- Compares against all known Safe versions (1.4.1, 1.3.0, 1.2.0, 1.1.1, 1.0.0) | ||
- Checks both L1 and L2 singleton variants | ||
- Uses the pre-computed `codeHash` from safe-deployments for efficient matching | ||
- Returns the matched version and whether it's an L1 singleton, or undefined if no match | ||
|
||
#### `detectSafeVersionFromMastercopy(safeProvider, safeAddress, chainId, isL1SafeSingleton)` | ||
- Combines the above two functions | ||
- Reads the mastercopy address from the Safe proxy | ||
- Matches the mastercopy code to a known Safe version | ||
- Returns version, mastercopy address, and L1 flag, or undefined if detection fails | ||
|
||
### 2. Contract Manager Integration (`src/managers/contractManager.ts`) | ||
|
||
Modified the `#initializeContractManager` method to use mastercopy matching as a fallback: | ||
|
||
```typescript | ||
try { | ||
// Try to fetch the version via VERSION() call | ||
safeVersion = await getSafeContractVersion(safeProvider, safeAddress) | ||
} catch (e) { | ||
// If VERSION() fails, try mastercopy matching | ||
const mastercopyMatch = await detectSafeVersionFromMastercopy( | ||
safeProvider, | ||
safeAddress, | ||
chainId, | ||
isL1SafeSingleton | ||
) | ||
|
||
if (mastercopyMatch) { | ||
// Successfully matched - use detected version and L1 flag | ||
safeVersion = mastercopyMatch.version | ||
detectedIsL1 = mastercopyMatch.isL1 | ||
} else { | ||
// No match found - fall back to default version | ||
safeVersion = DEFAULT_SAFE_VERSION | ||
} | ||
} | ||
``` | ||
|
||
The detected `isL1` flag is then passed to `getSafeContract` to ensure the correct singleton type is used. | ||
|
||
## Benefits | ||
|
||
1. **Backward Compatibility**: Safes deployed with official mastercopies work exactly as before | ||
2. **Extended Support**: Safes using custom-deployed (but code-identical) mastercopies are now supported | ||
3. **Graceful Fallback**: If VERSION() call fails for any reason, the SDK attempts mastercopy matching before falling back to the default version | ||
4. **Accurate Detection**: By comparing bytecode hashes, we ensure the mastercopy is exactly the same as an official version | ||
5. **Automatic L1/L2 Detection**: The implementation correctly identifies whether a Safe uses an L1 or L2 singleton | ||
|
||
## Use Cases | ||
|
||
This implementation enables the SDK to work with: | ||
- Safes deployed on custom networks or testnets using official Safe bytecode | ||
- Safes where the mastercopy was deployed independently but matches official versions | ||
- Safes where the VERSION() method is inaccessible but the bytecode is correct | ||
|
||
## Testing | ||
|
||
Unit tests have been added in `tests/unit/mastercopy-matcher.test.ts` covering: | ||
- Extracting mastercopy address from storage | ||
- Matching contract code against known versions | ||
- End-to-end detection workflow | ||
- Error handling and edge cases | ||
|
||
## Security Considerations | ||
|
||
- The implementation only accepts mastercopies whose bytecode EXACTLY matches official Safe deployments | ||
- No custom or modified mastercopies are accepted unless they have the identical bytecode hash | ||
- The keccak256 hash comparison provides cryptographic assurance of code identity |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
# Using Safe SDK with Custom Mastercopy Deployments | ||
|
||
## Overview | ||
|
||
The Safe protocol-kit now supports Safe contracts that use custom-deployed mastercopies (also called singletons), as long as the mastercopy bytecode exactly matches an official Safe version. This enables using the SDK on custom networks, testnets, or with independently deployed Safe contracts. | ||
|
||
## How It Works | ||
|
||
When you initialize a Safe instance, the SDK will: | ||
|
||
1. **First attempt**: Call the `VERSION()` method on the Safe contract to determine its version | ||
2. **Fallback mechanism**: If the VERSION() call fails: | ||
- Read the mastercopy address from storage slot 0 of the Safe proxy | ||
- Fetch the bytecode of the mastercopy contract | ||
- Compare the bytecode hash against all known Safe versions (1.0.0, 1.1.1, 1.2.0, 1.3.0, 1.4.1) | ||
- If a match is found, use that version to initialize the SDK | ||
- If no match is found, fall back to the default version (1.3.0) | ||
|
||
## Usage Example | ||
|
||
No changes are required to your existing code! The new functionality works transparently: | ||
|
||
```typescript | ||
import Safe from '@safe-global/protocol-kit' | ||
|
||
// Initialize with a Safe that uses a custom-deployed mastercopy | ||
const safe = await Safe.init({ | ||
provider: 'https://your-rpc-url', | ||
signer: privateKey, | ||
safeAddress: '0xYourSafeAddress' | ||
}) | ||
|
||
// The SDK will automatically: | ||
// 1. Try to call VERSION() | ||
// 2. If that fails, read the mastercopy address | ||
// 3. Match the mastercopy bytecode against known versions | ||
// 4. Initialize with the detected version | ||
|
||
console.log(safe.getContractVersion()) // e.g., "1.3.0" | ||
``` | ||
|
||
## Requirements | ||
|
||
For the mastercopy matching to work, the following conditions must be met: | ||
|
||
1. **Exact bytecode match**: The mastercopy bytecode must be byte-for-byte identical to an official Safe deployment | ||
2. **Contract must be deployed**: Both the Safe proxy and the mastercopy must be deployed on the network | ||
3. **Supported version**: The mastercopy must match one of the supported Safe versions (1.0.0, 1.1.1, 1.2.0, 1.3.0, or 1.4.1) | ||
|
||
## Benefits | ||
|
||
- **Custom network support**: Deploy Safes on your own test network using official Safe bytecode | ||
- **Independent deployments**: Use Safes where the mastercopy was deployed separately | ||
- **Automatic version detection**: No need to manually specify the version | ||
- **Backward compatible**: Existing code works without modifications | ||
|
||
## What Gets Detected | ||
|
||
The mastercopy matching detects: | ||
- **Safe version**: Which Safe contract version (1.0.0, 1.1.1, 1.2.0, 1.3.0, or 1.4.1) | ||
- **Singleton type**: Whether it's an L1 singleton or L2 singleton | ||
- **Mastercopy address**: The address of the matched mastercopy | ||
|
||
## Limitations | ||
|
||
- Only works with official Safe bytecode (no modified versions) | ||
- The mastercopy must be deployed and accessible on the network | ||
- Performance: The first initialization with a custom mastercopy will require additional RPC calls to fetch and compare bytecode | ||
|
||
## Technical Details | ||
|
||
For more technical information about the implementation, see [MASTERCOPY_MATCHING.md](../MASTERCOPY_MATCHING.md). | ||
|
||
## Troubleshooting | ||
|
||
### My Safe initialization fails with "Invalid ... contract address" | ||
|
||
This error means the SDK couldn't find a deployment for the contract type on your network. Make sure: | ||
- The Safe proxy is deployed at the specified address | ||
- The mastercopy referenced by the proxy is also deployed | ||
- You're using the correct RPC endpoint for your network | ||
|
||
### The detected version is incorrect | ||
|
||
If the SDK detects the wrong version, it likely means: | ||
- The mastercopy bytecode has been modified (not an exact match) | ||
- There's an issue with the RPC provider returning incorrect bytecode | ||
|
||
You can always manually specify the version using `contractNetworks`: | ||
|
||
```typescript | ||
const safe = await Safe.init({ | ||
provider: 'https://your-rpc-url', | ||
signer: privateKey, | ||
safeAddress: '0xYourSafeAddress', | ||
contractNetworks: { | ||
[chainId]: { | ||
safeSingletonAddress: '0xYourMastercopyAddress', | ||
safeSingletonAbi: [...] // optional | ||
} | ||
} | ||
}) | ||
``` | ||
|
||
## Example: Using Safe SDK on a Custom Testnet | ||
|
||
```typescript | ||
import Safe from '@safe-global/protocol-kit' | ||
|
||
// Scenario: You've deployed a Safe on a custom testnet using official v1.3.0 bytecode | ||
// The Safe proxy address is 0x123... | ||
// The mastercopy was deployed at 0xabc... | ||
|
||
const safe = await Safe.init({ | ||
provider: 'https://custom-testnet-rpc.example.com', | ||
signer: '0xYourPrivateKey', | ||
safeAddress: '0x123...' | ||
}) | ||
|
||
// The SDK will: | ||
// 1. Call VERSION() on 0x123... (delegates to mastercopy at 0xabc...) | ||
// 2. If that works, use the returned version | ||
// 3. If that fails: | ||
// - Read mastercopy address from storage (gets 0xabc...) | ||
// - Fetch bytecode from 0xabc... | ||
// - Compare with known Safe versions | ||
// - Find it matches v1.3.0 | ||
// - Initialize using v1.3.0 ABI | ||
|
||
console.log(safe.getContractVersion()) // "1.3.0" | ||
|
||
// Now you can use all Safe SDK features normally | ||
const owners = await safe.getOwners() | ||
const threshold = await safe.getThreshold() | ||
// ... etc | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ import { SafeVersion } from '@safe-global/types-kit' | |
import { isSafeConfigWithPredictedSafe } from '../utils/types' | ||
import SafeProvider from '../SafeProvider' | ||
import { getSafeContractVersion } from '@safe-global/protocol-kit/contracts/utils' | ||
import { detectSafeVersionFromMastercopy } from '../utils/mastercopyMatcher' | ||
|
||
class ContractManager { | ||
#contractNetworks?: ContractNetworksConfig | ||
|
@@ -42,18 +43,36 @@ class ContractManager { | |
if (isSafeConfigWithPredictedSafe(config)) { | ||
safeVersion = predictedSafe?.safeDeploymentConfig?.safeVersion ?? DEFAULT_SAFE_VERSION | ||
} else { | ||
let detectedVersion: SafeVersion | undefined | ||
let detectedIsL1: boolean | undefined | ||
|
||
try { | ||
// We try to fetch the version of the Safe from the blockchain | ||
safeVersion = await getSafeContractVersion(safeProvider, safeAddress as string) | ||
} catch (e) { | ||
// if contract is not deployed we use the default version | ||
safeVersion = DEFAULT_SAFE_VERSION | ||
// If contract is not deployed or VERSION() call fails, try mastercopy matching | ||
const mastercopyMatch = await detectSafeVersionFromMastercopy( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. getSafeContractVersion only checks the contract version. The catch clause will not be triggered because the unofficial contract will have the VERSION anyway right? |
||
safeProvider, | ||
safeAddress as string, | ||
chainId, | ||
isL1SafeSingleton | ||
) | ||
|
||
if (mastercopyMatch) { | ||
// Successfully matched the mastercopy to a known version | ||
detectedVersion = mastercopyMatch.version | ||
detectedIsL1 = mastercopyMatch.isL1 | ||
safeVersion = detectedVersion | ||
} else { | ||
// If no match found, use the default version | ||
safeVersion = DEFAULT_SAFE_VERSION | ||
} | ||
} | ||
|
||
this.#safeContract = await getSafeContract({ | ||
safeProvider, | ||
safeVersion, | ||
isL1SafeSingleton, | ||
isL1SafeSingleton: detectedIsL1 ?? isL1SafeSingleton, | ||
customSafeAddress: safeAddress, | ||
customContracts | ||
}) | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export const getProtocolKitVersion = () => '6.1.0' | ||
export const getProtocolKitVersion = () => '6.1.1' |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.