mirror of
https://github.com/logos-storage/logos-storage-contracts-eth.git
synced 2026-01-02 13:23:10 +00:00
feat: upgradable Marketplace contract
This commit is contained in:
parent
a179deb2f9
commit
17cb41726b
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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
6
contracts/Proxies.sol
Normal 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";
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
13
contracts/TestPeriods.sol
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
|
||||
@ -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 }
|
||||
})
|
||||
|
||||
@ -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
2
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user