Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
f19b7d4
Minimal init for cross chain support
joYyHack Aug 26, 2025
a461fab
extend subscription cross chain logic
joYyHack Aug 27, 2025
871da46
Apply subscriptions sync per all subscription managers
joYyHack Aug 31, 2025
e100a9b
Add side chain subscription manager
joYyHack Sep 2, 2025
897e107
update syncSubscriptions function implementation
joYyHack Sep 4, 2025
c0b9c38
Adjustments
joYyHack Sep 4, 2025
338b51f
Merge branch 'master' into feature/crosschain-support
joYyHack Sep 4, 2025
7462af9
remove duplicate
joYyHack Sep 4, 2025
25c5770
Move events to internal functions
joYyHack Sep 5, 2025
9b31325
Fix typo
joYyHack Sep 5, 2025
492e085
Move event to interface
joYyHack Sep 5, 2025
2822c49
Rename
joYyHack Sep 5, 2025
fa1a5ea
Remove line
joYyHack Sep 5, 2025
ab4630a
Remove unnecessary check
joYyHack Sep 5, 2025
fa1619f
init smt tree in initialize
joYyHack Sep 5, 2025
e467093
convert modifier into function
joYyHack Sep 5, 2025
912bc03
Add latest synced SMT root
joYyHack Sep 5, 2025
54d41fc
Remove line
joYyHack Sep 5, 2025
fdcb5e7
Convert modifier into function
joYyHack Sep 5, 2025
e7f7e2f
Minor update for setEndTime
joYyHack Sep 5, 2025
9e64ead
Add functions for gas limit + move events to internal functions
joYyHack Sep 5, 2025
be0ac35
Adjustments
joYyHack Sep 5, 2025
a5768fc
Adjust conditions
joYyHack Sep 5, 2025
07b1648
Update return param
joYyHack Sep 5, 2025
c3da4d3
Add virtual
joYyHack Sep 5, 2025
5b56fa7
Adjustment
joYyHack Sep 5, 2025
8d3e5cd
Add modifier
joYyHack Sep 5, 2025
a20d9d7
Send leftover back to msg.sender
joYyHack Sep 5, 2025
b09cc9e
Add nonReentrant
joYyHack Sep 5, 2025
6545863
Add base side chain manager
joYyHack Sep 5, 2025
c1ce4a7
Update solarity and apply changes
joYyHack Sep 9, 2025
a76641e
Fix tests + minor adjustments
joYyHack Sep 10, 2025
52853cf
Merge branch 'master' into feature/crosschain-support
joYyHack Sep 10, 2025
2432148
Adjustments
joYyHack Sep 10, 2025
938af51
Add natspec
joYyHack Sep 10, 2025
3f2de35
Add foundry to CI
joYyHack Sep 10, 2025
fa43839
Adjust CI
joYyHack Sep 10, 2025
7e631be
CI Adjustment
joYyHack Sep 10, 2025
98f1e09
Add some tests + adjustments
joYyHack Sep 11, 2025
2f35fd3
Adjustments
joYyHack Sep 15, 2025
edf7ac5
Tests + adjustments
joYyHack Sep 16, 2025
0251ffd
update mainnet migrations
joYyHack Sep 16, 2025
b357552
update readme
joYyHack Sep 16, 2025
6942e0a
update readme
joYyHack Sep 16, 2025
8f330bb
config upate
joYyHack Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,13 @@ runs:
- name: Install packages
run: npm install
shell: bash

- name: Setup foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Compile Foundry Contracts
run: forge build
shell: bash
working-directory: ./
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ generated-types

# Hardhat migrate
.storage.json

