Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(medusa): multiple strat #48

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions test/invariant/PROPERTIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,15 @@
| Function Name | Sighash | Function Signature | Handler |
| ------------------------- | -------- | ---------------------------------------------------------------- | ------- |
| initialize | c4d66de8 | initialize(address) | NA |
| createProfile | 3a92f65f | createProfile(uint256,string,(uint256,string),address,address[]) | [] |
| updateProfileName | cf189ff2 | updateProfileName(bytes32,string) | [] |
| updateProfileMetadata | ac402839 | updateProfileMetadata(bytes32,(uint256,string)) | [] |
| updateProfilePendingOwner | 3b66dacd | updateProfilePendingOwner(bytes32,address) | [] |
| acceptProfileOwnership | 2497f3c6 | acceptProfileOwnership(bytes32) | [] |
| addMembers | 5063f361 | addMembers(bytes32,address[]) | [] |
| removeMembers | e0cf1e4c | removeMembers(bytes32,address[]) | [] |
| recoverFunds | 24ae6a27 | recoverFunds(address,address) | [] |
| createProfile | 3a92f65f | createProfile(uint256,string,(uint256,string),address,address[]) | [x] |
| updateProfileName | cf189ff2 | updateProfileName(bytes32,string) | [x] |
| updateProfileMetadata | ac402839 | updateProfileMetadata(bytes32,(uint256,string)) | [x] |
| updateProfilePendingOwner | 3b66dacd | updateProfilePendingOwner(bytes32,address) | [x] |
| acceptProfileOwnership | 2497f3c6 | acceptProfileOwnership(bytes32) | [x] |
| addMembers | 5063f361 | addMembers(bytes32,address[]) | [x] |
| removeMembers | e0cf1e4c | removeMembers(bytes32,address[]) | [x] |
| recoverFunds | 24ae6a27 | recoverFunds(address,address) | []* |
* via Allo.sol

## Base strategy
| Function Name | Sighash | Function Signature | Handler |
Expand Down
5 changes: 2 additions & 3 deletions test/invariant/fuzz/FuzzTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {PropertiesParent} from "./properties/PropertiesParent.t.sol";

