feat: upgradable Marketplace contract

This commit is contained in:
Adam Uhlíř 2025-06-19 13:04:24 +02:00
parent a179deb2f9
commit 17cb41726b
No known key found for this signature in database
GPG Key ID: 1D17A9E81F76155B
19 changed files with 198 additions and 96 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:

View File

@ -43,6 +43,9 @@ To reuse a previously deployed `Token` contract, define the environment variable
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.
Running the prover

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,13 @@ contract Marketplace is SlotReservations, Proofs, StateRetrieval, Endian {
uint64 slotIndex;
}
constructor(
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);
}

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

@ -1,4 +1,4 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { buildModule } = require('@nomicfoundation/hardhat-ignition/modules')
const { loadZkeyHash } = require("../../verifier/verifier.js")
const { loadConfiguration } = require("../../configuration/configuration.js")
const TokenModule = require("./token.js")
@ -11,33 +11,146 @@ function getDefaultConfig() {
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())
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())
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

@ -62,7 +62,7 @@ describe("Marketplace constructor", function () {
const promise = ignition.deploy(MarketplaceModule, {
parameters: {
Marketplace: {
TestProxy: {
configuration: config,
},
},
@ -90,7 +90,7 @@ describe("Marketplace constructor", function () {
const promise = ignition.deploy(MarketplaceModule, {
parameters: {
Marketplace: {
TestProxy: {
configuration: config,
},
},
@ -143,7 +143,7 @@ describe("Marketplace", function () {
MarketplaceModule,
{
parameters: {
Marketplace: {
TestProxy: {
configuration: config,
},
},