Reputation: 2808
After having written an invariant fuzz test on a Solidity code,that uses in quite a bit of boilerplate code in order to randomly initialise the contract that is tested, I would like to re-use this random initialisation process in other fuzz tests.
pragma solidity ^0.8.24;
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
import { PRBTest } from "@prb/src/PRBTest.sol";
import { console2 } from "forge-std/src/console2.sol";
import { StdCheats } from "forge-std/src/StdCheats.sol";
import "forge-std/src/Vm.sol";
import { Test } from "lib/forge-std/src/Test.sol";
import "test/TestConstants.sol";
import { DecentralisedInvestmentManager } from "./../../../../../src/DecentralisedInvestmentManager.sol";
import { Helper } from "./../../../../../src/Helper.sol";
import { CheckFuzzTestScopes } from "./../../../../../test/helper/CheckFuzzTestScopes.sol";
contract TriggerReturnAllHandlerTests is Test, ReentrancyGuard {
DecentralisedInvestmentManager public immutable DIM;
// First it checks if the sum of the investments yield an overflow or not.
uint256 public investmentOverflow;
uint256 public noInvestmentOverflow;
// If the investments did not throw an overflow, it checks if it can initialise the DIM.
uint256 public validInitialisations;
uint256 public invalidInitialisations;
// If the DIM is safely initialised, it checks if it can validly perform the investments or not.
uint256 public validInvestments;
uint256 public invalidInvestments;
// If the investments are valid, it checks in followUpTriggerReturn all, whether it actually returned all funds or not.
uint256 public didReachInvestmentCeiling;
uint256 public didNotReachInvestmentCeiling;
address internal _projectLead;
CheckFuzzTestScopes private _checkFuzzTestScopes;
Helper private _helper;
/**
@dev Generates random contract initialisations, and if a relevant contract configuration is found, it performs the
triggerReturnAll test.
- This test asserts the triggerReturnAll function throws an error if the projectLead tries to
return funds even though the investmentTarget is reached.
- The test asserts the triggerReturnAll function returns the funds if the raisePeriod has passed without reaching the
investmentTarget. */
function testRandomNrOfInvestments(
address projectLead,
uint256 projectLeadFracNumerator,
uint256 projectLeadFracDenominator,
uint256 investmentTarget,
uint32 additionalWaitPeriod,
uint32 raisePeriod,
uint8 randNrOfInvestmentTiers,
uint256 randNrOfInvestments,
uint256[_MAX_NR_OF_TIERS] memory randomCeilings,
uint8[_MAX_NR_OF_TIERS] memory randomMultiples,
uint256[_MAX_NR_OF_INVESTMENTS] memory randomInvestments
) public {
randNrOfInvestments = bound(randNrOfInvestments, 0, _MAX_NR_OF_INVESTMENTS);
_checkFuzzTestScopes = new CheckFuzzTestScopes();
// Check if the random investment amounts lead to an overflow when added. If not, continue.
(
bool hasNoInvestmentOverflow,
uint256[] memory investmentAmounts,
uint256[] memory sortedCeilings,
uint8[] memory lowerBoundedMultiples
) = _checkFuzzTestScopes.hasNoInvestmentOverflow({
randNrOfInvestmentTiers: randNrOfInvestmentTiers,
randNrOfInvestments: randNrOfInvestments,
randomCeilings: randomCeilings,
randomMultiples: randomMultiples,
randomInvestments: randomInvestments
});
if (hasNoInvestmentOverflow) {
++noInvestmentOverflow;
// Check if the random variables lead to a valid DIM contract. If yes, continue.
(bool hasInitialisedRandomDim, DecentralisedInvestmentManager someDim) = _checkFuzzTestScopes
.hasValidRandomDimInitialisation({
projectLead: projectLead,
projectLeadFracNumerator: projectLeadFracNumerator,
projectLeadFracDenominator: projectLeadFracDenominator,
investmentTarget: investmentTarget,
ceilings: sortedCeilings,
lowerBoundedMultiples: lowerBoundedMultiples,
raisePeriod: raisePeriod,
sortedCeilings: sortedCeilings
});
if (hasInitialisedRandomDim) {
// Check if the initialised dim is random or non-random value.
++validInitialisations;
// Check if all investments were performed successfully. If yes, proceed.
if (
_checkFuzzTestScopes.performedValidInvestments({ investmentAmounts: investmentAmounts, someDim: someDim })
) {
// Store that this random run was for a valid investment, (track it to export it later).
++validInvestments;
(bool reachedInvestmentCeiling, uint256 cumInvestmentAmount) = _checkFuzzTestScopes.reachedInvestmentCeiling({
investmentAmounts: investmentAmounts,
investmentTarget: investmentTarget
});
// Check if the investmentCeiling was reached, or not, and call the accompanying triggerReturnAll test.
if (reachedInvestmentCeiling) {
++didReachInvestmentCeiling;
_triggerReturnAllRevertsWithInvestmentTargetReached({
someDim: someDim,
projectLead: projectLead,
investmentTarget: investmentTarget,
cumInvestmentAmount: cumInvestmentAmount,
additionalWaitPeriod: additionalWaitPeriod,
raisePeriod: raisePeriod,
sortedCeilings: sortedCeilings
});
} else {
++didNotReachInvestmentCeiling;
_assertTriggerReturnReturnsInvestorFunds({
someDim: someDim,
projectLead: projectLead,
cumInvestmentAmount: cumInvestmentAmount,
additionalWaitPeriod: additionalWaitPeriod,
raisePeriod: raisePeriod
});
}
} else {
++invalidInvestments;
}
} else {
++invalidInitialisations;
}
} else {
++investmentOverflow;
}
}
/**
Tests whether the triggerReturnAll() function returns all funds from the DIM contract if the investment ceiling is
reached. Otherwise it verifies the triggerReturnAll() function throws an error saying the investment target is
reached.
To ensure the funds can be returned, the vm automatically simulates a fast forward of the time to beyond the raise
period.
@dev This is the actual test that this file executes. */
// solhint-disable-next-line foundry-test-functions
function _triggerReturnAllRevertsWithInvestmentTargetReached(
DecentralisedInvestmentManager someDim,
address projectLead,
uint256 investmentTarget,
uint256 cumInvestmentAmount,
uint32 additionalWaitPeriod,
uint32 raisePeriod,
uint256[] memory sortedCeilings
) internal {
_helper = new Helper();
// Only the projectLead can trigger the return of all funds.
vm.prank(projectLead);
// For testing purposes, time is simulated to beyond the raise period. Another test will test calls before the raise period.
// solhint-disable-next-line not-rely-on-time
vm.warp(block.timestamp + raisePeriod + additionalWaitPeriod);
// Get the maximum ceiling of the last/highest tier.
uint256 maxTierCeiling = sortedCeilings[sortedCeilings.length - 1];
// If the investment target is reached, the funds should not be returnable, because the project lead should
// ensure the work is done to retrieve the funds.
vm.expectRevert(
abi.encodeWithSignature(
"InvestmentTargetReached(string,uint256,uint256)",
"Investment target reached!",
_helper.minimum(maxTierCeiling, cumInvestmentAmount),
investmentTarget
)
);
someDim.triggerReturnAll();
}
function _assertTriggerReturnReturnsInvestorFunds(
DecentralisedInvestmentManager someDim,
address projectLead,
uint256 cumInvestmentAmount,
uint32 additionalWaitPeriod,
uint32 raisePeriod
) internal {
// TODO: Verify the DIM contract contains the investment funds.
vm.prank(projectLead);
// solhint-disable-next-line not-rely-on-time
vm.warp(block.timestamp + raisePeriod + additionalWaitPeriod);
someDim.triggerReturnAll();
// Verify the funds from the DIM contract were not in the DIM contract anymore.
assertEq(address(someDim).balance, 0 ether, "The DIM did not contain 0 ether after returning all investments.");
// TODO: verify the investors have retrieved their investments.
}
}
Different questions will require different levels of scope, for example, some may only need hasNoInvestmentOverflow
to be true
, whereas others, like the one in the example, may need all 4 if conditions to be true
. However, if not all 4 conditions are true, I do not know how to return all the required objects (as they cannot all be initialised if some are not true). However Solidity typing needs to always have the same objects to be returned. I already create a dummy DecentralisedInvestmentManager
contract if it is invalid for it to be returned. However, I expect that this is not a good coding paradigm. Hence I was wondering if there is a better way to refactor this code.
How could I refactor the 4 checks to allow different invariant fuzz test functions to get the scope they need?
Upvotes: 0
Views: 14