|
| 1 | +# Using Safe SDK with Custom Mastercopy Deployments |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +## How It Works |
| 8 | + |
| 9 | +When you initialize a Safe instance, the SDK will: |
| 10 | + |
| 11 | +1. **First attempt**: Call the `VERSION()` method on the Safe contract to determine its version |
| 12 | +2. **Fallback mechanism**: If the VERSION() call fails: |
| 13 | + - Read the mastercopy address from storage slot 0 of the Safe proxy |
| 14 | + - Fetch the bytecode of the mastercopy contract |
| 15 | + - Compare the bytecode hash against all known Safe versions (1.0.0, 1.1.1, 1.2.0, 1.3.0, 1.4.1) |
| 16 | + - If a match is found, use that version to initialize the SDK |
| 17 | + - If no match is found, fall back to the default version (1.3.0) |
| 18 | + |
| 19 | +## Usage Example |
| 20 | + |
| 21 | +No changes are required to your existing code! The new functionality works transparently: |
| 22 | + |
| 23 | +```typescript |
| 24 | +import Safe from '@safe-global/protocol-kit' |
| 25 | + |
| 26 | +// Initialize with a Safe that uses a custom-deployed mastercopy |
| 27 | +const safe = await Safe.init({ |
| 28 | + provider: 'https://your-rpc-url', |
| 29 | + signer: privateKey, |
| 30 | + safeAddress: '0xYourSafeAddress' |
| 31 | +}) |
| 32 | + |
| 33 | +// The SDK will automatically: |
| 34 | +// 1. Try to call VERSION() |
| 35 | +// 2. If that fails, read the mastercopy address |
| 36 | +// 3. Match the mastercopy bytecode against known versions |
| 37 | +// 4. Initialize with the detected version |
| 38 | + |
| 39 | +console.log(safe.getContractVersion()) // e.g., "1.3.0" |
| 40 | +``` |
| 41 | + |
| 42 | +## Requirements |
| 43 | + |
| 44 | +For the mastercopy matching to work, the following conditions must be met: |
| 45 | + |
| 46 | +1. **Exact bytecode match**: The mastercopy bytecode must be byte-for-byte identical to an official Safe deployment |
| 47 | +2. **Contract must be deployed**: Both the Safe proxy and the mastercopy must be deployed on the network |
| 48 | +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) |
| 49 | + |
| 50 | +## Benefits |
| 51 | + |
| 52 | +- **Custom network support**: Deploy Safes on your own test network using official Safe bytecode |
| 53 | +- **Independent deployments**: Use Safes where the mastercopy was deployed separately |
| 54 | +- **Automatic version detection**: No need to manually specify the version |
| 55 | +- **Backward compatible**: Existing code works without modifications |
| 56 | + |
| 57 | +## What Gets Detected |
| 58 | + |
| 59 | +The mastercopy matching detects: |
| 60 | +- **Safe version**: Which Safe contract version (1.0.0, 1.1.1, 1.2.0, 1.3.0, or 1.4.1) |
| 61 | +- **Singleton type**: Whether it's an L1 singleton or L2 singleton |
| 62 | +- **Mastercopy address**: The address of the matched mastercopy |
| 63 | + |
| 64 | +## Limitations |
| 65 | + |
| 66 | +- Only works with official Safe bytecode (no modified versions) |
| 67 | +- The mastercopy must be deployed and accessible on the network |
| 68 | +- Performance: The first initialization with a custom mastercopy will require additional RPC calls to fetch and compare bytecode |
| 69 | + |
| 70 | +## Technical Details |
| 71 | + |
| 72 | +For more technical information about the implementation, see [MASTERCOPY_MATCHING.md](../MASTERCOPY_MATCHING.md). |
| 73 | + |
| 74 | +## Troubleshooting |
| 75 | + |
| 76 | +### My Safe initialization fails with "Invalid ... contract address" |
| 77 | + |
| 78 | +This error means the SDK couldn't find a deployment for the contract type on your network. Make sure: |
| 79 | +- The Safe proxy is deployed at the specified address |
| 80 | +- The mastercopy referenced by the proxy is also deployed |
| 81 | +- You're using the correct RPC endpoint for your network |
| 82 | + |
| 83 | +### The detected version is incorrect |
| 84 | + |
| 85 | +If the SDK detects the wrong version, it likely means: |
| 86 | +- The mastercopy bytecode has been modified (not an exact match) |
| 87 | +- There's an issue with the RPC provider returning incorrect bytecode |
| 88 | + |
| 89 | +You can always manually specify the version using `contractNetworks`: |
| 90 | + |
| 91 | +```typescript |
| 92 | +const safe = await Safe.init({ |
| 93 | + provider: 'https://your-rpc-url', |
| 94 | + signer: privateKey, |
| 95 | + safeAddress: '0xYourSafeAddress', |
| 96 | + contractNetworks: { |
| 97 | + [chainId]: { |
| 98 | + safeSingletonAddress: '0xYourMastercopyAddress', |
| 99 | + safeSingletonAbi: [...] // optional |
| 100 | + } |
| 101 | + } |
| 102 | +}) |
| 103 | +``` |
| 104 | + |
| 105 | +## Example: Using Safe SDK on a Custom Testnet |
| 106 | + |
| 107 | +```typescript |
| 108 | +import Safe from '@safe-global/protocol-kit' |
| 109 | + |
| 110 | +// Scenario: You've deployed a Safe on a custom testnet using official v1.3.0 bytecode |
| 111 | +// The Safe proxy address is 0x123... |
| 112 | +// The mastercopy was deployed at 0xabc... |
| 113 | + |
| 114 | +const safe = await Safe.init({ |
| 115 | + provider: 'https://custom-testnet-rpc.example.com', |
| 116 | + signer: '0xYourPrivateKey', |
| 117 | + safeAddress: '0x123...' |
| 118 | +}) |
| 119 | + |
| 120 | +// The SDK will: |
| 121 | +// 1. Call VERSION() on 0x123... (delegates to mastercopy at 0xabc...) |
| 122 | +// 2. If that works, use the returned version |
| 123 | +// 3. If that fails: |
| 124 | +// - Read mastercopy address from storage (gets 0xabc...) |
| 125 | +// - Fetch bytecode from 0xabc... |
| 126 | +// - Compare with known Safe versions |
| 127 | +// - Find it matches v1.3.0 |
| 128 | +// - Initialize using v1.3.0 ABI |
| 129 | + |
| 130 | +console.log(safe.getContractVersion()) // "1.3.0" |
| 131 | + |
| 132 | +// Now you can use all Safe SDK features normally |
| 133 | +const owners = await safe.getOwners() |
| 134 | +const threshold = await safe.getThreshold() |
| 135 | +// ... etc |
| 136 | +``` |
0 commit comments