# Foundry
out/
cache_forge/solidity-files-cache.json
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/wormhole-solidity-sdk"]
path = lib/wormhole-solidity-sdk
url = https://github.com/wormhole-foundation/wormhole-solidity-sdk
50 changes: 47 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
Unforgettable Contracts is a set of Solidity smart contracts designed for smart account or vault access recovery
on top of the [EIP-7947](https://eips.ethereum.org/EIPS/eip-7947).

It provides mechanisms for managing different recovery methods with subscription-based access.
It provides mechanisms for managing different recovery methods with subscription-based access, including crosschain
subscription synchronization via Wormhole.

### Contracts

Expand All @@ -22,12 +23,18 @@ contracts
│ ├── subscription
│ │ ├── modules
│ │ │ ├── BaseSubscriptionModule — "Basic subscription extension logic"
│ │ │ ├── CrossChainModule — "Crosschain subscription synchronization module"
│ │ │ ├── SBTPaymentModule — "Subscription payments using SBTs mapped to fixed durations"
│ │ │ ├── SignatureSubscriptionModule — "Subscription extension using signed EIP-712 permits"
│ │ │ └── TokensPaymentModule — "Subscription payments in ETH or ERC-20 tokens"
│ │ ├── BaseSideChainSubscriptionManager — "Base subscription manager for side chains"
│ │ └── BaseSubscriptionManager — "Subscription manager coordinating multiple payment modules"
│ ├── HelperDataRegistry — "Helper data storage updated via EIP-712 signatures"
│ └── RecoveryManager — "Central recovery coordinator with pluggable strategies"
├── crosschain
│ ├── SideChainSubscriptionManager — "Subscription manager for side chains with crosschain sync support"
│ ├── SubscriptionsStateReceiver — "Receives and processes crosschain subscription state updates"
│ └── SubscriptionsSynchronizer — "Synchronizes subscription states across chains via Wormhole"
├── libs
│ ├── EIP712SignatureChecker — "A wrapper around OZ SignatureChecker for EIP-712 signature validations"
│ └── TokensHelper — "ETH/ERC-20 transfer utilities"
Expand All @@ -41,14 +48,51 @@ contracts
└── VaultSubscriptionManager — "Vault-specific subscription manager"
```

#### Usage
### Setup

Install all the required dependencies:
This project uses both Hardhat and Foundry. Follow these steps to set up the repository:

#### Prerequisites

1. **Install Node.js** (v18 or later)
2. **Install Foundry**:
```bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
```

#### Installation

Install all required dependencies:

```bash
npm install
```

Initialize Foundry submodules:

```bash
git submodule update --init --recursive
```

### Usage

#### Compilation

To compile contracts using Hardhat:

```bash
npm run compile
```

To compile contracts using Foundry:

```bash
forge build
```

#### Testing

To run the tests, execute the following command:

```bash
Expand Down
3 changes: 2 additions & 1 deletion contracts/accounts/AccountSubscriptionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ contract AccountSubscriptionManager is
initData_.subscriptionCreators,
initData_.tokensPaymentInitData,
initData_.sbtPaymentInitData,
initData_.sigSubscriptionInitData
initData_.sigSubscriptionInitData,
initData_.crossChainInitData
);
}

Expand Down
4 changes: 3 additions & 1 deletion contracts/core/RecoveryManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ contract RecoveryManager is IRecoveryManager, ADeployerGuard, OwnableUpgradeable
}

