Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/poor-bees-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@peeramid-labs/eds": major
---

distributor instantiate methods are now payable
5 changes: 5 additions & 0 deletions .changeset/witty-icons-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@peeramid-labs/eds": major
---

fixed spelling
2 changes: 2 additions & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
.secrets/
3 changes: 3 additions & 0 deletions .solcover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
skipFiles: ["mocks/", "erc7744/"]
};
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["continous", "extcodecopy", "mstore", "retval", "strg"]
}
118 changes: 65 additions & 53 deletions README.md

Large diffs are not rendered by default.

13 changes: 4 additions & 9 deletions deploy/ERC7744.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,19 @@ const func = async (hre) => {
const { deploy } = deployments;

const { deployer } = await getNamedAccounts();
// generated with cast create2 --starts-with c0de1d
// const bigIntValue = BigInt(
// "50974397650477774973013714513428960054656154370774770732424793889552745009750"
// );
// // Convert to a hexadecimal string
// const salt = "0x" + bigIntValue.toString(16);
const salt = "0x70b27c94ed692bfb60748cee464ef910d4bf768ac1f3a63eeb4c05258f629256";
// generated with cast create2 --starts-with c0de1d
const salt = "0x9425035d50edcd7504fe5eeb5df841cc74fe6cccd82dca6ee75bcdf774bd88d9";

const result = await deploy("ERC7744", {
deterministicDeployment: salt,
from: deployer,
skipIfAlreadyDeployed: true
});

console.log("ERC7744 deployed at", result.address);
hre.network.name !== "hardhat" && console.log("ERC7744 deployed at", result.address);
if (result.bytecode) {
const codeHash = ethers.utils.keccak256(result.bytecode);
console.log(`CodeHash: ${codeHash}`);
hre.network.name !== "hardhat" && console.log(`CodeHash: ${codeHash}`);
}
};

