feat: collateral fractions (#47)
Co-authored-by: Eric Mastro <github@egonat.me>
This commit is contained in:
parent
8b39ef8f4a
commit
2b5d079882
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue