From 2b5d07988289eb918e9d9c3d9300211af0b7823f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Uhl=C3=AD=C5=99?= Date: Thu, 30 Mar 2023 11:11:21 +0200 Subject: [PATCH] feat: collateral fractions (#47) Co-authored-by: Eric Mastro --- contracts/Configuration.sol | 12 +++++++--- contracts/Marketplace.sol | 14 +++++++----- contracts/Requests.sol | 2 +- deploy/marketplace.js | 3 ++- package.json | 1 + test/Marketplace.test.js | 45 +++++++++++++++++++++++++++++++++++-- test/examples.js | 3 ++- 7 files changed, 67 insertions(+), 13 deletions(-) diff --git a/contracts/Configuration.sol b/contracts/Configuration.sol index 6b885ae..416f431 100644 --- a/contracts/Configuration.sol +++ b/contracts/Configuration.sol @@ -9,9 +9,15 @@ struct MarketplaceConfig { } struct CollateralConfig { - uint256 minimumAmount; // frees slot when collateral drops below this minimum - uint256 slashCriterion; // amount of proofs missed that lead to slashing - uint256 slashPercentage; // percentage of the collateral that is slashed + /// @dev percentage of remaining collateral slot after it has been freed + /// (equivalent to `collateral - (collateral*maxNumberOfSlashes*slashPercentage)/100`) + /// TODO: to be aligned more closely with actual cost of repair once bandwidth incentives are known, + /// see https://github.com/status-im/codex-contracts-eth/pull/47#issuecomment-1465511949. + uint8 repairRewardPercentage; + + uint8 maxNumberOfSlashes; // frees slot when the number of slashing reaches this value + uint16 slashCriterion; // amount of proofs missed that lead to slashing + uint8 slashPercentage; // percentage of the collateral that is slashed } struct ProofConfig { diff --git a/contracts/Marketplace.sol b/contracts/Marketplace.sol index 3926381..1a7ede7 100644 --- a/contracts/Marketplace.sol +++ b/contracts/Marketplace.sol @@ -44,6 +44,10 @@ contract Marketplace is Proofs, StateRetrieval { MarketplaceConfig memory configuration ) Proofs(configuration.proofs) marketplaceInvariant { token = token_; + + require(configuration.collateral.repairRewardPercentage <= 100, "Must be less than 100"); + require(configuration.collateral.slashPercentage <= 100, "Must be less than 100"); + require(configuration.collateral.maxNumberOfSlashes * configuration.collateral.slashPercentage <= 100, "Total slash percentage must be less then 100"); config = configuration; } @@ -126,17 +130,17 @@ contract Marketplace is Proofs, StateRetrieval { require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs"); _markProofAsMissing(slotId, period); Slot storage slot = _slots[slotId]; + Request storage request = _requests[slot.requestId]; if (missingProofs(slotId) % config.collateral.slashCriterion == 0) { - uint256 slashedAmount = (slot.currentCollateral * config.collateral.slashPercentage) / 100; + uint256 slashedAmount = (request.ask.collateral * config.collateral.slashPercentage) / 100; slot.currentCollateral -= slashedAmount; _funds.slashed += slashedAmount; _funds.balance -= slashedAmount; - if (slot.currentCollateral < config.collateral.minimumAmount) { - // When the collateral drops below the minimum threshold, the slot - // needs to be freed so that there is enough remaining collateral to be - // distributed for repairs and rewards (with any leftover to be burnt). + if (missingProofs(slotId) / config.collateral.slashCriterion >= config.collateral.maxNumberOfSlashes) { + // When the number of slashings is at or above the allowed amount, + // free the slot. _forciblyFreeSlot(slotId); } } diff --git a/contracts/Requests.sol b/contracts/Requests.sol index 9102d09..da730f8 100644 --- a/contracts/Requests.sol +++ b/contracts/Requests.sol @@ -8,7 +8,7 @@ struct Request { address client; Ask ask; Content content; - uint256 expiry; // time at which this request timeouts if all slots are not filled and is pronounced cancelled + uint256 expiry; // timestamp as seconds since unix epoch at which this request expires bytes32 nonce; // random nonce to differentiate between similar requests } diff --git a/deploy/marketplace.js b/deploy/marketplace.js index 08b5d55..2640654 100644 --- a/deploy/marketplace.js +++ b/deploy/marketplace.js @@ -2,7 +2,8 @@ async function deployMarketplace({ deployments, getNamedAccounts }) { const token = await deployments.get("TestToken") const configuration = { collateral: { - minimumAmount: 40, + repairRewardPercentage: 10, + maxNumberOfSlashes: 5, slashCriterion: 3, slashPercentage: 10, }, diff --git a/package.json b/package.json index 85d5812..7c2c4b4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "test": "npm run lint && hardhat test", "start": "hardhat node --export deployment-localhost.json", + "compile": "hardhat compile", "format": "prettier --write contracts/**/*.sol test/**/*.js", "lint": "solhint contracts/**.sol" }, diff --git a/test/Marketplace.test.js b/test/Marketplace.test.js index a4878fc..45aa5e8 100644 --- a/test/Marketplace.test.js +++ b/test/Marketplace.test.js @@ -29,6 +29,47 @@ const { currentTime, } = require("./evm") +describe('Marketplace constructor', function () { + let Marketplace, token, config + + beforeEach(async function () { + await snapshot() + await ensureMinimumBlockHeight(256) + + const TestToken = await ethers.getContractFactory("TestToken") + token = await TestToken.deploy() + + Marketplace = await ethers.getContractFactory("TestMarketplace") + config = exampleConfiguration() + }) + + afterEach(async function () { + await revert() + }) + + function testPercentageOverflow(property) { + it(`should reject for ${property} overflowing percentage values`, async () => { + config.collateral[property] = 101 + + await expect(Marketplace.deploy(token.address, config)).to.be.revertedWith( + "Must be less than 100" + ) + }) + } + + testPercentageOverflow('repairRewardPercentage') + testPercentageOverflow('slashPercentage') + + it('should reject when total slash percentage exceeds 100%', async () => { + config.collateral.slashPercentage = 1 + config.collateral.maxNumberOfSlashes = 101 + + await expect(Marketplace.deploy(token.address, config)).to.be.revertedWith( + "Total slash percentage must be less then 100" + ) + }) +}) + describe("Marketplace", function () { const proof = hexlify(randomBytes(42)) const config = exampleConfiguration() @@ -765,7 +806,7 @@ describe("Marketplace", function () { }) it("frees slot when collateral slashed below minimum threshold", async function () { - const minimum = config.collateral.minimumAmount + const minimum = request.ask.collateral - (request.ask.collateral*config.collateral.maxNumberOfSlashes*config.collateral.slashPercentage)/100 await waitUntilStarted(marketplace, request, proof, token) while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) { expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(minimum) @@ -779,7 +820,7 @@ describe("Marketplace", function () { }) it("free slot when minimum reached and resets missed proof counter", async function () { - const minimum = config.collateral.minimumAmount + const minimum = request.ask.collateral - (request.ask.collateral*config.collateral.maxNumberOfSlashes*config.collateral.slashPercentage)/100 await waitUntilStarted(marketplace, request, proof, token) let missedProofs = 0 while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) { diff --git a/test/examples.js b/test/examples.js index 4fcad08..9096a8e 100644 --- a/test/examples.js +++ b/test/examples.js @@ -5,7 +5,8 @@ const { hexlify, randomBytes } = ethers.utils const exampleConfiguration = () => ({ collateral: { - minimumAmount: 40, + repairRewardPercentage: 10, + maxNumberOfSlashes: 5, slashCriterion: 3, slashPercentage: 10, },