contract FuzzTest is PropertiesParent {
/// @custom:property-id 0
/// @custom:property Check if
/// @custom:property Check sanity
function property_sanityCheck() public {
assertTrue(address(allo) != address(0), "sanity check");
assertTrue(address(registry) != address(0), "sanity check");
Expand All @@ -15,6 +15,5 @@ contract FuzzTest is PropertiesParent {
assertTrue(allo.isTrustedForwarder(forwarder), "sanity check");
}

// This is a good place to include Forge test for debugging purposes
function test_forgeDebug() public {}
function test_debug() public {}
}
118 changes: 113 additions & 5 deletions test/invariant/fuzz/Setup.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,30 @@ import {Allo, IAllo, Metadata} from "contracts/core/Allo.sol";
import {Registry, Anchor} from "contracts/core/Anchor.sol";
import {IRegistry} from "contracts/core/interfaces/IRegistry.sol";
import {DirectAllocationStrategy} from "contracts/strategies/examples/direct-allocation/DirectAllocation.sol";
import {QVSimple} from "contracts/strategies/examples/quadratic-voting/QVSimple.sol";
import {SQFSuperfluid} from "contracts/strategies/examples/sqf-superfluid/SQFSuperfluid.sol";

import {IRecipientsExtension} from "strategies/extensions/register/IRecipientsExtension.sol";

import {Actors} from "./helpers/Actors.t.sol";
import {Pools} from "./helpers/Pools.t.sol";
import {Utils} from "./helpers/Utils.t.sol";
import {FuzzERC20, ERC20} from "./helpers/FuzzERC20.sol";

contract Setup is Actors {
contract Setup is Actors, Pools {
uint256 percentFee;
uint256 baseFee;

uint64 defaultRegistrationStartTime;
uint64 defaultRegistrationEndTime;
uint256 defaultAllocationStartTime;
uint256 defaultAllocationEndTime;
uint256 defaultWithdrawalCooldown;
uint256 DEFAULT_MAX_BID;

Allo allo;
Registry registry;

DirectAllocationStrategy strategy_directAllocation;

ERC20 token;

address protocolDeployer = makeAddr("protocolDeployer");
Expand Down Expand Up @@ -58,8 +68,10 @@ contract Setup is Actors {
forwarder
);

// Deploy base strategy
strategy_directAllocation = new DirectAllocationStrategy(address(allo));
// Deploy strategies implementations
_initImplementations(address(allo));

// strategy_directAllocation = new DirectAllocationStrategy(address(allo));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we remove this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also yes


// Deploy token
token = ERC20(address(new FuzzERC20()));
Expand All @@ -79,5 +91,101 @@ contract Setup is Actors {
registry.getProfileById(_id).anchor
);
}

// Create pools for each strategy
_initPools();
}

function _initPools() internal {
defaultRegistrationStartTime = uint64(block.timestamp);
defaultRegistrationEndTime = uint64(block.timestamp + 7 days);
defaultAllocationStartTime = uint64(block.timestamp + 7 days + 1);
defaultAllocationEndTime = uint64(block.timestamp + 10 days);
defaultWithdrawalCooldown = 1 days;
DEFAULT_MAX_BID = 1000;

for (uint256 i = 1; i <= uint256(type(PoolStrategies).max); i++) {
address _deployer = _ghost_actors[i % 4];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is it % 4 ? if we are interested to use a specific address we could mark it explicitly just in case _ghost_actors is changed in the future

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first 4 actors are anchor owner, no?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense


IRegistry.Profile memory profile = registry.getProfileByAnchor(
_ghost_anchorOf[_deployer]
);

bytes memory _metadata;

if (PoolStrategies(i) == PoolStrategies.DirectAllocation) {
_metadata = "";
} else if (PoolStrategies(i) == PoolStrategies.DonationVoting) {
_metadata = abi.encode(
IRecipientsExtension.RecipientInitializeData({
metadataRequired: false,
registrationStartTime: defaultRegistrationStartTime,
registrationEndTime: defaultRegistrationEndTime
}),
defaultAllocationStartTime,
defaultAllocationEndTime,
defaultWithdrawalCooldown,
token,
true
);
} else if (
PoolStrategies(i) == PoolStrategies.EasyRPGF
) {} else if (PoolStrategies(i) == PoolStrategies.ImpactStream) {
_metadata = abi.encode(
IRecipientsExtension.RecipientInitializeData({
metadataRequired: false,
registrationStartTime: uint64(block.timestamp),
registrationEndTime: uint64(block.timestamp + 7 days)
}),
QVSimple.QVSimpleInitializeData({
allocationStartTime: uint64(block.timestamp),
allocationEndTime: uint64(block.timestamp + 7 days),
maxVoiceCreditsPerAllocator: 100,
isUsingAllocationMetadata: false
})
);
} else if (PoolStrategies(i) == PoolStrategies.QuadraticVoting) {
_metadata = abi.encode(
IRecipientsExtension.RecipientInitializeData({
metadataRequired: false,
registrationStartTime: uint64(block.timestamp),
registrationEndTime: uint64(block.timestamp + 7 days)
}),
QVSimple.QVSimpleInitializeData({
allocationStartTime: uint64(block.timestamp),
allocationEndTime: uint64(block.timestamp + 7 days),
maxVoiceCreditsPerAllocator: 100,
isUsingAllocationMetadata: false
})
);
} else if (PoolStrategies(i) == PoolStrategies.RFP) {
_metadata = abi.encode(
IRecipientsExtension.RecipientInitializeData({
metadataRequired: false,
registrationStartTime: uint64(block.timestamp),
registrationEndTime: uint64(block.timestamp + 7 days)
}),
DEFAULT_MAX_BID
);
} else if (PoolStrategies(i) == PoolStrategies.SQFSuperfluid) {
// Skip for now - mock?
return;
}

vm.prank(_deployer);
uint256 _poolId = allo.createPool(
profile.id,
_strategyImplementations[PoolStrategies(i)],
_metadata,
address(token),
0,
profile.metadata,
new address[](0)
);

ghost_poolAdmins[_poolId] = _deployer;

_recordPool(_poolId, PoolStrategies(i));
}
}
}
27 changes: 16 additions & 11 deletions test/invariant/fuzz/handlers/HandlerAllo.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,23 @@ pragma solidity ^0.8.19;

import {Setup} from "../Setup.t.sol";
import {IRegistry} from "contracts/core/Registry.sol";
import {Allo, Metadata} from "contracts/core/Allo.sol";
import {Allo, IAllo, Metadata} from "contracts/core/Allo.sol";
import {FuzzERC20} from "../helpers/FuzzERC20.sol";

