[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 cache
artifacts artifacts
deployment-localhost.json 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; 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
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", "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",