Move to ethers 6 and use hardhat ignition for deployments

This commit is contained in:
Arnaud 2025-04-09 16:08:13 +02:00
parent 2dddc26015
commit de58989edf
No known key found for this signature in database
GPG Key ID: 20E40A5D3110766F
24 changed files with 7063 additions and 28937 deletions

View File

@ -10,7 +10,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
jobs:
formatting:
runs-on: ubuntu-latest
@ -29,7 +28,7 @@ jobs:
# workaround for https://github.com/NomicFoundation/hardhat/issues/3877
- uses: actions/setup-node@v4
with:
node-version: 18.15
node-version: 22
- run: npm install
- run: npm test
- uses: actions/cache@v4
@ -53,9 +52,9 @@ jobs:
- name: Install Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: 'jre'
distribution: "zulu"
java-version: "11"
java-package: "jre"
- name: Install Certora CLI
run: pip3 install certora-cli==7.10.2
@ -88,4 +87,3 @@ jobs:
rule:
- verify:marketplace
- verify:state_changes

2
.gitignore vendored
View File

@ -4,3 +4,5 @@ artifacts
deployment-localhost.json
crytic-export
.certora_internal
coverage
coverage.json

View File

@ -1,6 +1,4 @@
require("@nomiclabs/hardhat-waffle")
require("hardhat-deploy")
require("hardhat-deploy-ethers")
require("@nomicfoundation/hardhat-toolbox")
module.exports = {
solidity: {

View File

@ -0,0 +1,15 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("Endian", (m) => {
const deployer = m.getAccount(0)
const endian = m.contract("Endian", [], {
from: deployer,
})
const testEndian = m.contract("TestEndian", [], {
from: deployer,
})
return { endian, testEndian }
})

View File

@ -0,0 +1,56 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { loadZkeyHash } = require("../../verifier/verifier.js")
const { loadConfiguration } = 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
}
function getDefaultVerifier(m) {
const { verifier } = m.useModule(VerifierModule)
return verifier
}
module.exports = buildModule("Marketplace", (m) => {
const deployer = m.getAccount(0)
const { token } = m.useModule(TokenModule)
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", getDefaultConfig())
const marketplace = m.contract(
"Marketplace",
[configuration, token, verifier],
{
from: deployer,
}
)
let testMarketplace
if (hre.network.config.tags.includes("local")) {
const { testVerifier } = m.useModule(VerifierModule)
testMarketplace = m.contract(
"TestMarketplace",
[configuration, token, testVerifier],
{
from: deployer,
}
)
}
return {
marketplace,
testMarketplace,
token,
}
})
// // Tags and dependencies
// module.exports.tags = ["Marketplace"]

View File

@ -0,0 +1,12 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("Periods", (m) => {
const deployer = m.getAccount(0)
const secondsPerPeriod = m.getParameter("secondsPerPeriod", 0)
const periods = m.contract("Periods", [secondsPerPeriod], {
from: deployer,
})
return { periods }
})

View File

@ -0,0 +1,15 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const VerifierModule = require("./verifier.js")
module.exports = buildModule("Proofs", (m) => {
const deployer = m.getAccount(0)
const { verifier } = m.useModule(VerifierModule)
const configuration = m.getParameter("configuration", null)
const testProofs = m.contract("TestProofs", [configuration, verifier], {
from: deployer,
})
return { testProofs }
})

View File

@ -0,0 +1,17 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
module.exports = buildModule("SlotReservations", (m) => {
const deployer = m.getAccount(0)
const configuration = m.getParameter("configuration", null)
const testSlotReservations = m.contract(
"TestSlotReservations",
[configuration],
{
from: deployer,
}
)
return { testSlotReservations }
})

22
ignition/modules/token.js Normal file
View File

@ -0,0 +1,22 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const MAX_ACCOUNTS = 20
const MINTED_TOKENS = 1_000_000_000_000_000n
module.exports = buildModule("Token", (m) => {
const deployer = m.getAccount(0)
const token = m.contract("TestToken", [], {
from: deployer,
})
if (hre.network.config.tags.includes("local")) {
for (let i = 0; i < MAX_ACCOUNTS; i++) {
const account = m.getAccount(i)
const futureId = "SendingEth" + i
m.send(futureId, account, MINTED_TOKENS)
}
}
return { token }
})

View File

@ -0,0 +1,17 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules")
const { loadVerificationKey } = require("../../verifier/verifier.js")
module.exports = buildModule("Verifier", (m) => {
const deployer = m.getAccount(0)
const verificationKey = loadVerificationKey(hre.network.name)
const verifier = m.contract("Groth16Verifier", [verificationKey], {
from: deployer,
})
const testVerifier = m.contract("TestVerifier", [], {
from: deployer,
})
return { verifier, testVerifier }
})

35068
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,29 +2,28 @@
"name": "codex-contracts-eth",
"license": "MIT",
"scripts": {
"test": "npm run lint && hardhat test",
"test": "npm run lint && hardhat test --parallel",
"fuzz": "hardhat compile && fuzzing/fuzz.sh",
"start": "hardhat node --export deployment-localhost.json",
"start": "hardhat node",
"compile": "hardhat compile",
"format": "prettier --write contracts/*.sol contracts/**/*.sol test/**/*.js",
"format:check": "prettier --check contracts/*.sol contracts/**/*.sol test/**/*.js",
"lint": "solhint contracts/**.sol",
"deploy": "hardhat deploy",
"deploy": "npx hardhat run scripts/deploy.js",
"verify": "npm run verify:marketplace && npm run verify:state_changes",
"verify:marketplace": "certoraRun certora/confs/Marketplace.conf",
"verify:state_changes": "certoraRun certora/confs/StateChanges.conf"
"verify:state_changes": "certoraRun certora/confs/StateChanges.conf",
"coverage": "npx hardhat coverage",
"gas:report": "REPORT_GAS=true npx hardhat test"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@openzeppelin/contracts": "^5.3.0",
"@stdlib/stats-binomial-test": "^0.0.7",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.8",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@stdlib/stats-binomial-test": "^0.2.2",
"chai": "^4.3.7",
"ethereum-waffle": "^3.4.4",
"ethers": "^5.7.2",
"ethers": "6.14.3",
"hardhat": "^2.24.2",
"hardhat-deploy": "^0.11.34",
"hardhat-deploy-ethers": "^0.3.0-beta.13",
"prettier": "^2.8.2",
"prettier-plugin-solidity": "^1.4.2",
"solhint": "^5.0.5"

24
scripts/deploy.js Normal file
View File

@ -0,0 +1,24 @@
const hre = require("hardhat")
const { mine } = require("@nomicfoundation/hardhat-network-helpers")
const MarketplaceModule = require("../ignition/modules/marketplace")
async function main() {
if (hre.network.config.tags.includes("local")) {
await mine(256)
}
const { marketplace, testMarketplace } = await hre.ignition.deploy(
MarketplaceModule
)
console.info("Deployed Marketplace with Groth16 Verifier at:")
console.log(await marketplace.getAddress())
console.log()
console.info("Deployed Marketplace with Test Verifier at:")
console.log(await testMarketplace.getAddress())
}
main().catch(console.error)

View File

@ -1,5 +1,5 @@
const { expect } = require("chai")
const { ethers } = require("hardhat")
const EndianModule = require("../ignition/modules/endian")
describe("Endian", function () {
const big =
@ -10,8 +10,8 @@ describe("Endian", function () {
let endian
beforeEach(async function () {
let Endian = await ethers.getContractFactory("TestEndian")
endian = await Endian.deploy()
const { testEndian } = await ignition.deploy(EndianModule)
endian = testEndian
})
it("converts from little endian to big endian", async function () {

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,35 @@
const { expect } = require("chai")
const { ethers } = require("hardhat")
const PeriodsModule = require("../ignition/modules/periods")
describe("Periods", function () {
it("should revert when secondsPerPeriod is 0", async function () {
const PeriodsContract = await ethers.getContractFactory("Periods")
await expect(PeriodsContract.deploy(0)).to.be.revertedWith(
"Periods_InvalidSecondsPerPeriod"
)
const promise = ignition.deploy(PeriodsModule, {
parameters: {
Periods: {
secondsPerPeriod: 0,
},
},
})
const expectedError = "Periods_InvalidSecondsPerPeriod"
const error = await expect(promise).to.be.rejected
expect(error)
.to.have.property("message")
.that.contains(
expectedError,
`Expected error ${expectedError}, but got ${error.message}`
)
})
it("should not revert when secondsPerPeriod more than 0", async function () {
const PeriodsContract = await ethers.getContractFactory("Periods")
await expect(PeriodsContract.deploy(10)).not.to.be.reverted
const promise = ignition.deploy(PeriodsModule, {
parameters: {
Periods: {
secondsPerPeriod: 10,
},
},
})
await expect(promise).not.to.be.rejected
})
})

View File

@ -1,20 +1,21 @@
const { expect } = require("chai")
const { ethers, deployments } = require("hardhat")
const { hexlify, randomBytes } = ethers.utils
const { ethers } = require("hardhat")
const { hexlify, randomBytes } = ethers
const {
snapshot,
revert,
mine,
ensureMinimumBlockHeight,
currentTime,
advanceTime,
advanceTimeTo,
mine,
} = require("./evm")
const { periodic } = require("./time")
const { loadProof, loadPublicInput } = require("../verifier/verifier")
const { SlotState } = require("./requests")
const binomialTest = require("@stdlib/stats-binomial-test")
const { exampleProof } = require("./examples")
const ProofsModule = require("../ignition/modules/proofs")
describe("Proofs", function () {
const slotId = hexlify(randomBytes(32))
@ -30,13 +31,22 @@ describe("Proofs", function () {
beforeEach(async function () {
await snapshot()
await ensureMinimumBlockHeight(256)
const Proofs = await ethers.getContractFactory("TestProofs")
await deployments.fixture(["Verifier"])
const verifier = await deployments.get("Groth16Verifier")
proofs = await Proofs.deploy(
{ period, timeout, downtime, zkeyHash: "", downtimeProduct },
verifier.address
)
const { testProofs } = await ignition.deploy(ProofsModule, {
parameters: {
Proofs: {
configuration: {
period,
timeout,
downtime,
zkeyHash: "",
downtimeProduct,
},
},
},
})
proofs = testProofs
})
afterEach(async function () {
@ -113,7 +123,7 @@ describe("Proofs", function () {
let previous = await proofs.getPointer(slotId)
await mine()
let current = await proofs.getPointer(slotId)
expect(current).to.equal((previous + 1) % 256)
expect(current).to.equal((previous + 1n) % 256n)
}
})
})
@ -203,14 +213,14 @@ describe("Proofs", function () {
let invalid = exampleProof()
await expect(
proofs.proofReceived(slotId, invalid, pubSignals)
).to.be.revertedWith("Proofs_InvalidProof")
).to.be.revertedWithCustomError(proofs, "Proofs_InvalidProof")
})
it("fails proof submission when public input is incorrect", async function () {
let invalid = [1, 2, 3]
await expect(
proofs.proofReceived(slotId, proof, invalid)
).to.be.revertedWith("Proofs_InvalidProof")
).to.be.revertedWithCustomError(proofs, "Proofs_InvalidProof")
})
it("emits an event when proof was submitted", async function () {
@ -223,7 +233,7 @@ describe("Proofs", function () {
await proofs.proofReceived(slotId, proof, pubSignals)
await expect(
proofs.proofReceived(slotId, proof, pubSignals)
).to.be.revertedWith("Proofs_ProofAlreadySubmitted")
).to.be.revertedWithCustomError(proofs, "Proofs_ProofAlreadySubmitted")
})
it("marks a proof as missing", async function () {
@ -240,7 +250,7 @@ describe("Proofs", function () {
let currentPeriod = periodOf(await currentTime())
await expect(
proofs.markProofAsMissing(slotId, currentPeriod)
).to.be.revertedWith("Proofs_PeriodNotEnded")
).to.be.revertedWithCustomError(proofs, "Proofs_PeriodNotEnded")
})
it("does not mark a proof as missing after timeout", async function () {
@ -249,7 +259,7 @@ describe("Proofs", function () {
await advanceTimeTo(periodEnd(currentPeriod) + timeout + 1)
await expect(
proofs.markProofAsMissing(slotId, currentPeriod)
).to.be.revertedWith("Proofs_ValidationTimedOut")
).to.be.revertedWithCustomError(proofs, "Proofs_ValidationTimedOut")
})
it("does not mark a received proof as missing", async function () {
@ -259,7 +269,7 @@ describe("Proofs", function () {
await advanceTimeTo(periodEnd(receivedPeriod) + 1)
await expect(
proofs.markProofAsMissing(slotId, receivedPeriod)
).to.be.revertedWith("Proofs_ProofNotMissing")
).to.be.revertedWithCustomError(proofs, "Proofs_ProofNotMissing")
})
it("does not mark proof as missing when not required", async function () {
@ -270,7 +280,7 @@ describe("Proofs", function () {
await advanceTimeTo(periodEnd(currentPeriod) + 1)
await expect(
proofs.markProofAsMissing(slotId, currentPeriod)
).to.be.revertedWith("Proofs_ProofNotRequired")
).to.be.revertedWithCustomError(proofs, "Proofs_ProofNotRequired")
})
it("does not mark proof as missing twice", async function () {
@ -280,7 +290,10 @@ describe("Proofs", function () {
await proofs.markProofAsMissing(slotId, missedPeriod)
await expect(
proofs.markProofAsMissing(slotId, missedPeriod)
).to.be.revertedWith("Proofs_ProofAlreadyMarkedMissing")
).to.be.revertedWithCustomError(
proofs,
"Proofs_ProofAlreadyMarkedMissing"
)
})
it("requires no proofs when slot is finished", async function () {

View File

@ -3,6 +3,7 @@ const { ethers } = require("hardhat")
const { exampleRequest, exampleConfiguration } = require("./examples")
const { requestId, slotId } = require("./ids")
const { SlotState } = require("./requests")
const SlotReservationsModule = require("../ignition/modules/slot-reservations")
describe("SlotReservations", function () {
let reservations
@ -15,10 +16,18 @@ describe("SlotReservations", function () {
const config = exampleConfiguration()
beforeEach(async function () {
let SlotReservations = await ethers.getContractFactory(
"TestSlotReservations"
const { testSlotReservations } = await ignition.deploy(
SlotReservationsModule,
{
parameters: {
SlotReservations: {
configuration: config.reservations,
},
},
}
)
reservations = await SlotReservations.deploy(config.reservations)
reservations = testSlotReservations
;[provider, address1, address2, address3] = await ethers.getSigners()
request = await exampleRequest()
@ -76,7 +85,10 @@ describe("SlotReservations", function () {
it("cannot reserve a slot more than once", async function () {
await reservations.reserveSlot(reqId, slotIndex)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith(
await expect(
reservations.reserveSlot(reqId, slotIndex)
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed"
)
expect(await reservations.length(id)).to.equal(1)
@ -95,7 +107,10 @@ describe("SlotReservations", function () {
switchAccount(address3)
await reservations.reserveSlot(reqId, slotIndex)
switchAccount(provider)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith(
await expect(
reservations.reserveSlot(reqId, slotIndex)
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed"
)
expect(await reservations.length(id)).to.equal(3)
@ -115,7 +130,10 @@ describe("SlotReservations", function () {
it("cannot reserve a slot if not free or not in repair", async function () {
await reservations.setSlotState(id, SlotState.Filled)
await expect(reservations.reserveSlot(reqId, slotIndex)).to.be.revertedWith(
await expect(
reservations.reserveSlot(reqId, slotIndex)
).to.be.revertedWithCustomError(
reservations,
"SlotReservations_ReservationNotAllowed"
)
expect(await reservations.length(id)).to.equal(0)

View File

@ -1,63 +1,52 @@
const { ethers } = require("hardhat")
const {
time,
mine,
takeSnapshot,
setNextBlockTimestamp,
} = require("@nomicfoundation/hardhat-network-helpers")
let snapshots = []
const snapshots = []
async function snapshot() {
const id = await ethers.provider.send("evm_snapshot")
const time = await currentTime()
const snapshot = await takeSnapshot()
const automine = await ethers.provider.send("hardhat_getAutomine")
snapshots.push({ id, time, automine })
const time = await currentTime()
snapshots.push({ snapshot, automine, time })
}
async function revert() {
const { id, time, automine } = snapshots.pop()
await ethers.provider.send("evm_revert", [id])
const current = await currentTime()
const nextTime = Math.max(time + 1, current + 1)
await ethers.provider.send("evm_setNextBlockTimestamp", [nextTime])
await ethers.provider.send("evm_setAutomine", [automine])
}
/**
* Enables or disables Hardhat's automine mode.
*
* When automine mode is disabled, transactions that revert are silently ignored!
*/
async function setAutomine(enabled) {
await ethers.provider.send("evm_setAutomine", [enabled])
}
async function mine() {
await ethers.provider.send("evm_mine")
const { snapshot, time, automine } = snapshots.pop()
if (snapshot) {
setNextBlockTimestamp(time)
await ethers.provider.send("evm_setAutomine", [automine])
return snapshot.restore()
}
}
async function ensureMinimumBlockHeight(height) {
while ((await ethers.provider.getBlockNumber()) < height) {
while ((await time.latestBlock()) < height) {
await mine()
}
}
async function setNextBlockTimestamp(timestamp) {
return time.setNextBlockTimestamp(timestamp)
}
async function currentTime() {
let block = await ethers.provider.getBlock("latest")
return block.timestamp
return time.latest()
}
async function advanceTime(seconds) {
await ethers.provider.send("evm_increaseTime", [seconds])
await time.increase(seconds)
await mine()
}
async function advanceTimeTo(timestamp) {
await setNextBlockTimestamp(timestamp)
await time.setNextBlockTimestamp(timestamp)
await mine()
}
async function setNextBlockTimestamp(timestamp) {
await ethers.provider.send("evm_setNextBlockTimestamp", [timestamp])
}
module.exports = {
snapshot,
revert,

View File

@ -1,6 +1,5 @@
const { ethers } = require("hardhat")
const { hours } = require("./time")
const { hexlify, randomBytes } = ethers.utils
const { hexlify, randomBytes } = require("hardhat").ethers
const exampleConfiguration = () => ({
collateral: {
@ -36,7 +35,7 @@ const exampleRequest = async () => {
},
content: {
cid: Buffer.from("zb2rhheVmk3bLks5MgzTqyznLu1zqGH5jrfTA1eAZXrjx7Vob"),
merkleRoot: Array.from(randomBytes(32)),
merkleRoot: randomBytes(32),
},
expiry: hours(1),
nonce: hexlify(randomBytes(32)),

View File

@ -1,12 +1,11 @@
const { ethers } = require("hardhat")
const { keccak256, defaultAbiCoder } = ethers.utils
const { keccak256, AbiCoder } = require("hardhat").ethers
function requestId(request) {
const Ask = "tuple(uint256, uint256, uint256, uint64, uint64, uint64, int64)"
const Content = "tuple(bytes, bytes32)"
const Request =
"tuple(address, " + Ask + ", " + Content + ", uint64, bytes32)"
return keccak256(defaultAbiCoder.encode([Request], requestToArray(request)))
return keccak256(new AbiCoder().encode([Request], requestToArray(request)))
}
function askToArray(ask) {
@ -40,7 +39,7 @@ function requestToArray(request) {
function slotId(slot) {
const types = "tuple(bytes32, uint256)"
const values = [slot.request, slot.index]
const encoding = defaultAbiCoder.encode([types], [values])
const encoding = new AbiCoder().encode([types], [values])
return keccak256(encoding)
}

View File

@ -4,16 +4,16 @@ const { payoutForDuration } = require("./price")
const { collateralPerSlot } = require("./collateral")
async function waitUntilCancelled(contract, request) {
const expiry = (await contract.requestExpiry(requestId(request))).toNumber()
const expiry = await contract.requestExpiry(requestId(request))
// We do +1, because the expiry check in contract is done as `>` and not `>=`.
await advanceTimeTo(expiry + 1)
return advanceTimeTo(expiry + 1n)
}
async function waitUntilSlotsFilled(contract, request, proof, token, slots) {
let collateral = collateralPerSlot(request)
await token.approve(contract.address, collateral * slots.length)
await token.approve(await contract.getAddress(), collateral * slots.length)
let requestEnd = (await contract.requestEnd(requestId(request))).toNumber()
let requestEnd = await contract.requestEnd(requestId(request))
const payouts = []
for (let slotIndex of slots) {
await contract.reserveSlot(requestId(request), slotIndex)
@ -40,9 +40,9 @@ async function waitUntilStarted(contract, request, proof, token) {
}
async function waitUntilFinished(contract, requestId) {
const end = (await contract.requestEnd(requestId)).toNumber()
const end = await contract.requestEnd(requestId)
// We do +1, because the end check in contract is done as `>` and not `>=`.
await advanceTimeTo(end + 1)
await advanceTimeTo(end + 1n)
}
async function waitUntilFailed(contract, request) {
@ -96,6 +96,11 @@ function patchOverloads(contract) {
}
}
function littleEndianToBigInt(littleEndian) {
const buffer = Buffer.from(littleEndian)
return BigInt(`0x${buffer.toString("hex")}`)
}
module.exports = {
waitUntilCancelled,
waitUntilStarted,
@ -104,4 +109,5 @@ module.exports = {
waitUntilFailed,
waitUntilSlotFailed,
patchOverloads,
littleEndianToBigInt,
}

View File

@ -9,7 +9,21 @@ function maxPrice(request) {
}
function payoutForDuration(request, start, end) {
return (end - start) * pricePerSlotPerSecond(request)
return (Number(end) - Number(start)) * pricePerSlotPerSecond(request)
}
module.exports = { maxPrice, pricePerSlotPerSecond, payoutForDuration }
function calculatePartialPayout(request, expiresAt, filledAt) {
return (Number(expiresAt) - Number(filledAt)) * pricePerSlotPerSecond(request)
}
function calculateBalance(balance, reward) {
return BigInt(balance) + BigInt(reward)
}
module.exports = {
maxPrice,
pricePerSlotPerSecond,
payoutForDuration,
calculatePartialPayout,
calculateBalance,
}

View File

@ -1,5 +1,4 @@
const { Assertion } = require("chai")
const { currentTime } = require("./evm")
const RequestState = {
New: 0,
@ -10,13 +9,13 @@ const RequestState = {
}
const SlotState = {
Free: 0,
Filled: 1,
Finished: 2,
Failed: 3,
Paid: 4,
Cancelled: 5,
Repair: 6,
Free: 0n,
Filled: 1n,
Finished: 2n,
Failed: 3n,
Paid: 4n,
Cancelled: 5n,
Repair: 6n,
}
function enableRequestAssertions() {