contract HandlerAllo is Setup {
uint256[] ghost_poolIds;
mapping(uint256 _poolId => address[] _managers) ghost_poolManagers;
mapping(uint256 _poolId => address _poolAdmin) ghost_poolAdmins;
mapping(uint256 _poolId => address[] _recipients) ghost_recipients;

function handler_createPool(uint256 _msgValue) public {
function handler_createPool(
uint256 _msgValue,
uint256 _seedPoolStrategy
) public {
_seedPoolStrategy = bound(
_seedPoolStrategy,
uint256(type(PoolStrategies).min) + 1, // Avoid None elt
uint256(type(PoolStrategies).max)
);

// Get the profile ID
IRegistry.Profile memory profile = registry.getProfileByAnchor(
_ghost_anchorOf[msg.sender]
Expand All @@ -21,6 +28,10 @@ contract HandlerAllo is Setup {
// Avoid EOA
if (profile.anchor == address(0)) return;

// Avoid redeploying pool with a strategy already tested
if (_strategyHasImplementation(PoolStrategies(_seedPoolStrategy)))
return;

// Create a pool
(bool succ, bytes memory ret) = targetCall(
address(allo),
Expand All @@ -29,7 +40,7 @@ contract HandlerAllo is Setup {
allo.createPool,
(
profile.id,
address(strategy_directAllocation),
_strategyImplementations[PoolStrategies(_seedPoolStrategy)],
bytes(""),
address(token),
0,
Expand All @@ -38,12 +49,6 @@ contract HandlerAllo is Setup {
)
)
);

if (succ) {
uint256 _poolId = abi.decode(ret, (uint256));
ghost_poolIds.push(_poolId);
ghost_poolAdmins[_poolId] = msg.sender;
}
}

function handler_updatePoolMetadata(
Expand Down
4 changes: 2 additions & 2 deletions test/invariant/fuzz/handlers/HandlerStrategy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ contract HandlerStrategy is HandlerAllo {

// Withdraw
(bool succ, ) = targetCall(
address(allo),
address(_pool.strategy),
0,
abi.encodeCall(
strategy_directAllocation.withdraw,
BaseStrategy.withdraw,
(_pool.token, _amount, _recipient)
)
);
Expand Down
91 changes: 91 additions & 0 deletions test/invariant/fuzz/helpers/Pools.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {Utils} from "./Utils.t.sol";
import {Anchor} from "contracts/core/Anchor.sol";
import {Allo, IAllo} from "contracts/core/Allo.sol";

import {DirectAllocationStrategy} from "contracts/strategies/examples/direct-allocation/DirectAllocation.sol";
import {DonationVotingOnchain} from "contracts/strategies/examples/donation-voting/DonationVotingOnchain.sol";
import {EasyRPGF} from "contracts/strategies/examples/easy-rpgf/EasyRPGF.sol";
import {QVImpactStream} from "contracts/strategies/examples/impact-stream/QVImpactStream.sol";
import {QVSimple} from "contracts/strategies/examples/quadratic-voting/QVSimple.sol";
import {RFPSimple} from "contracts/strategies/examples/rfp/RFPSimple.sol";
import {SQFSuperfluid} from "contracts/strategies/examples/sqf-superfluid/SQFSuperfluid.sol";

contract Pools is Utils {
Allo private allo;

enum PoolStrategies {
None,
DirectAllocation,
DonationVoting,
EasyRPGF,
ImpactStream,
QuadraticVoting,
RFP,
SQFSuperfluid
}

uint256[] internal ghost_poolIds;
mapping(uint256 _poolId => address _poolAdmin) ghost_poolAdmins;

// mapping(uint256 _poolId => PoolStrategies _strategy) internal _poolStrategy;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this mapping be deleted?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes


mapping(PoolStrategies _strategy => address _implementation)
internal _strategyImplementations;

function _initImplementations(address _allo) internal {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

_strategyImplementations[PoolStrategies.DirectAllocation] = address(
new DirectAllocationStrategy(_allo)
);
_strategyImplementations[PoolStrategies.DonationVoting] = address(
new DonationVotingOnchain(_allo, "MyFancyName")
);
_strategyImplementations[PoolStrategies.EasyRPGF] = address(
new EasyRPGF(_allo)
);
_strategyImplementations[PoolStrategies.ImpactStream] = address(
new QVImpactStream(_allo)
);
_strategyImplementations[PoolStrategies.QuadraticVoting] = address(
new QVSimple(_allo, "MyFancyName")
);
_strategyImplementations[PoolStrategies.RFP] = address(
new RFPSimple(_allo)
);
_strategyImplementations[PoolStrategies.SQFSuperfluid] = address(
new SQFSuperfluid(_allo)
);

allo = Allo(_allo);
}

function _recordPool(uint256 _poolId, PoolStrategies _strategy) internal {
// _poolStrategy[_poolId] = _strategy;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be deleted?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

ghost_poolIds.push(_poolId);
}

// reverse lookup pool id -> strategy type
function _poolStrategy(uint256 _poolId) internal returns (PoolStrategies) {
IAllo.Pool memory _pool = allo.getPool(_poolId);
for (uint256 i; i < uint256(type(PoolStrategies).max); i++)
if (
_strategyImplementations[PoolStrategies(i)] ==
address(_pool.strategy)
) return PoolStrategies(i);

emit TestFailure("Wrong pool strategy implementation address");
}

function _strategyHasImplementation(
Copy link

@0xRaccoon 0xRaccoon Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another option is to use an aux mapping(PoolStrategies strategy => address implementation) to avoid the for loop

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as we are not optimising for gas, I think we should avoid as much as possible having state in the test contract, when we can easily(ish) compute it instead - wdyt?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it's fine for me

PoolStrategies _strategy
) internal returns (bool) {
for (uint256 i; i < ghost_poolIds.length; i++) {
if (_poolStrategy(ghost_poolIds[i]) == _strategy) return true;
}

return false;
}
}
Loading
Loading