diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c1c012..65250b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/Readme.md b/Readme.md index 6b93f5f..7466796 100644 --- a/Readme.md +++ b/Readme.md @@ -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 diff --git a/contracts/FuzzMarketplace.sol b/contracts/FuzzMarketplace.sol deleted file mode 100644 index 3291e36..0000000 --- a/contracts/FuzzMarketplace.sol +++ /dev/null @@ -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); - } -} diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 8b0a29c..8a09004 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -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) diff --git a/contracts/Periods.sol b/contracts/Periods.sol index 5e18ee1..08a9353 100644 --- a/contracts/Periods.sol +++ b/contracts/Periods.sol @@ -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(); } diff --git a/contracts/Proofs.sol b/contracts/Proofs.sol index 1dfc7ce..ba9df9e 100644 --- a/contracts/Proofs.sol +++ b/contracts/Proofs.sol @@ -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; } diff --git a/contracts/Proxies.sol b/contracts/Proxies.sol new file mode 100644 index 0000000..6560c68 --- /dev/null +++ b/contracts/Proxies.sol @@ -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"; \ No newline at end of file diff --git a/contracts/SlotReservations.sol b/contracts/SlotReservations.sol index 8faa87b..bc7c392 100644 --- a/contracts/SlotReservations.sol +++ b/contracts/SlotReservations.sol @@ -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; } diff --git a/contracts/TestMarketplace.sol b/contracts/TestMarketplace.sol index c18d369..815d8ea 100644 --- a/contracts/TestMarketplace.sol +++ b/contracts/TestMarketplace.sol @@ -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); } diff --git a/contracts/TestPeriods.sol b/contracts/TestPeriods.sol new file mode 100644 index 0000000..d21ff17 --- /dev/null +++ b/contracts/TestPeriods.sol @@ -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); + } +} diff --git a/contracts/TestProofs.sol b/contracts/TestProofs.sol index 4d49aad..bca6057 100644 --- a/contracts/TestProofs.sol +++ b/contracts/TestProofs.sol @@ -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) { diff --git a/contracts/TestSlotReservations.sol b/contracts/TestSlotReservations.sol index 3793223..f72a1fd 100644 --- a/contracts/TestSlotReservations.sol +++ b/contracts/TestSlotReservations.sol @@ -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); } diff --git a/ignition/modules/marketplace.js b/ignition/modules/marketplace.js index e0798eb..b0fc732 100644 --- a/ignition/modules/marketplace.js +++ b/ignition/modules/marketplace.js @@ -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 } +}) diff --git a/ignition/modules/periods.js b/ignition/modules/periods.js index c3a352a..c2a3b96 100644 --- a/ignition/modules/periods.js +++ b/ignition/modules/periods.js @@ -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 } }) diff --git a/ignition/modules/proofs.js b/ignition/modules/proofs.js index bc71762..570f663 100644 --- a/ignition/modules/proofs.js +++ b/ignition/modules/proofs.js @@ -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 } }) diff --git a/ignition/modules/slot-reservations.js b/ignition/modules/slot-reservations.js index d795381..64989f2 100644 --- a/ignition/modules/slot-reservations.js +++ b/ignition/modules/slot-reservations.js @@ -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 } }) diff --git a/package-lock.json b/package-lock.json index da57f6a..669ed22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8d972d4..8d88f5c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index 153f9a1..9c472f7 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -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, }, },