mirror of
https://github.com/codex-storage/codex-contracts-eth.git
synced 2025-02-16 13:26:25 +00:00
[fuzzing] Enable fuzzing for Marketplace
Replaces runtime invariant checks with fuzzing tests, simplifying the contract code and lowering gas costs.
This commit is contained in:
parent
d57cfc69cd
commit
3390e21071
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@ node_modules
|
|||||||
cache
|
cache
|
||||||
artifacts
|
artifacts
|
||||||
deployment-localhost.json
|
deployment-localhost.json
|
||||||
|
crytic-export
|
||||||
|
32
contracts/FuzzMarketplace.sol
Normal file
32
contracts/FuzzMarketplace.sol
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
pragma solidity ^0.8.0;
|
||||||
|
|
||||||
|
import "./TestToken.sol";
|
||||||
|
import "./Marketplace.sol";
|
||||||
|
|
||||||
|
contract FuzzMarketplace is Marketplace {
|
||||||
|
constructor()
|
||||||
|
Marketplace(
|
||||||
|
new TestToken(),
|
||||||
|
MarketplaceConfig(CollateralConfig(10, 5, 3, 10), ProofConfig(10, 5, 64))
|
||||||
|
)
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
@ -16,11 +16,12 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
IERC20 public immutable token;
|
IERC20 public immutable token;
|
||||||
MarketplaceConfig public config;
|
MarketplaceConfig public config;
|
||||||
|
|
||||||
MarketplaceFunds private _funds;
|
|
||||||
mapping(RequestId => Request) private _requests;
|
mapping(RequestId => Request) private _requests;
|
||||||
mapping(RequestId => RequestContext) private _requestContexts;
|
mapping(RequestId => RequestContext) private _requestContexts;
|
||||||
mapping(SlotId => Slot) internal _slots;
|
mapping(SlotId => Slot) internal _slots;
|
||||||
|
|
||||||
|
MarketplaceTotals internal _marketplaceTotals;
|
||||||
|
|
||||||
struct RequestContext {
|
struct RequestContext {
|
||||||
RequestState state;
|
RequestState state;
|
||||||
uint256 slotsFilled;
|
uint256 slotsFilled;
|
||||||
@ -48,7 +49,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
constructor(
|
constructor(
|
||||||
IERC20 token_,
|
IERC20 token_,
|
||||||
MarketplaceConfig memory configuration
|
MarketplaceConfig memory configuration
|
||||||
) Proofs(configuration.proofs) marketplaceInvariant {
|
) Proofs(configuration.proofs) {
|
||||||
token = token_;
|
token = token_;
|
||||||
|
|
||||||
require(configuration.collateral.repairRewardPercentage <= 100, "Must be less than 100");
|
require(configuration.collateral.repairRewardPercentage <= 100, "Must be less than 100");
|
||||||
@ -57,9 +58,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
config = configuration;
|
config = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestStorage(
|
function requestStorage(Request calldata request) public {
|
||||||
Request calldata request
|
|
||||||
) public marketplaceInvariant {
|
|
||||||
require(request.client == msg.sender, "Invalid client address");
|
require(request.client == msg.sender, "Invalid client address");
|
||||||
|
|
||||||
RequestId id = request.id();
|
RequestId id = request.id();
|
||||||
@ -71,8 +70,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
_addToMyRequests(request.client, id);
|
_addToMyRequests(request.client, id);
|
||||||
|
|
||||||
uint256 amount = request.price();
|
uint256 amount = request.price();
|
||||||
_funds.received += amount;
|
_marketplaceTotals.received += amount;
|
||||||
_funds.balance += amount;
|
|
||||||
_transferFrom(msg.sender, amount);
|
_transferFrom(msg.sender, amount);
|
||||||
|
|
||||||
emit StorageRequested(id, request.ask);
|
emit StorageRequested(id, request.ask);
|
||||||
@ -104,8 +102,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
// Collect collateral
|
// Collect collateral
|
||||||
uint256 collateralAmount = request.ask.collateral;
|
uint256 collateralAmount = request.ask.collateral;
|
||||||
_transferFrom(msg.sender, collateralAmount);
|
_transferFrom(msg.sender, collateralAmount);
|
||||||
_funds.received += collateralAmount;
|
_marketplaceTotals.received += collateralAmount;
|
||||||
_funds.balance += collateralAmount;
|
|
||||||
slot.currentCollateral = collateralAmount;
|
slot.currentCollateral = collateralAmount;
|
||||||
|
|
||||||
_addToMySlots(slot.host, slotId);
|
_addToMySlots(slot.host, slotId);
|
||||||
@ -142,9 +139,6 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
|
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
|
||||||
uint256 slashedAmount = (request.ask.collateral * config.collateral.slashPercentage) / 100;
|
uint256 slashedAmount = (request.ask.collateral * config.collateral.slashPercentage) / 100;
|
||||||
slot.currentCollateral -= slashedAmount;
|
slot.currentCollateral -= slashedAmount;
|
||||||
_funds.slashed += slashedAmount;
|
|
||||||
_funds.balance -= slashedAmount;
|
|
||||||
|
|
||||||
if (missingProofs(slotId) / config.collateral.slashCriterion >= config.collateral.maxNumberOfSlashes) {
|
if (missingProofs(slotId) / config.collateral.slashCriterion >= config.collateral.maxNumberOfSlashes) {
|
||||||
// When the number of slashings is at or above the allowed amount,
|
// When the number of slashings is at or above the allowed amount,
|
||||||
// free the slot.
|
// free the slot.
|
||||||
@ -153,7 +147,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _forciblyFreeSlot(SlotId slotId) internal marketplaceInvariant {
|
function _forciblyFreeSlot(SlotId slotId) internal {
|
||||||
Slot storage slot = _slots[slotId];
|
Slot storage slot = _slots[slotId];
|
||||||
RequestId requestId = slot.requestId;
|
RequestId requestId = slot.requestId;
|
||||||
RequestContext storage context = _requestContexts[requestId];
|
RequestContext storage context = _requestContexts[requestId];
|
||||||
@ -182,7 +176,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
function _payoutSlot(
|
function _payoutSlot(
|
||||||
RequestId requestId,
|
RequestId requestId,
|
||||||
SlotId slotId
|
SlotId slotId
|
||||||
) private requestIsKnown(requestId) marketplaceInvariant {
|
) private requestIsKnown(requestId) {
|
||||||
RequestContext storage context = _requestContexts[requestId];
|
RequestContext storage context = _requestContexts[requestId];
|
||||||
Request storage request = _requests[requestId];
|
Request storage request = _requests[requestId];
|
||||||
context.state = RequestState.Finished;
|
context.state = RequestState.Finished;
|
||||||
@ -192,8 +186,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
_removeFromMySlots(slot.host, slotId);
|
_removeFromMySlots(slot.host, slotId);
|
||||||
|
|
||||||
uint256 amount = _requests[requestId].pricePerSlot() + slot.currentCollateral;
|
uint256 amount = _requests[requestId].pricePerSlot() + slot.currentCollateral;
|
||||||
_funds.sent += amount;
|
_marketplaceTotals.sent += amount;
|
||||||
_funds.balance -= amount;
|
|
||||||
slot.state = SlotState.Paid;
|
slot.state = SlotState.Paid;
|
||||||
require(token.transfer(slot.host, amount), "Payment failed");
|
require(token.transfer(slot.host, amount), "Payment failed");
|
||||||
}
|
}
|
||||||
@ -201,7 +194,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
/// @notice Withdraws storage request funds back to the client that deposited them.
|
/// @notice Withdraws storage request funds back to the client that deposited them.
|
||||||
/// @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address.
|
/// @dev Request must be expired, must be in RequestState.New, and the transaction must originate from the depositer address.
|
||||||
/// @param requestId the id of the request
|
/// @param requestId the id of the request
|
||||||
function withdrawFunds(RequestId requestId) public marketplaceInvariant {
|
function withdrawFunds(RequestId requestId) public {
|
||||||
Request storage request = _requests[requestId];
|
Request storage request = _requests[requestId];
|
||||||
require(block.timestamp > request.expiry, "Request not yet timed out");
|
require(block.timestamp > request.expiry, "Request not yet timed out");
|
||||||
require(request.client == msg.sender, "Invalid client address");
|
require(request.client == msg.sender, "Invalid client address");
|
||||||
@ -219,8 +212,7 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
// fill a slot. The amount that we paid to hosts will then have to be
|
// fill a slot. The amount that we paid to hosts will then have to be
|
||||||
// deducted from the price.
|
// deducted from the price.
|
||||||
uint256 amount = request.price();
|
uint256 amount = request.price();
|
||||||
_funds.sent += amount;
|
_marketplaceTotals.sent += amount;
|
||||||
_funds.balance -= amount;
|
|
||||||
require(token.transfer(msg.sender, amount), "Withdraw failed");
|
require(token.transfer(msg.sender, amount), "Withdraw failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -322,19 +314,8 @@ contract Marketplace is Proofs, StateRetrieval {
|
|||||||
event SlotFreed(RequestId indexed requestId, SlotId slotId);
|
event SlotFreed(RequestId indexed requestId, SlotId slotId);
|
||||||
event RequestCancelled(RequestId indexed requestId);
|
event RequestCancelled(RequestId indexed requestId);
|
||||||
|
|
||||||
modifier marketplaceInvariant() {
|
struct MarketplaceTotals {
|
||||||
MarketplaceFunds memory oldFunds = _funds;
|
|
||||||
_;
|
|
||||||
assert(_funds.received >= oldFunds.received);
|
|
||||||
assert(_funds.sent >= oldFunds.sent);
|
|
||||||
assert(_funds.slashed >= oldFunds.slashed);
|
|
||||||
assert(_funds.received == _funds.balance + _funds.sent + _funds.slashed);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MarketplaceFunds {
|
|
||||||
uint256 balance;
|
|
||||||
uint256 received;
|
uint256 received;
|
||||||
uint256 sent;
|
uint256 sent;
|
||||||
uint256 slashed;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3
fuzzing/corpus/.gitignore
vendored
Normal file
3
fuzzing/corpus/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
!.keep
|
0
fuzzing/corpus/.keep
Normal file
0
fuzzing/corpus/.keep
Normal file
6
fuzzing/echidna.yaml
Normal file
6
fuzzing/echidna.yaml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# configure Echidna fuzzing tests
|
||||||
|
|
||||||
|
testMode: "assertion" # check that solidity asserts are never triggered
|
||||||
|
multi-abi: true # allow calls to e.g. TestToken in test scenarios
|
||||||
|
corpusDir: "fuzzing/corpus" # collect coverage maximizing corpus in this dir
|
||||||
|
format: "text" # disable interactive ui
|
3
fuzzing/fuzz.sh
Executable file
3
fuzzing/fuzz.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
echidna-test . --contract FuzzMarketplace --config fuzzing/echidna.yaml
|
@ -3,6 +3,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint && hardhat test",
|
"test": "npm run lint && hardhat test",
|
||||||
|
"fuzz": "fuzzing/fuzz.sh",
|
||||||
"start": "hardhat node --export deployment-localhost.json",
|
"start": "hardhat node --export deployment-localhost.json",
|
||||||
"compile": "hardhat compile",
|
"compile": "hardhat compile",
|
||||||
"format": "prettier --write contracts/**/*.sol test/**/*.js",
|
"format": "prettier --write contracts/**/*.sol test/**/*.js",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user