Expand Down
16 changes: 8 additions & 8 deletions docs/contracts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,16 +266,16 @@ function _install(contract IDistributor distributor, bytes32 distributionId, byt
function _uninstall(uint256 instanceId) internal virtual
```

### getInstance
### getApp

```solidity
function getInstance(uint256 instanceId) public view returns (address[] instaneContracts)
function getApp(uint256 instanceId) public view returns (address[] instaneContracts)
```

### getInstancesNum
### getAppsNum

```solidity
function getInstancesNum() public view returns (uint256)
function getAppsNum() public view returns (uint256)
```

### isInstance
Expand Down Expand Up @@ -814,16 +814,16 @@ function install(contract IDistributor distributor, bytes32 distributionId, byte
function uninstall(uint256 instanceId) external
```

### getInstance
### getApp

```solidity
function getInstance(uint256 instanceId) external view returns (address[] instaneContracts)
function getApp(uint256 instanceId) external view returns (address[] instaneContracts)
```

### getInstancesNum
### getAppsNum

```solidity
function getInstancesNum() external view returns (uint256)
function getAppsNum() external view returns (uint256)
```

### isInstance
Expand Down
113 changes: 113 additions & 0 deletions docs/guides/Distributions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Distributions

Distributions are smart contracts serving as the primary mechanism within EDS for deploying instances of specific contract logic. They encapsulate the source code (or a reference to it) and provide methods to instantiate it, potentially applying versioning or upgradability patterns.

Distributions are generally designed to be stateless, focusing solely on deploying logic. Managing the state *of* deployed instances is typically handled by users or specialized [Distributors](./Distributors.md).

> [!WARNING]
> Deploying stateful logic directly within a Distribution contract itself is discouraged. It complicates upgrades and may interfere with indexers like the [Indexer](./Indexer.md) that rely on bytecode hashes. Use a [Distributor](./Distributors.md) for managing stateful application logic built upon stateless Distribution sources.

## Distribution Types

EDS provides several base distribution contracts:

### 1. Clone-Based Distributions (`CloneDistribution`)

The abstract `CloneDistribution` contract (`src/distributions/CloneDistribution.sol`) serves as a base for distributions that deploy new instances using `Clones.clone`. Concrete implementations must override the `sources()` function to return the address(es) of the logic contract(s) to be cloned, along with the distribution's name and version.

Its `instantiate` function (called via `_instantiate`) clones the source(s) and emits a `Distributed` event.

#### Example: `CodeHashDistribution`

`CodeHashDistribution` (`src/distributions/CodeHashDistribution.sol`) extends `CloneDistribution`. It's initialized with a `codeHash` (referencing code indexed by the [Indexer](./Indexer.md)), metadata, name, and version. Its `sources()` implementation resolves the `codeHash` to the deployed contract address using `LibERC7744.getContainerOrThrow()` and returns that address to be cloned.

```solidity
// Example: Deploying a CodeHashDistribution
// Assume MyLogicContract code is indexed and its hash is CODE_HASH
bytes32 name = "MyDistribution";
uint256 version = LibSemver.toUint256(LibSemver.Version(1, 0, 0));
bytes32 metadata = bytes32(0); // Optional metadata
CodeHashDistribution myDist = new CodeHashDistribution(CODE_HASH, metadata, name, version);

// Later, anyone can instantiate it (if stateless)
(address[] memory instances, ,) = myDist.instantiate(""); // data is unused in base CloneDistribution
address myInstance = instances[0];
```

#### Example: `LatestVersionDistribution`

`LatestVersionDistribution` (`src/distributions/LatestVersionDistribution.sol`) also extends `CloneDistribution`. It links to a [Repository](./Repositories.md). Its `sources()` implementation calls `repository.getLatest()` to find the `sourceId` of the latest version in the repository, resolves it to an address using `LibERC7744.getContainerOrThrow()`, and returns that address to be cloned. This ensures users always instantiate the most recent version available in the linked repository.

### 2. Upgradable Distributions (`UpgradableDistribution`)

The `UpgradableDistribution` contract (`src/distributions/UpgradableDistribution.sol`) provides a mechanism for deploying instances that follow a specific upgradability pattern, integrating with [Distributors](./Distributors.md) and the [Upgradability](./Upgradability.md) flow. It does *not* inherit from `CloneDistribution`.

**Mechanism:**

1. It's initialized similarly to `CodeHashDistribution` with a `codeHash`, metadata, name, and version.
2. Its `instantiate` function requires ABI-encoded `data` containing the `installer` address and initialization `args` for the proxy.
3. Crucially, `instantiate` deploys a `WrappedTransparentUpgradeableProxy` (`src/proxies/WrappedTransparentUpgradeableProxy.sol`) for each source.
4. When creating the `WrappedTransparentUpgradeableProxy`:
* The logic contract address (resolved from `codeHash`) is set as the implementation.
* The **installer** address (from `data`) is set as the proxy's *owner*.
* The **distributor** address (`msg.sender` of the `instantiate` call) is set as the proxy's *admin* and registered as the sole middleware layer using `ERC7746Hooked` (`src/middleware/ERC7746Hooked.sol`).
5. This setup means:
* Standard proxy owner functions (like changing the admin) are controlled by the **installer**.
* Upgrades (changing the implementation) are controlled by the **distributor** acting as the `ProxyAdmin` *and* the middleware layer. The upgrade process happens via the distributor calling the proxy, triggering the ERC7746 hook flow detailed in the [Upgradability](./Upgradability.md) guide. This flow may involve checks requiring installer consent, depending on the Distributor's implementation.

```solidity
// Example: Instantiating an UpgradableDistribution
// Assume upDist is an instance of UpgradableDistribution
// Assume distributorContract is the address of the Distributor managing upgrades
// Assume installerAddress is the end-user installing the instance
// Assume initArgs is the ABI encoded initialization data for the logic contract

bytes memory data = abi.encode(installerAddress, initArgs);

// Called by the distributor:
(address[] memory instances, ,) = distributorContract.call{value: 0}( // Or however the distributor triggers instantiation
abi.encodeWithSelector(
upDist.instantiate.selector,
data
)
);
address proxyInstance = instances[0];

// Now, proxyInstance points to the logic, owned by installerAddress,
// with upgrades managed by distributorContract via ERC7746 middleware.
```

For comprehensive details on the multi-party trust process for upgrades, refer to the [Upgradability](./Upgradability.md) documentation. Management of versions and migration scripts on the developer side is handled via the [Repository](./Repositories.md).

## CLI

> [!NOTE]
> The CLI provides utilities to simplify deploying and managing distributions. It may abstract some of the underlying contract details.

```bash
# Deploy a new distribution (CLI might determine type based on flags/sources)
# Example using source addresses (likely creates CloneDistribution or similar internally)
eds distribution deploy <source-addresses> --name <distribution-name> --version <version> [--uri <uri>] [--proxy-type <Clonable?>] # Check CLI help for exact flags

# Example using source code hashes (likely creates CodeHashDistribution or UpgradableDistribution)
eds distribution deploy-with-hashes <source-hashes> --name <distribution-name> --version <version> [--uri <uri>] [--proxy-type <Upgradable|Clonable?>] # Check CLI help

# Get information about a distribution (calls get() and contractURI())
eds distribution <address> info

# Instantiate a distribution (calls instantiate())
# Note: For UpgradableDistribution, '--data' needs correct encoding (installer, args)
eds distribution <address> instantiate [--data <hex-data>]

# Verify a distribution's source code matches code hashes (likely specific to hash-based distributions)
eds distribution <address> verify

# Common options available for all commands
--rpc-url <url> # RPC endpoint
--private-key <key> # Private key for transactions
--gas-limit <limit> # Optional gas limit
--gas-price <price> # Optional gas price
```



132 changes: 132 additions & 0 deletions docs/guides/Distributors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Distributors

Distributors are smart contracts that manage the lifecycle, versioning, and distribution of code within EDS. They serve as trusted intermediaries between contracts and end-users, providing a management layer on top of stateless [Distributions](./Distributions.md).

## Core Concepts

The distributor system is built around the abstract `Distributor` contract (`src/distributors/Distributor.sol`), which implements the `IDistributor` interface.

Key functionalities include:

* **Distribution Registry:** Maintains a registry of distribution components, identified by a unique `distributorId`. Each distribution component consists of:
* A `distributionLocation` (the address of either an `IDistribution` contract or an `IRepository` contract)
* An optional `initializer` contract address (used for specialized initialization logic during instantiation)
* For versioned distributions, it tracks version requirements using `LibSemver`

* **App Instance Management:** Creates and tracks deployed instances of distributions:
* Each time a distribution is instantiated, it's assigned a unique `appId` and the instantiated contract addresses are recorded
* Maintains mappings between app components, app IDs, and distributions
* Records the installed version of each app instance

* **Versioning and Migrations:** For versioned distributions (backed by a `Repository`):
* Tracks version requirements for distributions
* Manages migration plans between different versions
* Provides functionality to upgrade app instances from one version to another

* **ERC7746 Middleware Integration:**
* Implements the ERC7746 middleware pattern as a security layer
* Provides hooks (`beforeCall`, `afterCall`) that are triggered by proxies during upgrade attempts
* Uses these hooks to enforce that only authorized parties (the distributor and the app's installer/owner) can perform upgrades

## Distributor Implementations

Several concrete implementations are provided:

### 1. `OwnableDistributor`

The `OwnableDistributor` contract (`src/distributors/OwnableDistributor.sol`) extends the base `Distributor` with OpenZeppelin's `Ownable` access control. This restricts administration functions to a designated owner, who can:

* Add new distributions (both unversioned via code hash or versioned via repository)
* Change version requirements for distributions
* Disable distributions
* Add or remove version migrations
* The `instantiate` function, however, is public, allowing anyone to create instances of registered distributions

### 2. `TokenizedDistributor`

The `TokenizedDistributor` contract (`src/distributors/TokenizedDistributor.sol`) introduces a payment model to the distributor pattern. Key features:

* Requires payment in a specified ERC20 token to instantiate a distribution
* Configurable fees per distribution (or a default fee)
* Payments are forwarded to a designated beneficiary
* This model allows for monetizing access to managed distributions

### 3. Special Purpose: `WrappedProxyInitializer`

The `WrappedProxyInitializer` contract (`src/distributors/WrappedProxyInitializer.sol`) is not a distributor itself, but a helper contract that implements the `IInitializer` interface. It's used by distributors to:

* Set up the instantiation flow for `UpgradableDistribution` contracts
* Properly encode the installer address (the `msg.sender` that calls the distributor) and initialization data
* Handle error propagation during instantiation
* This initializer enables distributors to create proxies where the caller becomes the proxy owner/installer, while the distributor remains as the admin/middleware

## Upgrade Flow with ERC7746

A key feature of distributors is their role in the multi-party upgrade process:

1. A distributor maintains a registry of app instances and their versions
2. When an upgrade is initiated (via `upgradeUserInstance`), the distributor:
* Verifies that the app is valid and a migration plan exists for the target version
* Executes the migration, which may involve calling a migration script or directly upgrading the proxy
* During the upgrade, the proxy's ERC7746 middleware call is intercepted by the distributor's hooks
* The hooks verify that both the distributor and the app's installer consent to the upgrade
3. This design ensures that neither the distributor nor the installer can unilaterally force an upgrade

For details on how this works at the proxy level, see the [Upgradability](./Upgradability.md) documentation.

## CLI Operations

> [!NOTE]
> The CLI provides utilities to manage distributors.

```bash
# Deploy a new distributor
eds distributor deploy --name <distributor-name> [--options]

# List all distributions in a distributor
eds distributor <address> list [--format json|table]

# Get info about a specific distribution
eds distributor <address> info <distribution-name>

# Add an unversioned distribution
eds distributor <address> add unversioned <distribution-codehash> <initializer-address> --name <distribution-name>

# Add a versioned distribution
eds distributor <address> add versioned <repository-address> <initializer-address> --name <distribution-name> --version <version-requirement>

# Remove a distribution
eds distributor <address> remove <distribution-name>

# Change the version requirement for a distribution
eds distributor <address> version change <distribution-name> --version <version-requirement>

# Add a migration for version upgrades
eds distributor <address> migration add <migration-hash> --name <distribution-name> --from-version <version> --to-version <version> --strategy <CALL|DELEGATECALL|DELEGATE_REPOSITORY> [--calldata <data>]

# Remove a migration
eds distributor <address> migration remove <migration-hash>

# List all migrations
eds distributor <address> migration list [--distribution <n>]

# Enable a distribution
eds distributor <address> enable <distribution-name>

# Disable a distribution
eds distributor <address> disable <distribution-name>

# Common options available for all commands
--rpc-url <url> # RPC endpoint
--private-key <key> # Private key for transactions
--gas-limit <limit> # Optional gas limit
--gas-price <price> # Optional gas price
```








Loading