Skip to content

16 distributor upgrades handler #53

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

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
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
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/"]
};
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"cSpell.words": ["mstore", "retval", "strg"]
"cSpell.words": ["continous", "extcodecopy", "mstore", "retval", "strg"]
}
39 changes: 39 additions & 0 deletions contracts/mocks/MockFailingDistribution.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../../src/interfaces/IDistribution.sol";
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";

contract MockFailingDistribution is IDistribution {
ShortString private immutable distributionName;
uint256 private constant DISTRIBUTION_VERSION = 1;

constructor() {
distributionName = ShortStrings.toShortString("MockFailingDistribution");
}

function instantiate(bytes memory args) external override returns (address[] memory instances, bytes32 name, uint256 version) {
// Check if the args contain a specific "FAIL" string to trigger a revert
if (args.length > 0 && keccak256(args) == keccak256(abi.encode(bytes32("FAIL")))) {
revert("Intentional instantiation failure");
}

// Normal instantiation
instances = new address[](1);
instances[0] = address(this);
emit Distributed(msg.sender, instances);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function get() external view override returns (address[] memory sources, bytes32 name, uint256 version) {
sources = new address[](1);
sources[0] = address(this);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function contractURI() external pure returns (string memory) {
return "ipfs://mockFailingDistribution";
}
}
38 changes: 38 additions & 0 deletions contracts/mocks/MockLowLevelDistribution.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../../src/interfaces/IDistribution.sol";
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";

contract MockLowLevelDistribution is IDistribution {
ShortString private immutable distributionName;
uint256 private constant DISTRIBUTION_VERSION = 1;

constructor() {
distributionName = ShortStrings.toShortString("MockLowLevelDistribution");
}
Comment on lines +7 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider standardizing mock distribution implementations.

This mock implements a similar functionality to src/mocks/MockLowLevelDistribution.sol but uses a different approach. Consider standardizing these implementations or clarifying the purpose of having two different versions.

Run the following to understand the usage differences between the two implementations:


🏁 Script executed:

#!/bin/bash
# Find where these mocks are being used in tests
echo "==== Usage of MockLowLevelDistribution in tests ===="
rg -l "MockLowLevelDistribution" --glob "test/**/*.ts" --glob "test/**/*.js"

# Analyze actual imports in test files
echo "==== Actual import statements ===="
rg "import.*MockLowLevelDistribution" --glob "test/**/*.ts" --glob "test/**/*.js"

Length of output: 378


🏁 Script executed:

#!/bin/bash
# Locate all MockLowLevelDistribution.sol files in the repo
echo "=== Listing MockLowLevelDistribution.sol files ==="
find . -type f -name "MockLowLevelDistribution.sol"

# Show all references to MockLowLevelDistribution across the codebase
echo "=== All references to MockLowLevelDistribution ==="
rg -n "MockLowLevelDistribution" || true

# Inspect imports and usage in the only test file that mentioned it
echo "=== Header of test/eds/Distributor.ts ==="
sed -n '1,50p' test/eds/Distributor.ts || true

echo "=== Context around MockLowLevelDistribution in test/eds/Distributor.ts ==="
rg -C3 "MockLowLevelDistribution" test/eds/Distributor.ts || true

Length of output: 3570


🏁 Script executed:

#!/bin/bash
# Show full content of contracts/mocks/MockLowLevelDistribution.sol
echo "=== contracts/mocks/MockLowLevelDistribution.sol ==="
sed -n '1,200p' contracts/mocks/MockLowLevelDistribution.sol || true

# Show full content of src/mocks/MockLowLevelDistribution.sol
echo "=== src/mocks/MockLowLevelDistribution.sol ==="
sed -n '1,200p' src/mocks/MockLowLevelDistribution.sol || true

Length of output: 2933


Standardize MockLowLevelDistribution Implementations

It looks like you now have two separate MockLowLevelDistribution contracts that both force a low-level revert but via different mechanisms and with slightly different APIs. This duplication can lead to ambiguous artifact resolution and extra maintenance.

Please consolidate or clearly differentiate them:

src/mocks/MockLowLevelDistribution.sol
– inline assembly invalid() to trigger the error
– emits Distributed event
– uses bytes32("MockLowLevelDistribution") for the name

contracts/mocks/MockLowLevelDistribution.sol
– OpenZeppelin ShortStrings.toShortString + low-level call to address(0) + require
– no event emission
– unwraps a ShortString to bytes32

Action items:

  • Choose a single approach (or rename one mock to reflect its variant)
  • Remove or merge the duplicate file
  • Update imports and tests to reference the chosen implementation


function instantiate(bytes memory) external override returns (address[] memory instances, bytes32 name, uint256 version) {
// This will be a low-level call error (CALL opcode that fails)
// solhint-disable-next-line avoid-low-level-calls
(bool success, ) = address(0).call(abi.encodeWithSignature("nonExistentFunction()"));
require(success, "MockLowLevelDistribution: low level call failed");

// This code is unreachable
instances = new address[](1);
instances[0] = address(this);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function get() external view override returns (address[] memory sources, bytes32 name, uint256 version) {
sources = new address[](1);
sources[0] = address(this);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function contractURI() external pure returns (string memory) {
return "ipfs://mockLowLevelDistribution";
}
}
43 changes: 43 additions & 0 deletions contracts/mocks/MockMigration.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "../../src/interfaces/IMigration.sol";
import "../../src/interfaces/IRepository.sol";
import "../../src/versioning/LibSemver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

contract MockMigration is IMigration, ERC165 {
event MigrationExecuted(
address[] instances,
uint256 oldVersion,
uint256 newVersion,
bytes distributorCalldata,
bytes userCalldata
);

function migrate(
address[] memory instances,
LibSemver.Version memory oldVersion,
LibSemver.Version memory newVersion,
IRepository repository,
bytes calldata distributorCalldata,
bytes calldata userCalldata
) external override {
// Emit an event with migration details
emit MigrationExecuted(
instances,
LibSemver.toUint256(oldVersion),
LibSemver.toUint256(newVersion),
distributorCalldata,
userCalldata
);

// Don't do anything else - this is just a mock for testing
}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165) returns (bool) {
return
interfaceId == type(IMigration).interfaceId ||
super.supportsInterface(interfaceId);
}
}
35 changes: 35 additions & 0 deletions contracts/mocks/MockPanicDistribution.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "../../src/interfaces/IDistribution.sol";
import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStrings.sol";

contract MockPanicDistribution is IDistribution {
ShortString private immutable distributionName;
uint256 private constant DISTRIBUTION_VERSION = 1;

constructor() {
distributionName = ShortStrings.toShortString("MockPanicDistribution");
}

function instantiate(bytes memory) external override returns (address[] memory instances, bytes32 name, uint256 version) {
// This will cause a panic
assert(false);

// This code is unreachable but needed to compile
instances = new address[](0);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function get() external view override returns (address[] memory sources, bytes32 name, uint256 version) {
sources = new address[](1);
sources[0] = address(this);
name = ShortString.unwrap(distributionName);
version = DISTRIBUTION_VERSION;
}

function contractURI() external pure returns (string memory) {
return "ipfs://MockPanicDistribution";
}
}
86 changes: 86 additions & 0 deletions contracts/mocks/MockRepository.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0 <0.9.0;

import "../../src/interfaces/IRepository.sol";
import "../../src/versioning/LibSemver.sol";
import "../../src/erc7744/LibERC7744.sol";

contract MockRepository is IRepository {
using LibERC7744 for bytes32;
using LibSemver for LibSemver.Version;
using LibSemver for LibSemver.VersionRequirement;

mapping(uint256 => bytes32) private sources;
mapping(uint256 => bytes) private sourceMetadata;
mapping(uint64 => bytes32) private migrationScripts;
bytes32 private _repositoryName = bytes32("MockRepository");

function addSource(LibSemver.Version memory version, bytes32 sourceId, bytes memory metadata) external {
sources[version.toUint256()] = sourceId;
sourceMetadata[version.toUint256()] = metadata;
}

function addMigrationScript(uint64 majorVersion, bytes32 migrationHash) external {
migrationScripts[majorVersion] = migrationHash;
}

function resolveVersion(LibSemver.VersionRequirement calldata requirement) external view override returns (uint256) {
// For simplicity, just return the exact version number from the requirement
return requirement.version.toUint256();
}

function get(LibSemver.VersionRequirement calldata requirement) external view override returns (Source memory) {
uint256 version = requirement.version.toUint256();
bytes32 sourceId = sources[version];
bytes memory metadata = sourceMetadata[version];
if (sourceId == bytes32(0)) {
// If no specific source is set, return a default
sourceId = bytes32(uint256(uint160(address(this))));
}
return Source(requirement.version, sourceId, metadata);
}

function getLatest() external view override returns (Source memory) {
// Just return a placeholder value
LibSemver.Version memory latestVersion = LibSemver.parse(1);
bytes32 sourceId = bytes32(uint256(uint160(address(this))));
return Source(latestVersion, sourceId, "");
}

function getMigrationScript(uint64 majorVersion) external view override returns (bytes32) {
return migrationScripts[majorVersion];
}

function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
return
interfaceId == type(IRepository).interfaceId ||
interfaceId == 0x01ffc9a7; // ERC165
}

function repositoryName() external view override returns (bytes32) {
return _repositoryName;
}

function contractURI() external pure override returns (string memory) {
return "ipfs://mockContract";
}

function updateReleaseMetadata(LibSemver.Version memory version, bytes calldata releaseMetadata) external override {
sourceMetadata[version.toUint256()] = releaseMetadata;
emit ReleaseMetadataUpdated(version.toUint256(), releaseMetadata);
}

function newRelease(bytes32 sourceId, bytes memory metadata, LibSemver.Version memory version, bytes32 migrationHash) external override {
uint256 versionUint = version.toUint256();
sources[versionUint] = sourceId;
sourceMetadata[versionUint] = metadata;
migrationScripts[version.major] = migrationHash;
emit VersionAdded(versionUint, sourceId, metadata);
emit MigrationScriptAdded(version.major, migrationHash);
}

function changeMigrationScript(uint64 majorVersion, bytes32 migrationHash) external override {
migrationScripts[majorVersion] = migrationHash;
emit MigrationScriptAdded(majorVersion, migrationHash);
}
}
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
93 changes: 93 additions & 0 deletions docs/Distributions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Distributions

Distributions are the main entities in EDS. They are used to distribute and upgrade contracts.

Distributions are enshrined to be developed in state-less manner, this allows easier auditing and secure portability of code cross-chain.

> [!WARNING]
> If you deploy stateless distributions they will may likely be problematic to adapt by developers because of bytecode hash referencing by [Indexer](./Indexer.md). This is done intentionally to enshrine more secure development best practices.

## Stateful distributions

Stateful distributions are distributions that are not stateless. They are used to distribute and upgrade contracts that are not stateless.

If you need to deploy such stateful distributions, we suggest using [Distributor](./Distributors.md) instead that will manage state of your distribution and is designed with features for that in mind.

## Creating a distribution

In order to create a distribution, first deploy your contract on-chain or copy address you want to use and then index contract code using [Indexer](./Indexer.md)

Once indexed, you can create a distribution using one of available [distribution contracts](../src/distributions)

When extending distributions, you must implement `sources()` virtual function. For every source you return there, distribution abstracts will create one way or another a proxy that will deploy and link every source to proxy.

Here is simple example of stateless distribution using [CloneDistribution](../src/distributions/CloneDistribution.sol):

```solidity
import "@openzeppelin/contracts/utils/Strings.sol";
import "@peeramid-labs/eds/versioning/LibSemver.sol";
import "@peeramid-labs/eds/distributions/CloneDistribution.sol";
contract MyDistribution is CloneDistribution {

ShortString private immutable distributionName;
uint256 private immutable distributionVersion;


constructor(LibSemver.Version memory version) {
distributionName = ShortStrings.toShortString("MyDistribution");
distributionVersion = version.toUint256();
}
using LibSemver for LibSemver.Version;
function sources() public pure override returns (address[] memory sources, bytes32 name, uint256 version) {
sources = new address[](1);
sources[0] = 0x1234567890123456789012345678901234567890;
name = ShortString.unwrap(distributionName);
version = distributionVersion;
}
}
```

### Creating upgradable repository managed distribution

In EDS [Upgradability](./Upgradability.md) is complex multi-party trust process. It enables pattern where distributor & user must agree on upgrade before it can be executed.

In order to enable this process, standard upgradable proxies cannot be used. Instead, we must use proxies that have [ERC7746 Hooks](./Hooks.md) implemented within immutable part of the contract.
This hooks must be implemented by developer of the distribution in such way, that only [Distributor](./Distributors.md) can upgrade the distribution, but the Installer consent is checked in runtime.

Example of such upgradable distribution is [WrappedTransparentUpgradeableProxy](../src/proxies/WrappedTransparentUpgradeableProxy.sol).

Management of the upgrades & migrations on developer side is done via [Repository](./Repositories.md) contract.
Distributors are free to implement their own logic of migration of the state, wrapping or completley bypassing Developer packages as they need to.

For more details refer to [Upgradability](./Upgradability.md) documentation.

## Creating Distributions from CLI

> [!NOTE]
> The CLI provides utilities to create and manage distributions.

```bash
# Deploy a new distribution with source addresses
eds distribution deploy <source-addresses> --proxy-type <Upgradable|Clonable> --name <distribution-name> --version <version> [--uri <uri>]

# Deploy a distribution with source code hashes
eds distribution deploy-with-hashes <source-hashes> --proxy-type <Upgradable|Clonable> --name <distribution-name> --version <version> [--uri <uri>]

# Get information about a distribution
eds distribution <address> info

# Instantiate a distribution
eds distribution <address> instantiate [--data <hex-data>]

# Verify a distribution's source code matches code hashes
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
```



Loading