feat: collateral fractions (#47)

Co-authored-by: Eric Mastro <github@egonat.me>
This commit is contained in:
Adam Uhlíř 2023-03-30 11:11:21 +02:00 committed by GitHub
parent 8b39ef8f4a
commit 2b5d079882
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 67 additions and 13 deletions

View File

@ -9,9 +9,15 @@ struct MarketplaceConfig {
} }
struct CollateralConfig { struct CollateralConfig {
uint256 minimumAmount; // frees slot when collateral drops below this minimum /// @dev percentage of remaining collateral slot after it has been freed
uint256 slashCriterion; // amount of proofs missed that lead to slashing /// (equivalent to `collateral - (collateral*maxNumberOfSlashes*slashPercentage)/100`)
uint256 slashPercentage; // percentage of the collateral that is slashed /// 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 { struct ProofConfig {

View File

@ -44,6 +44,10 @@ contract Marketplace is Proofs, StateRetrieval {
MarketplaceConfig memory configuration MarketplaceConfig memory configuration
) Proofs(configuration.proofs) marketplaceInvariant { ) Proofs(configuration.proofs) marketplaceInvariant {
token = token_; 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; config = configuration;
} }
@ -126,17 +130,17 @@ contract Marketplace is Proofs, StateRetrieval {
require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs"); require(slotState(slotId) == SlotState.Filled, "Slot not accepting proofs");
_markProofAsMissing(slotId, period); _markProofAsMissing(slotId, period);
Slot storage slot = _slots[slotId]; Slot storage slot = _slots[slotId];
Request storage request = _requests[slot.requestId];
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) { 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; slot.currentCollateral -= slashedAmount;
_funds.slashed += slashedAmount; _funds.slashed += slashedAmount;
_funds.balance -= slashedAmount; _funds.balance -= slashedAmount;
if (slot.currentCollateral < config.collateral.minimumAmount) { if (missingProofs(slotId) / config.collateral.slashCriterion >= config.collateral.maxNumberOfSlashes) {
// When the collateral drops below the minimum threshold, the slot // When the number of slashings is at or above the allowed amount,
// needs to be freed so that there is enough remaining collateral to be // free the slot.
// distributed for repairs and rewards (with any leftover to be burnt).
_forciblyFreeSlot(slotId); _forciblyFreeSlot(slotId);
} }
} }

View File

@ -8,7 +8,7 @@ struct Request {
address client; address client;
Ask ask; Ask ask;
Content content; 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 bytes32 nonce; // random nonce to differentiate between similar requests
} }

View File

@ -2,7 +2,8 @@ async function deployMarketplace({ deployments, getNamedAccounts }) {
const token = await deployments.get("TestToken") const token = await deployments.get("TestToken")
const configuration = { const configuration = {
collateral: { collateral: {
minimumAmount: 40, repairRewardPercentage: 10,
maxNumberOfSlashes: 5,
slashCriterion: 3, slashCriterion: 3,
slashPercentage: 10, slashPercentage: 10,
}, },

View File

@ -4,6 +4,7 @@
"scripts": { "scripts": {
"test": "npm run lint && hardhat test", "test": "npm run lint && hardhat test",
"start": "hardhat node --export deployment-localhost.json", "start": "hardhat node --export deployment-localhost.json",
"compile": "hardhat compile",
"format": "prettier --write contracts/**/*.sol test/**/*.js", "format": "prettier --write contracts/**/*.sol test/**/*.js",
"lint": "solhint contracts/**.sol" "lint": "solhint contracts/**.sol"
}, },

View File

@ -29,6 +29,47 @@ const {
currentTime, currentTime,
} = require("./evm") } = 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 () { describe("Marketplace", function () {
const proof = hexlify(randomBytes(42)) const proof = hexlify(randomBytes(42))
const config = exampleConfiguration() const config = exampleConfiguration()
@ -765,7 +806,7 @@ describe("Marketplace", function () {
}) })
it("frees slot when collateral slashed below minimum threshold", async 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) await waitUntilStarted(marketplace, request, proof, token)
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) { while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {
expect(await marketplace.getSlotCollateral(slotId(slot))).to.be.gt(minimum) 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 () { 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) await waitUntilStarted(marketplace, request, proof, token)
let missedProofs = 0 let missedProofs = 0
while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) { while ((await marketplace.slotState(slotId(slot))) === SlotState.Filled) {

View File

@ -5,7 +5,8 @@ const { hexlify, randomBytes } = ethers.utils
const exampleConfiguration = () => ({ const exampleConfiguration = () => ({
collateral: { collateral: {
minimumAmount: 40, repairRewardPercentage: 10,
maxNumberOfSlashes: 5,
slashCriterion: 3, slashCriterion: 3,
slashPercentage: 10, slashPercentage: 10,
}, },