/// @inheritdoc IRecoveryManager
function subscriptionManagerExists(address subscriptionManager_) public view returns (bool) {
function subscriptionManagerExists(
address subscriptionManager_
) public view override returns (bool) {
return _getRecoveryManagerStorage().subscriptionManagers.contains(subscriptionManager_);
}

Expand Down
186 changes: 186 additions & 0 deletions contracts/core/subscription/BaseSideChainSubscriptionManager.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";

import {SparseMerkleTree} from "@solarity/solidity-lib/libs/data-structures/SparseMerkleTree.sol";

import {IBaseSideChainSubscriptionManager} from "../../interfaces/core/IBaseSideChainSubscriptionManager.sol";
import {ISubscriptionsStateReceiver} from "../../interfaces/crosschain/ISubscriptionsStateReceiver.sol";
import {BaseSubscriptionModule} from "./modules/BaseSubscriptionModule.sol";

abstract contract BaseSideChainSubscriptionManager is
IBaseSideChainSubscriptionManager,
BaseSubscriptionModule,
OwnableUpgradeable,
ReentrancyGuardUpgradeable,
PausableUpgradeable,
UUPSUpgradeable
{
bytes32 private constant BASE_SIDECHAIN_SUBSCRIPTION_MANAGER_STORAGE_SLOT =
keccak256("unforgettable.contract.base.sidechain.subscription.manager.storage");

struct BaseSideChainSubscriptionManagerStorage {
ISubscriptionsStateReceiver subscriptionsStateReceiver;
address sourceSubscriptionManager;
}

function _getBaseSideChainSubscriptionManagerStorage()
private
pure
returns (BaseSideChainSubscriptionManagerStorage storage _bscsms)
{
bytes32 slot_ = BASE_SIDECHAIN_SUBSCRIPTION_MANAGER_STORAGE_SLOT;

assembly ("memory-safe") {
_bscsms.slot := slot_
}
}

function __BaseSideChainSubscriptionManager_init(
BaseSideChainSubscriptionManagerInitData memory initData_
) public onlyInitializing {
__Ownable_init(msg.sender);

_setSubscriptionsStateReceiver(initData_.subscriptionsStateReceiver);
_setSourceSubscriptionManager(initData_.sourceSubscriptionManager);
}

/**
* @notice A function to set a new SubscriptionsStateReceiver contract.
* @param subscriptionsStateReceiver_ The address of the SubscriptionsStateReceiver contract.
*/
function setSubscriptionsStateReceiver(
address subscriptionsStateReceiver_
) external onlyOwner {
_setSubscriptionsStateReceiver(subscriptionsStateReceiver_);
}

/**
* @notice A function to set a new source SubscriptionManager contract.
* @param sourceSubscriptionManager_ The address of the source SubscriptionManager contract.
*/
function setSourceSubscriptionManager(address sourceSubscriptionManager_) external onlyOwner {
_setSourceSubscriptionManager(sourceSubscriptionManager_);
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function pause() public virtual onlyOwner {
_pause();
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function unpause() public virtual onlyOwner {
_unpause();
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function syncSubscription(
address account_,
AccountSubscriptionData calldata subscriptionData_,
SparseMerkleTree.Proof calldata proof_
) public virtual whenNotPaused nonReentrant {
_verifyProof(account_, subscriptionData_, proof_);

if (getSubscriptionStartTime(account_) == 0) {
_setStartTime(account_, subscriptionData_.startTime);
}

if (subscriptionData_.endTime > getSubscriptionEndTime(account_)) {
_setEndTime(account_, subscriptionData_.endTime);
}

emit SubscriptionSynced(account_, subscriptionData_.startTime, subscriptionData_.endTime);
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function implementation() external view returns (address) {
return ERC1967Utils.getImplementation();
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function getSubscriptionsStateReceiver() external view returns (address) {
return address(_getBaseSideChainSubscriptionManagerStorage().subscriptionsStateReceiver);
}

/// @inheritdoc IBaseSideChainSubscriptionManager
function getSourceSubscriptionManager() external view returns (address) {
return _getBaseSideChainSubscriptionManagerStorage().sourceSubscriptionManager;
}

function _setSubscriptionsStateReceiver(address subscriptionsStateReceiver_) internal {
_checkAddress(subscriptionsStateReceiver_, "SubscriptionsStateReceiver");

_getBaseSideChainSubscriptionManagerStorage()
.subscriptionsStateReceiver = ISubscriptionsStateReceiver(subscriptionsStateReceiver_);

emit SubscriptionsStateReceiverUpdated(subscriptionsStateReceiver_);
}

function _setSourceSubscriptionManager(address sourceSubscriptionManager_) internal {
_checkAddress(sourceSubscriptionManager_, "SourceSubscriptionManager");

_getBaseSideChainSubscriptionManagerStorage()
.sourceSubscriptionManager = sourceSubscriptionManager_;

emit SourceSubscriptionManagerUpdated(sourceSubscriptionManager_);
}

// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImplementation_) internal override onlyOwner {}

function _verifyProof(
address account_,
AccountSubscriptionData calldata subscriptionData,
SparseMerkleTree.Proof calldata proof_
) internal view {
BaseSideChainSubscriptionManagerStorage
storage $ = _getBaseSideChainSubscriptionManagerStorage();

require(
proof_.key == keccak256(abi.encode($.sourceSubscriptionManager, account_)),
InvalidProofKey()
);
require(
proof_.value ==
keccak256(
abi.encode(
$.sourceSubscriptionManager,
account_,
subscriptionData.startTime,
subscriptionData.endTime
)
),
InvalidProofValue()
);

bytes32 root_ = SparseMerkleTree.processProof(_hash2, _hash3, proof_);

require($.subscriptionsStateReceiver.rootInHistory(root_), UnknownRoot(root_));
}

function _hash2(bytes32 a_, bytes32 b_) private pure returns (bytes32 result_) {
assembly {
mstore(0, a_)
mstore(32, b_)

result_ := keccak256(0, 64)
}
}

function _hash3(bytes32 a_, bytes32 b_, bytes32 c) private pure returns (bytes32 result_) {
assembly {
let freePtr_ := mload(64)

mstore(freePtr_, a_)
mstore(add(freePtr_, 32), b_)
mstore(add(freePtr_, 64), c)

result_ := keccak256(freePtr_, 96)
}
}
}
24 changes: 23 additions & 1 deletion contracts/core/subscription/BaseSubscriptionManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ import {ITokensPaymentModule} from "../../interfaces/core/subscription/ITokensPa
import {ISBTPaymentModule} from "../../interfaces/core/subscription/ISBTPaymentModule.sol";
import {ISignatureSubscriptionModule} from "../../interfaces/core/subscription/ISignatureSubscriptionModule.sol";

import {BaseSubscriptionModule} from "./modules/BaseSubscriptionModule.sol";
import {SBTPaymentModule} from "./modules/SBTPaymentModule.sol";
import {TokensPaymentModule} from "./modules/TokensPaymentModule.sol";
import {SignatureSubscriptionModule} from "./modules/SignatureSubscriptionModule.sol";
import {CrossChainModule} from "./modules/CrossChainModule.sol";

abstract contract BaseSubscriptionManager is
ISubscriptionManager,
TokensPaymentModule,
SBTPaymentModule,
SignatureSubscriptionModule,
CrossChainModule,
OwnableUpgradeable,
ReentrancyGuardUpgradeable,
PausableUpgradeable,
Expand Down Expand Up @@ -57,7 +60,8 @@ abstract contract BaseSubscriptionManager is
address[] calldata subscriptionCreators_,
TokensPaymentModuleInitData calldata tokensPaymentInitData_,
SBTPaymentModuleInitData calldata sbtPaymentInitData_,
SigSubscriptionModuleInitData calldata sigSubscriptionInitData_
SigSubscriptionModuleInitData calldata sigSubscriptionInitData_,
CrossChainModuleInitData calldata crossChainInitData_
) public onlyInitializing {
__Ownable_init(msg.sender);
__ReentrancyGuard_init();
Expand All @@ -69,6 +73,7 @@ abstract contract BaseSubscriptionManager is
__TokensPaymentModule_init(tokensPaymentInitData_);
__SBTPaymentModule_init(sbtPaymentInitData_);
__SignatureSubscriptionModule_init(sigSubscriptionInitData_);
__CrossChainModule_init(crossChainInitData_);
}

/// @inheritdoc ISubscriptionManager
Expand Down Expand Up @@ -159,6 +164,16 @@ abstract contract BaseSubscriptionManager is
_setSubscriptionSigner(newSigner_);
}

/**
* @notice A function to set a new SubscriptionsSynchronizer contract.
* @param subscriptionSynchronizer_ Address of the new SubscriptionsSynchronizer contract.
*/
function setSubscriptionSynchronizer(
address subscriptionSynchronizer_
) public virtual onlyOwner {
_setSubscriptionSynchronizer(subscriptionSynchronizer_);
}

/// @inheritdoc ISubscriptionManager
function createSubscription(address account_) public virtual onlySubscriptionCreator {
_createSubscription(account_);
Expand Down Expand Up @@ -246,6 +261,13 @@ abstract contract BaseSubscriptionManager is
emit SubscriptionCreated(account_, block.timestamp);
}

function _extendSubscription(
address account_,
uint64 duration_
) internal virtual override(CrossChainModule, BaseSubscriptionModule) {
super._extendSubscription(account_, duration_);
}

// solhint-disable-next-line no-empty-blocks
function _authorizeUpgrade(address newImplementation_) internal override onlyOwner {}

Expand Down
Loading