Merge 64174d8ce666e33d21e17acea76b2b66e0f1b6c8 into da1400ce9a65c3695d7847f7ca8e3386a5a189c8

This commit is contained in:
Adam Uhlíř 2025-08-27 04:57:16 +10:00 committed by GitHub
commit 8a7cb119ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 574 additions and 286 deletions

View File

@ -17,6 +17,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm install
- run: npm run lint
- run: npm run format:check
test:
@ -30,11 +31,6 @@ jobs:
node-version: 22
- run: npm install
- run: npm test
- uses: actions/cache@v4
with:
path: fuzzing/corpus
key: fuzzing
- run: npm run fuzz
verify:
runs-on: ubuntu-latest

206
README.md Normal file
View File

@ -0,0 +1,206 @@
# Codex Marketplace Contracts
An implementation of the smart contracts that underlay the Codex
storage network. Its goal is to facilitate storage marketplace
for the Codex's persistance layer.
## Running
To run the tests, execute the following commands:
npm install
npm test
You can also run fuzzing tests (using [Echidna][echidna]) on the contracts:
npm run fuzz
To start a local Ethereum node with the contracts deployed, execute:
npm start
### Running the prover
To run the formal verification rules using Certora, first, make sure you have Java (JDK >= 11.0) installed on your
machine, and then install the Certora CLI
```
$ pip install certora-cli
```
Once that is done the `certoraRun` command can be used to send CVL specs to the prover.
You can run Certora's specs with the provided `npm` script:
npm run verify
## Deployment
To deploy the marketplace, you need to specify the network using `--network MY_NETWORK`:
```bash
npm run deploy -- --network localhost
```
Hardhat uses [reconciliation](https://hardhat.org/ignition/docs/advanced/reconciliation) to recover from
errors or resume a previous deployment. In our case, we will likely redeploy a new contract every time,
so we will need
to [clear the previous deployment](https://hardhat.org/ignition/docs/guides/modifications#clearing-an-existing-deployment-with-reset):
```bash
npm run deploy -- --network testnet --reset
```
To reuse a previously deployed `Token` contract, define the environment variable `TOKEN_ADDRESS`.
The deployment script will use `contractAt` from Hardhat Ignition to retrieve the existing contract
instead of deploying a new one.
When deploying to other network then Hardhat localhost's, you have to specify the Proxy's owner address
using the env. variable `PROXY_ADMIN_ADDRESS`. This account then can perform upgrades to the contract.
The deployment files are kept under version
control [as recommended by Hardhat](https://hardhat.org/ignition/docs/advanced/versioning), except the build files,
which are 18 MB.
## Smart contracts overview
This contract suite deploys two smart contracts:
1. `Marketplace` smart contract
2. `Vault` smart contract
The `Marketplace` smart contract implements the storage marketplace logic. Its internal logic is divided into
multiple abstract subcontracts that focus on specific pieces like `Periods`, `Proofs`, `SlotReservations`, and so on,
which are all bundled at the top level of the `Marketplace` contract itself.
The `Vault` smart contract is a specialized contract designed to safely keep users' funds. It is utilized by the
`Marketplace` contract to delegate all funds' safe-keeping to it. There are several mechanisms in the `Vault` contract
that should prevent a complete "grab & run" of all the funds in case an exploit is found in the `Marketplace` smart
contract.
### Upgradability
The `Marketplace` contract employs the contract's upgradability pattern using [
`TransparentUpgradeableProxy`](https://docs.openzeppelin.com/contracts/5.x/api/proxy#TransparentUpgradeableProxy), which
allows replacing the underlying implementation while preserving the contract address and its storage. The upgrade can be
performed only by the account that is specified during the initial deployment through the `PROXY_ADMIN_ADDRESS`
environment variable. This capability is dedicated to emergency upgrades, as described in
our [Codex Contract Deployment, Upgrades and Security](https://github.com/codex-storage/codex-research/blob/master/design/contract-deployment.md)
document.
The steps to perform an emergency upgrade are:
1. Create a new `Marketplace` contract that will incorporate the changes. Name it, for example, `MarketplaceV2`.
- The original `Marketplace` and its abstract subcontracts should not be edited once deployed.
- If you need to make changes in one of the abstract subcontracts, also create a new version copy like `PeriodsV2`.
- **Do not modify any storage variables in the contract!** The upgrade mechanism is not designed for this.
2. Create a new Ignition deployment script that will perform the upgrade.
- Take inspiration from the `marketplace-test-upgrade` module, which performs the upgrade in our test suite.
- The upgrading transaction needs to originate from the account that was specified as `PROXY_ADMIN_ADDRESS` in the
initial deployment.
3. Deploy the upgrade with `hardhat ignition deploy <new module> --network <deployment network>`.
Once the new feature upgrade is planned, the first step when drafting this new version is to reconcile all
the upgrade's changes (if there were any) back into the `Marketplace` contract and any modified subcontract
on the new feature branch.
#### Safe Multisig Upgrade
When the `Proxy`'s owner is set to Safe's Multisig Wallet, the upgrade process needs to be modified. The upgrade
deployment module cannot create the `upgradeAndCall` call directly; hence, the deploy module needs only to deploy the
upgraded implementation logic.
Then, the `upgradeAndCall` call needs to be created through the Safe Wallet UI using the "Transaction Builder," where it
is recommended to input the `ProxyAdmin` ABI, which helps to easily create the `upgradeAndCall` call. The `proxy`
parameter is the Marketplace's Proxy address, the `implementation` parameter is the address of the new upgraded
implementation, and the `data` can be left empty (`0x`).
## Marketplace overview
The Codex storage network depends on hosts offering storage to clients of the
network. The smart contracts in this repository handle interactions between
client and hosts as they negotiate and fulfill a contract to store data for a
certain amount of time.
When all goes well, the client and hosts perform the following steps:
Client Host Marketplace Contract
| | |
| |
| --------------- request (1) -------------> |
| |
| ----- data (2) ---> | |
| | |
| ----- fill (3) ----> |
| |
| ---- proof (4) ----> |
| |
| ---- proof (4) ----> |
| |
| ---- proof (4) ----> |
| |
| <-- payment (5) ---- |
1. Client submits a request for storage, containing the size of the data that
it wants to store and the length of time it wants to store it
2. Client makes the data available to hosts
3. Hosts submit storage proofs to fill slots in the contract
4. While the storage contract is active, host prove that they are still
storing the data by responding to frequent random challenges
5. At the end of the contract the hosts are paid
For full overview
see [Codex Marketplace specification](https://github.com/codex-storage/codex-spec/blob/master/specs/marketplace.md).
### Storage Contracts
A storage contract contains of a number of slots. Each of these slots represents
an agreement with a storage host to store a part of the data. Hosts that want to
offer storage can fill a slot in the contract.
A contract can be negotiated through requests. A request contains the size of
the data, the length of time during which it needs to be stored, and a number of
slots. It also contains the reward that a client is willing to pay and proof
requirements such as how often a proof will need to be submitted by hosts. A
random nonce is included to ensure uniqueness among similar requests.
When a new storage contract is created the client immediately pays the entire
price of the contract. The payment is only released to the host upon successful
completion of the contract.
### Collateral
To motivate a host to remain honest, it must put up some collateral before it is
allowed to participate in storage contracts. The collateral may not be withdrawn
as long as a host is participating in an active storage contract.
Should a host be misbehaving, then its collateral may be reduced by a certain
percentage (slashed).
### Proofs
Hosts are required to submit frequent proofs while a contract is active. These
proofs ensure with a high probability that hosts are still holding on to the
data that they were entrusted with.
To ensure that hosts are not able to predict and precalculate proofs, these
proofs are based on a random challenge. Currently we use ethereum block hashes
to determine two things: 1) whether or not a proof is required at this point in
time, and 2) the random challenge for the proof. Although hosts will not be able
to predict the exact times at which proofs are required, the frequency of proofs
averages out to a value that was set by the client in the request for storage.
Hosts have a small period of time in which they are expected to submit a proof.
When that time has expired without seeing a proof, validators are able to point
out the lack of proof. If a host misses too many proofs, it results into a
slashing of its collateral.
## References
* [A marketplace for storage
durability](https://github.com/codex-storage/codex-research/blob/master/design/marketplace.md)
(design document)
* [Timing of Storage
Proofs](https://github.com/codex-storage/codex-research/blob/master/design/storage-proof-timing.md)
(design document)
* [Codex Marketplace spec](https://github.com/codex-storage/codex-spec/blob/master/specs/marketplace.md)

170
Readme.md
View File

@ -1,170 +0,0 @@
Codex Contracts
================
An experimental implementation of the smart contracts that underlay the Codex
storage network. Its goal is to experiment with the rules around the bidding
process, the storage contracts, the storage proofs and the host collateral.
Neither completeness nor correctness are guaranteed at this moment in time.
Running
-------
To run the tests, execute the following commands:
npm install
npm test
You can also run fuzzing tests (using [Echidna][echidna]) on the contracts:
npm run fuzz
To start a local Ethereum node with the contracts deployed, execute:
npm start
Deployment
----------
To deploy the marketplace, you need to specify the network using `--network MY_NETWORK`:
```bash
npm run deploy -- --network localhost
```
Hardhat uses [reconciliation](https://hardhat.org/ignition/docs/advanced/reconciliation) to recover from
errors or resume a previous deployment. In our case, we will likely redeploy a new contract every time,
so we will need to [clear the previous deployment](https://hardhat.org/ignition/docs/guides/modifications#clearing-an-existing-deployment-with-reset):
```bash
npm run deploy -- --network testnet --reset
```
To reuse a previously deployed `Token` contract, define the environment variable `TOKEN_ADDRESS`.
The deployment script will use `contractAt` from Hardhat Ignition to retrieve the existing contract
instead of deploying a new one.
The deployment files are kept under version control [as recommended by Hardhat](https://hardhat.org/ignition/docs/advanced/versioning), except the build files, which are 18 MB.
Running the prover
------------------
To run the formal verification rules using Certora, first, make sure you have Java (JDK >= 11.0) installed on your machine, and then install the Certora CLI
```
$ pip install certora-cli
```
Once that is done the `certoraRun` command can be used to send CVL specs to the prover.
You can run Certora's specs with the provided `npm` script:
npm run verify
Overview
--------
The Codex storage network depends on hosts offering storage to clients of the
network. The smart contracts in this repository handle interactions between
client and hosts as they negotiate and fulfill a contract to store data for a
certain amount of time.
When all goes well, the client and hosts perform the following steps:
Client Host Marketplace Contract
| | |
| |
| --------------- request (1) -------------> |
| |
| ----- data (2) ---> | |
| | |
| ----- fill (3) ----> |
| |
| ---- proof (4) ----> |
| |
| ---- proof (4) ----> |
| |
| ---- proof (4) ----> |
| |
| <-- payment (5) ---- |
1. Client submits a request for storage, containing the size of the data that
it wants to store and the length of time it wants to store it
2. Client makes the data available to hosts
3. Hosts submit storage proofs to fill slots in the contract
4. While the storage contract is active, host prove that they are still
storing the data by responding to frequent random challenges
5. At the end of the contract the hosts are paid
Contracts
---------
A storage contract contains of a number of slots. Each of these slots represents
an agreement with a storage host to store a part of the data. Hosts that want to
offer storage can fill a slot in the contract.
A contract can be negotiated through requests. A request contains the size of
the data, the length of time during which it needs to be stored, and a number of
slots. It also contains the reward that a client is willing to pay and proof
requirements such as how often a proof will need to be submitted by hosts. A
random nonce is included to ensure uniqueness among similar requests.
When a new storage contract is created the client immediately pays the entire
price of the contract. The payment is only released to the host upon successful
completion of the contract.
Collateral
----------
To motivate a host to remain honest, it must put up some collateral before it is
allowed to participate in storage contracts. The collateral may not be withdrawn
as long as a host is participating in an active storage contract.
Should a host be misbehaving, then its collateral may be reduced by a certain
percentage (slashed).
Proofs
------
Hosts are required to submit frequent proofs while a contract is active. These
proofs ensure with a high probability that hosts are still holding on to the
data that they were entrusted with.
To ensure that hosts are not able to predict and precalculate proofs, these
proofs are based on a random challenge. Currently we use ethereum block hashes
to determine two things: 1) whether or not a proof is required at this point in
time, and 2) the random challenge for the proof. Although hosts will not be able
to predict the exact times at which proofs are required, the frequency of proofs
averages out to a value that was set by the client in the request for storage.
Hosts have a small period of time in which they are expected to submit a proof.
When that time has expired without seeing a proof, validators are able to point
out the lack of proof. If a host misses too many proofs, it results into a
slashing of its collateral.
References
----------
* [A marketplace for storage
durability](https://github.com/codex-storage/codex-research/blob/master/design/marketplace.md)
(design document)
* [Timing of Storage
Proofs](https://github.com/codex-storage/codex-research/blob/master/design/storage-proof-timing.md)
(design document)
To Do
-----
* Contract repair
Allow another host to take over a slot in the contract when the original
host missed too many proofs.
* Reward validators
A validator that points out missed proofs should be compensated for its
vigilance and for the gas costs of invoking the smart contract.
* Analysis and optimization of gas usage
[echidna]: https://github.com/crytic/echidna

View File

@ -10,10 +10,6 @@ import {RequestId, SlotId} from "../../contracts/Requests.sol";
import {Requests} from "../../contracts/Requests.sol";
contract MarketplaceHarness is Marketplace {
constructor(MarketplaceConfig memory config, IERC20 token, IGroth16Verifier verifier)
Marketplace(config, token, verifier)
{}
function publicPeriodEnd(Period period) public view returns (uint64) {
return _periodEnd(period);
}

View File

@ -1,4 +1,5 @@
const fs = require("fs")
const { loadZkeyHash } = require("../verifier/verifier.js")
const BASE_PATH = __dirname + "/networks"
@ -23,6 +24,17 @@ const DEFAULT_CONFIGURATION = {
requestDurationLimit: 60*60*24*30 // 30 days
}
function getDefaultConfig(networkName) {
if (networkName === undefined) {
throw new TypeError("Network name needs to be specified!")
}
const zkeyHash = loadZkeyHash(networkName)
const config = loadConfiguration(networkName)
config.proofs.zkeyHash = zkeyHash
return config
}
function loadConfiguration(name) {
const path = `${BASE_PATH}/${name}/configuration.js`
if (fs.existsSync(path)) {
@ -32,4 +44,4 @@ function loadConfiguration(name) {
}
}
module.exports = { loadConfiguration }
module.exports = { loadConfiguration, getDefaultConfig }

View File

@ -1,39 +0,0 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./TestToken.sol";
import "./Marketplace.sol";
import "./TestVerifier.sol";
contract FuzzMarketplace is Marketplace {
constructor()
Marketplace(
MarketplaceConfig(
CollateralConfig(10, 5, 10, 20),
ProofConfig(10, 5, 64, 67, ""),
SlotReservationsConfig(20),
60 * 60 * 24 * 30 // 30 days
),
new TestToken(),
new TestVerifier()
)
// solhint-disable-next-line no-empty-blocks
{
}
// Properties to be tested through fuzzing
MarketplaceTotals private _lastSeenTotals;
function neverDecreaseTotals() public {
assert(_marketplaceTotals.received >= _lastSeenTotals.received);
assert(_marketplaceTotals.sent >= _lastSeenTotals.sent);
_lastSeenTotals = _marketplaceTotals;
}
function neverLoseFunds() public view {
uint256 total = _marketplaceTotals.received - _marketplaceTotals.sent;
assert(token().balanceOf(address(this)) >= total);
}
}

View File

@ -2,6 +2,7 @@
pragma solidity 0.8.28;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Configuration.sol";
@ -12,7 +13,7 @@ import "./StateRetrieval.sol";
import "./Endian.sol";
import "./Groth16.sol";
contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
contract Marketplace is Initializable, SlotReservations, Proofs, StateRetrieval, Endian {
error Marketplace_RepairRewardPercentageTooHigh();
error Marketplace_SlashPercentageTooHigh();
error Marketplace_MaximumSlashingTooHigh();
@ -46,7 +47,7 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
using Requests for Request;
using AskHelpers for Ask;
IERC20 private immutable _token;
IERC20 private _token;
MarketplaceConfig private _config;
mapping(RequestId => Request) private _requests;
@ -96,11 +97,20 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
uint64 slotIndex;
}
constructor(
constructor() {
// In case that the contract would get deployed without initialization
// this prevents attackers to call the initializations themselves with
// potentially malicious initialization values.
_disableInitializers();
}
function initialize (
MarketplaceConfig memory config,
IERC20 token_,
IGroth16Verifier verifier
) SlotReservations(config.reservations) Proofs(config.proofs, verifier) {
) public initializer {
_initializeSlotReservations(config.reservations);
_initializeProofs(config.proofs, verifier);
_token = token_;
if (config.collateral.repairRewardPercentage > 100)

View File

@ -1,14 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
contract Periods {
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
contract Periods is Initializable {
error Periods_InvalidSecondsPerPeriod();
type Period is uint64;
uint64 internal immutable _secondsPerPeriod;
uint64 internal _secondsPerPeriod;
constructor(uint64 secondsPerPeriod) {
function _initializePeriods(uint64 secondsPerPeriod) internal onlyInitializing {
if (secondsPerPeriod == 0) {
revert Periods_InvalidSecondsPerPeriod();
}

View File

@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "./Configuration.sol";
import "./Requests.sol";
import "./Periods.sol";
@ -26,15 +28,17 @@ abstract contract Proofs is Periods {
/**
* Creation of the contract requires at least 256 mined blocks!
* @param config Proving configuration
* @param verifier Proof verifier
*/
constructor(
function _initializeProofs(
ProofConfig memory config,
IGroth16Verifier verifier
) Periods(config.period) {
) internal onlyInitializing {
if (block.number <= 256) {
revert Proofs_InsufficientBlockHeight();
}
_initializePeriods(config.period);
_config = config;
_verifier = verifier;
}

6
contracts/Proxies.sol Normal file
View File

@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

View File

@ -1,18 +1,20 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import "./Requests.sol";
import "./Configuration.sol";
abstract contract SlotReservations {
abstract contract SlotReservations is Initializable {
using EnumerableSet for EnumerableSet.AddressSet;
error SlotReservations_ReservationNotAllowed();
mapping(SlotId => EnumerableSet.AddressSet) internal _reservations;
SlotReservationsConfig private _config;
constructor(SlotReservationsConfig memory config) {
function _initializeSlotReservations(SlotReservationsConfig memory config) internal onlyInitializing {
_config = config;
}

View File

@ -5,14 +5,6 @@ import "./Marketplace.sol";
// exposes internal functions of Marketplace for testing
contract TestMarketplace is Marketplace {
constructor(
MarketplaceConfig memory config,
IERC20 token,
IGroth16Verifier verifier
)
Marketplace(config, token, verifier) // solhint-disable-next-line no-empty-blocks
{}
function forciblyFreeSlot(SlotId slotId) public {
_forciblyFreeSlot(slotId);
}

View File

@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./Marketplace.sol";
// exposes internal functions of Marketplace for testing
contract TestMarketplaceUpgraded is Marketplace {
function newShinyMethod() public pure returns (uint256) {
return 42;
}
}

13
contracts/TestPeriods.sol Normal file
View File

@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import "./Periods.sol";
contract TestPeriods is Periods {
function initialize (
uint64 secondsPerPeriod
) public initializer {
_initializePeriods(secondsPerPeriod);
}
}

View File

@ -13,11 +13,12 @@ contract TestProofs is Proofs {
// private to internal, which may cause problems in the Marketplace contract.
ProofConfig private _proofConfig;
constructor(
function initialize (
ProofConfig memory config,
IGroth16Verifier verifier
) Proofs(config, verifier) {
) public initializer {
_proofConfig = config;
_initializeProofs(config, verifier);
}
function slotState(SlotId slotId) public view override returns (SlotState) {

View File

@ -8,9 +8,11 @@ contract TestSlotReservations is SlotReservations {
mapping(SlotId => SlotState) private _states;
// solhint-disable-next-line no-empty-blocks
constructor(SlotReservationsConfig memory config) SlotReservations(config) {}
function initialize (
SlotReservationsConfig memory config
) public initializer {
_initializeSlotReservations(config);
}
function contains(SlotId slotId, address host) public view returns (bool) {
return _reservations[slotId].contains(host);
}

View File

@ -0,0 +1,36 @@
const { buildModule } = require('@nomicfoundation/hardhat-ignition/modules')
const MarketplaceModule = require("./marketplace.js")
/**
* This module upgrades the Marketplace's Proxy with a new implementation.
* It deploys the new Marketplace contract and then calls the Proxy with
* the `upgradeAndCall` function, which swaps the implementations.
*/
const upgradeModule = buildModule('UpgradeProxyImplementation', (m) => {
const config = hre.network.config
if (!(config && config.tags && config.tags.includes("local"))) {
throw new Error("Module is not meant for real deployments!")
}
const proxyAdminOwner = m.getAccount(9)
const marketplaceUpgraded = m.contract("TestMarketplaceUpgraded", [])
const {proxyAdmin, proxy, token} = m.useModule(MarketplaceModule);
m.call(proxyAdmin, "upgradeAndCall", [proxy, marketplaceUpgraded, "0x"], {
from: proxyAdminOwner,
});
return { proxyAdmin, proxy, token };
})
/**
* The main module that represents the upgraded Marketplace contract.
*/
module.exports = buildModule('MarketplaceUpgraded', (m) => {
const { proxy, proxyAdmin, token } = m.useModule(upgradeModule)
const marketplace = m.contractAt('TestMarketplaceUpgraded', proxy)
return { marketplace, proxy, proxyAdmin, token }
})

View File

@ -1,43 +1,149 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { loadZkeyHash } = require("../../verifier/verifier.js")
const { loadConfiguration } = require("../../configuration/configuration.js")
const { buildModule } = require('@nomicfoundation/hardhat-ignition/modules')
const { getDefaultConfig } = require("../../configuration/configuration.js")
const TokenModule = require("./token.js")
const VerifierModule = require("./verifier.js")
function getDefaultConfig() {
const zkeyHash = loadZkeyHash(hre.network.name)
const config = loadConfiguration(hre.network.name)
config.proofs.zkeyHash = zkeyHash
return config
}
module.exports = buildModule("Marketplace", (m) => {
const { token } = m.useModule(TokenModule)
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", getDefaultConfig())
const marketplace = m.contract(
"Marketplace",
[configuration, token, verifier],
{},
)
/**
* Module that deploy the Marketplace logic
*/
const marketplaceLogicModule = buildModule("MarketplaceLogic", (m) => {
const marketplace = m.contract("Marketplace", [])
let testMarketplace
const config = hre.network.config
if (config && config.tags && config.tags.includes("local")) {
const { testVerifier } = m.useModule(VerifierModule)
testMarketplace = m.contract(
"TestMarketplace",
[configuration, token, testVerifier],
{},
)
testMarketplace = m.contract("TestMarketplace", [])
}
return {
marketplace,
testMarketplace,
token,
}
})
/**
* This module deploy Proxy with Marketplace contract used as implementation
* and it initializes the Marketplace in the Proxy context.
*/
const proxyModule = buildModule('Proxy', (m) => {
const deployer = m.getAccount(0)
const config = hre.network.config
// This address is the owner of the ProxyAdmin contract,
// so it will be the only account that can upgrade the proxy when needed.
let proxyAdminOwner
if (config && config.tags && config.tags.includes("local")) {
// The Proxy Admin is not allowed to make "forwarded" calls through Proxy,
// it can only upgrade it, hence this account must not be used for example in tests.
proxyAdminOwner = process.env.PROXY_ADMIN_ADDRESS || m.getAccount(9)
} else {
if (!process.env.PROXY_ADMIN_ADDRESS) {
throw new Error("In non-Hardhat network you need to specify PROXY_ADMIN_ADDRESS env. variable")
}
proxyAdminOwner = process.env.PROXY_ADMIN_ADDRESS
}
const { marketplace } = m.useModule(marketplaceLogicModule)
const { token } = m.useModule(TokenModule)
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", getDefaultConfig(hre.network.name))
const encodedMarketplaceInitializerCall = m.encodeFunctionCall(
marketplace,
"initialize",
[configuration, token, verifier]
);
// The TransparentUpgradeableProxy contract creates the ProxyAdmin within its constructor.
const proxy = m.contract(
'TransparentUpgradeableProxy',
[
marketplace,
proxyAdminOwner,
encodedMarketplaceInitializerCall,
],
{ from: deployer },
)
// We need to get the address of the ProxyAdmin contract that was created by the TransparentUpgradeableProxy
// so that we can use it to upgrade the proxy later.
const proxyAdminAddress = m.readEventArgument(
proxy,
'AdminChanged',
'newAdmin'
)
// Here we use m.contractAt(...) to create a contract instance for the ProxyAdmin that we can interact with later to upgrade the proxy.
const proxyAdmin = m.contractAt('ProxyAdmin', proxyAdminAddress)
return { proxyAdmin, proxy, token }
})
/**
* This module deploy Proxy with TestMarketplace contract used for testing purposes.
*/
const testProxyModule = buildModule('TestProxy', (m) => {
const deployer = m.getAccount(0)
const config = hre.network.config
// We allow testing contract only in local/Hardhat network
if (!(config && config.tags && config.tags.includes("local"))) {
return { testProxy: undefined }
}
let proxyAdminOwner = process.env.PROXY_ADMIN_ADDRESS || m.getAccount(9)
const { testMarketplace } = m.useModule(marketplaceLogicModule)
const { token } = m.useModule(TokenModule)
const { testVerifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", getDefaultConfig(hre.network.name))
const encodedMarketplaceInitializerCall = m.encodeFunctionCall(
testMarketplace,
"initialize",
[configuration, token, testVerifier]
);
const testProxy = m.contract(
'TransparentUpgradeableProxy',
[
testMarketplace,
proxyAdminOwner,
encodedMarketplaceInitializerCall,
],
{ from: deployer },
)
return { testProxy }
})
/**
* The main module that represents Marketplace contract.
* Underneath there is the deployed Proxy contract with Markeplace's logic used as proxy's implementation
* and initilized proxy's context with Marketplace's configuration.
*/
module.exports = buildModule('Marketplace', (m) => {
const { proxy, proxyAdmin, token } = m.useModule(proxyModule)
const config = hre.network.config
// We use the Proxy contract as it would be Marketplace contract
const marketplace = m.contractAt('Marketplace', proxy)
// We allow testing contract only in local/Hardhat network
if (config && config.tags && config.tags.includes("local")) {
const { testProxy } = m.useModule(testProxyModule)
const testMarketplace = m.contractAt('TestMarketplace', testProxy)
return { marketplace, proxy, proxyAdmin, testMarketplace, token }
}
// Return the contract instance, along with the original proxy and proxyAdmin contracts
// so that they can be used by other modules, or in tests and scripts.
return { marketplace, proxy, proxyAdmin, token }
})

View File

@ -2,8 +2,9 @@ const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("Periods", (m) => {
const secondsPerPeriod = m.getParameter("secondsPerPeriod", 0)
const periods = m.contract("TestPeriods", [])
const periods = m.contract("Periods", [secondsPerPeriod], {})
m.call(periods, "initialize", [secondsPerPeriod]);
return { periods }
})

View File

@ -5,7 +5,8 @@ module.exports = buildModule("Proofs", (m) => {
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", null)
const testProofs = m.contract("TestProofs", [configuration, verifier], {})
const testProofs = m.contract("TestProofs", [])
m.call(testProofs, "initialize", [configuration, verifier])
return { testProofs }
})

View File

@ -2,12 +2,9 @@ const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("SlotReservations", (m) => {
const configuration = m.getParameter("configuration", null)
const testSlotReservations = m.contract("TestSlotReservations", [])
const testSlotReservations = m.contract(
"TestSlotReservations",
[configuration],
{},
)
m.call(testSlotReservations, "initialize", [configuration]);
return { testSlotReservations }
})

2
package-lock.json generated
View File

@ -8,7 +8,7 @@
"license": "MIT",
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.8",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.11",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.12",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@openzeppelin/contracts": "^5.3.0",
"@stdlib/stats-binomial-test": "^0.2.2",

View File

@ -2,7 +2,7 @@
"name": "codex-contracts-eth",
"license": "MIT",
"scripts": {
"test": "npm run lint && hardhat test",
"test": "hardhat test",
"fuzz": "hardhat compile && fuzzing/fuzz.sh",
"start": "concurrently --names \"hardhat,deployment\" --prefix \"[{time} {name}]\" \"hardhat node\" \"sleep 2 && npm run mine && npm run deploy -- --network localhost\"",
"compile": "hardhat compile",
@ -18,10 +18,10 @@
"gas:report": "REPORT_GAS=true hardhat test"
},
"devDependencies": {
"@openzeppelin/contracts": "^5.3.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.8",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.11",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.12",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@openzeppelin/contracts": "^5.3.0",
"@stdlib/stats-binomial-test": "^0.2.2",
"chai": "^4.5.0",
"ethers": "6.14.4",

View File

@ -41,6 +41,7 @@ const {
} = require("./evm")
const { getBytes } = require("ethers")
const MarketplaceModule = require("../ignition/modules/marketplace")
const MarketplaceUpgradeModule = require("../ignition/modules/marketplace-test-upgrade")
const { assertDeploymentRejectedWithCustomError } = require("./helpers")
const ACCOUNT_STARTING_BALANCE = 1_000_000_000_000_000n
@ -62,7 +63,7 @@ describe("Marketplace constructor", function () {
const promise = ignition.deploy(MarketplaceModule, {
parameters: {
Marketplace: {
TestProxy: {
configuration: config,
},
},
@ -86,11 +87,9 @@ describe("Marketplace constructor", function () {
config.collateral.slashPercentage = 1
config.collateral.maxNumberOfSlashes = 101
const expectedError = "Marketplace_MaximumSlashingTooHigh"
const promise = ignition.deploy(MarketplaceModule, {
parameters: {
Marketplace: {
TestProxy: {
configuration: config,
},
},
@ -103,6 +102,108 @@ describe("Marketplace constructor", function () {
})
})
// Currently, Hardhat Ignition does not track deployments in the Hardhat network,
// so when Hardhat runs tests on its network, Ignition will deploy new contracts
// every time it is deploying something. This prevents proper testing of the upgradability
// because a new Proxy will be deployed on the "upgrade deployment," and it won't reuse the
// original one.
// This can be tested manually by running `hardhat node` and then `hardhat test --network localhost`.
// But even then, the test "should share the same storage" is having issues, which I suspect
// are related to different network usage.
// Blocked until this issue is resolved: https://github.com/NomicFoundation/hardhat/issues/6927
describe.skip("Marketplace upgrades", function () {
const config = exampleConfiguration()
let originalMarketplace, proxy, token, request, client
enableRequestAssertions()
beforeEach(async function () {
await snapshot()
await ensureMinimumBlockHeight(256)
const {
marketplace,
proxy: deployedProxy,
token: token_,
} = await ignition.deploy(MarketplaceModule, {
deploymentId: "test",
parameters: {
TestProxy: {
configuration: config,
},
},
})
proxy = deployedProxy
originalMarketplace = marketplace
token = token_
await ensureMinimumBlockHeight(256)
;[client] = await ethers.getSigners()
request = await exampleRequest()
request.client = client.address
token = token.connect(client)
originalMarketplace = marketplace.connect(client)
})
afterEach(async function () {
await revert()
})
it("should get upgraded", async () => {
expect(() => originalMarketplace.newShinyMethod()).to.throw(TypeError)
const { marketplace: upgradedMarketplace, proxy: upgradedProxy } =
await ignition.deploy(MarketplaceUpgradeModule, {
deploymentId: "test",
parameters: {
TestProxy: {
configuration: config,
},
},
})
expect(await upgradedMarketplace.newShinyMethod()).to.equal(42)
expect(await originalMarketplace.getAddress()).to.equal(
await upgradedMarketplace.getAddress(),
)
expect(await originalMarketplace.configuration()).to.deep.equal(
await upgradedMarketplace.configuration(),
)
})
it("should share the same storage", async () => {
// Old implementation not supporting the shiny method
expect(() => originalMarketplace.newShinyMethod()).to.throw(TypeError)
// We create a Request on the old implementation
await token.approve(
await originalMarketplace.getAddress(),
maxPrice(request),
)
const now = await currentTime()
await setNextBlockTimestamp(now)
const expectedExpiry = now + request.expiry
await expect(originalMarketplace.requestStorage(request))
.to.emit(originalMarketplace, "StorageRequested")
.withArgs(requestId(request), askToArray(request.ask), expectedExpiry)
// Assert that the data is there
expect(
await originalMarketplace.getRequest(requestId(request)),
).to.be.request(request)
// Upgrade Marketplace
const { marketplace: upgradedMarketplace, token: _token } =
await ignition.deploy(MarketplaceUpgradeModule)
expect(await upgradedMarketplace.newShinyMethod()).to.equal(42)
// Assert that the data is there
expect(
await upgradedMarketplace.getRequest(requestId(request)),
).to.be.request(request)
})
})
describe("Marketplace", function () {
const proof = exampleProof()
const config = exampleConfiguration()
@ -143,7 +244,8 @@ describe("Marketplace", function () {
MarketplaceModule,
{
parameters: {
Marketplace: {
TestProxy: {
// TestMarketplace initialization is done in the TestProxy module
configuration: config,
},
},