Legacy Contracts Created with Safe SDK
How Safe-owned legacies (Multisig and Transfer) integrate with an existing Safe via Safe Guards and Safe Modules, and how the lifecycle events flow.
A Safe is a multi-signature smart account on Ethereum: a predefined threshold of owner keys is required to execute any transaction. 10102 integrates with Safe using the two extension points Safe exposes to third-party code: Guards and Modules.
Safe Guards β hooks that run on every transaction a Safe executes. 10102 uses a Guard to capture the timestamp of the Safe's last outgoing transaction.
Safe Modules β code that can execute transactions on behalf of the Safe without the usual multi-sig approval flow. 10102 uses a Module to execute the final activation action (transferring assets, or adding beneficiaries as owners) when the inactivity window elapses.
Two legacy flavors live on this path:
Multisig legacy β hands over control of the Safe itself by adding beneficiaries as co-signers on activation.
Transfer legacy (Safe owner) β transfers specific assets out of the Safe to beneficiaries on activation, proportional to configured allocations.
Contract roles
MultisigLegacyRouter
Creates and updates Multisig legacies. Enforces that edits come from the Safe itself (at threshold).
TransferLegacyRouter
Creates and updates Safe-owner Transfer legacies.
Per-legacy MultisigLegacyContract
Stores beneficiaries, activation trigger, name/note for a single Multisig legacy.
Per-legacy TransferLegacyContract
Stores beneficiaries, allocations, asset list, activation trigger for a single Safe-owner Transfer legacy.
SafeGuard
Installed on the owner's Safe. Persists lastOutgoingTxTimestamp on every Safe execution.
SafeLegacyModule
Installed on the owner's Safe. Empowered to execute the activation on behalf of the Safe at threshold-bypass.
All of these live in the public computing-sc repository; addresses are in contract-addresses.json.
Creating a legacy from a Safe
Step 1 β Have a Safe
The user needs an existing Safe wallet. If they don't have one, they create it at app.safe.global with their chosen signer set and threshold. 10102 does not deploy Safes on your behalf.
Step 2 β Connect and configure
The user connects to the 10102 app as a Safe (via WalletConnect or the Safe app embed).
They configure the legacy in the UI: beneficiaries, allocations (Transfer) or threshold-on-activation (Multisig), activation trigger (inactivity window), name/note.
The app builds a Safe transaction bundle containing:
setGuard(SafeGuard)β installs the 10102 guard on the Safe.enableModule(SafeLegacyModule)β enables the 10102 module on the Safe.createLegacy(...)β calls the appropriate router with legacy configuration.
The bundle goes through the Safe's normal multi-sig approval and execution flow: co-signers sign until threshold is reached, then it executes atomically.
Step 3 β On-chain effects
Once the bundle executes:
The Safe emits
ChangeGuardandEnableModuleevents. The subgraph indexes them and marks the Safe as 10102-enabled.The Router emits a
LegacyCreatedevent (exact name varies by router; see the ABIs). The subgraph creates a legacy entity with the Safe address as creator, the beneficiaries, allocations / threshold, and activation trigger.SafeGuardinitializeslastOutgoingTxTimestampto the creation block timestamp.
Activity tracking (the happy path)
Every time the Safe executes any outgoing transaction β not just 10102 ones β the Safe's execution hooks call SafeGuard.checkTransaction(...), which updates lastOutgoingTxTimestamp. This is the entire "heartbeat" mechanism for Safe-owned legacies: no explicit check-in button needed, because normal Safe usage is the check-in. The UI also exposes an explicit I'm still alive action for owners who want a deliberate heartbeat.
Because the Guard lives inside the Safe, activity detection for Safe-owned legacies is fully on-chain and doesn't depend on Chainlink/Moralis oracles β unlike pure-EOA legacies. See Indexing & Activity Tracking for the EOA story.
Editing a legacy
Any Safe owner can initiate an edit (beneficiary changes, allocation changes, name/note, activation trigger). The edit is a normal Safe transaction and requires threshold signatures just like the creation. When it executes:
The per-legacy contract's state is updated.
The Router emits an
LegacyUpdatedevent (name varies); the subgraph updates the entity.SafeGuard.lastOutgoingTxTimestampis updated by the underlying Safe execution β edits implicitly reset the inactivity timer.
One asymmetry worth naming: off-chain notification settings (watchers, email reminders) are managed by PremiumSetting, which gates those edits on the original creator EOA, not the Safe at threshold. So any Safe owner can edit beneficiaries and activation triggers, but only the single EOA who submitted the original creation transaction can edit watchers or reminder configuration. This is tracked as a known asymmetry to resolve via a future PremiumSetting upgrade.
Deleting a legacy
Delete is also a Safe transaction at threshold. The bundle includes:
deleteLegacy(...)β tells the Router to tear down the per-legacy contract state.setGuard(0x0)β removes the 10102 guard from the Safe.disableModule(SafeLegacyModule)β removes the 10102 module from the Safe.
The Safe emits ChangeGuard, DisableModule, and the Router emits a LegacyDeleted event. The subgraph marks the legacy as deleted. The Safe is left in exactly the state it was in before creation: no residual permissions, no residual guards.
Users can also tear down manually via Safe's Transaction Builder at app.safe.global β calling setGuard(0x0) + disableModule(...) on the Safe directly β which is the fallback path if the 10102 UI is ever unavailable.
Activation β the endgame
When a beneficiary attempts to activate a legacy (via the app, via Safe, or via Etherscan using the Legacy Claim Card), the Router checks:
The caller is one of the configured beneficiaries (primary, or contingent after their window elapses).
block.timestamp - SafeGuard.lastOutgoingTxTimestamp >= configuredInactivityWindow.
If both checks pass, the Router invokes the SafeLegacyModule to execute the activation action on behalf of the Safe β bypassing the normal threshold, because the Module is pre-authorized for this one specific action and nothing else.
Multisig legacy activation
The Module executes addOwnerWithThreshold(beneficiary_i, newThreshold) once per beneficiary, then changeThreshold(configuredThreshold) to the value the owner specified at creation. The Safe emits AddedOwner and ChangedThreshold events; the subgraph updates the Safe's owner set and records the legacy as activated. Post-activation, the Safe is now co-owned by the beneficiaries at the new threshold, and everything it holds, stakes, or governs is under their collective control.
Transfer legacy (Safe owner) activation
The Module executes a series of transfer calls β ETH and ERC-20 β moving the configured allocations out of the Safe to the beneficiary addresses. The per-legacy contract emits an Activated event; the subgraph marks it activated. The Safe itself is untouched (it just becomes lighter by the allocated amounts).
Why this design
Keeping the Module strictly single-purpose β authorized only for the activation action, never for arbitrary transactions β preserves the security model of the Safe. No matter how badly the Module were compromised, it can only do the one thing the owner explicitly configured at creation. This is the same pattern Safe itself recommends for third-party integrations.
Last updated