[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:
Mark Spanbroek 2023-01-30 10:32:30 +01:00 committed by markspanbroek
parent d57cfc69cd
commit 3390e21071
8 changed files with 58 additions and 31 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ node_modules
cache
artifacts
deployment-localhost.json
crytic-export

View 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);
}
}

View File

@ -16,11 +16,12 @@ contract Marketplace is Proofs, StateRetrieval {
IERC20 public immutable token;
MarketplaceConfig public config;
MarketplaceFunds private _funds;
mapping(RequestId => Request) private _requests;
mapping(RequestId => RequestContext) private _requestContexts;
mapping(SlotId => Slot) internal _slots;
MarketplaceTotals internal _marketplaceTotals;
struct RequestContext {
RequestState state;
uint256 slotsFilled;
@ -48,7 +49,7 @@ contract Marketplace is Proofs, StateRetrieval {
constructor(
IERC20 token_,
MarketplaceConfig memory configuration
) Proofs(configuration.proofs) marketplaceInvariant {
) Proofs(configuration.proofs) {
token = token_;
require(configuration.collateral.repairRewardPercentage <= 100, "Must be less than 100");
@ -57,9 +58,7 @@ contract Marketplace is Proofs, StateRetrieval {
config = configuration;
}
function requestStorage(
Request calldata request
) public marketplaceInvariant {
function requestStorage(Request calldata request) public {
require(request.client == msg.sender, "Invalid client address");
RequestId id = request.id();
@ -71,8 +70,7 @@ contract Marketplace is Proofs, StateRetrieval {
_addToMyRequests(request.client, id);
uint256 amount = request.price();
_funds.received += amount;
_funds.balance += amount;
_marketplaceTotals.received += amount;
_transferFrom(msg.sender, amount);
emit StorageRequested(id, request.ask);
@ -104,8 +102,7 @@ contract Marketplace is Proofs, StateRetrieval {
// Collect collateral
uint256 collateralAmount = request.ask.collateral;
_transferFrom(msg.sender, collateralAmount);
_funds.received += collateralAmount;
_funds.balance += collateralAmount;
_marketplaceTotals.received += collateralAmount;
slot.currentCollateral = collateralAmount;
_addToMySlots(slot.host, slotId);
@ -142,9 +139,6 @@ contract Marketplace is Proofs, StateRetrieval {
if (missingProofs(slotId) % config.collateral.slashCriterion == 0) {
uint256 slashedAmount = (request.ask.collateral * config.collateral.slashPercentage) / 100;
slot.currentCollateral -= slashedAmount;
_funds.slashed += slashedAmount;
_funds.balance -= slashedAmount;
if (missingProofs(slotId) / config.collateral.slashCriterion >= config.collateral.maxNumberOfSlashes) {
// When the number of slashings is at or above the allowed amount,
// 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];
RequestId requestId = slot.requestId;
RequestContext storage context = _requestContexts[requestId];
@ -182,7 +176,7 @@ contract Marketplace is Proofs, StateRetrieval {
function _payoutSlot(
RequestId requestId,
SlotId slotId
) private requestIsKnown(requestId) marketplaceInvariant {
) private requestIsKnown(requestId) {
RequestContext storage context = _requestContexts[requestId];
Request storage request = _requests[requestId];
context.state = RequestState.Finished;
@ -192,8 +186,7 @@ contract Marketplace is Proofs, StateRetrieval {
_removeFromMySlots(slot.host, slotId);
uint256 amount = _requests[requestId].pricePerSlot() + slot.currentCollateral;
_funds.sent += amount;
_funds.balance -= amount;
_marketplaceTotals.sent += amount;
slot.state = SlotState.Paid;
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.
/// @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
function withdrawFunds(RequestId requestId) public marketplaceInvariant {
function withdrawFunds(RequestId requestId) public {
Request storage request = _requests[requestId];
require(block.timestamp > request.expiry, "Request not yet timed out");
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
// deducted from the price.
uint256 amount = request.price();
_funds.sent += amount;
_funds.balance -= amount;
_marketplaceTotals.sent += amount;
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 RequestCancelled(RequestId indexed requestId);
modifier marketplaceInvariant() {
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;
struct MarketplaceTotals {
uint256 received;
uint256 sent;
uint256 slashed;
}
}

3
fuzzing/corpus/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
*
!.gitignore
!.keep

0
fuzzing/corpus/.keep Normal file
View File

6
fuzzing/echidna.yaml Normal file
View 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
View File

@ -0,0 +1,3 @@
#!/bin/bash
set -e
echidna-test . --contract FuzzMarketplace --config fuzzing/echidna.yaml

View File

@ -3,6 +3,7 @@
"license": "MIT",
"scripts": {
"test": "npm run lint && hardhat test",
"fuzz": "fuzzing/fuzz.sh",
"start": "hardhat node --export deployment-localhost.json",
"compile": "hardhat compile",
"format": "prettier --write contracts/**/*.sol test/**/*.js",