diff --git a/test/invariant/PROPERTIES.md b/test/invariant/PROPERTIES.md index 459d61f3..e13a7df6 100644 --- a/test/invariant/PROPERTIES.md +++ b/test/invariant/PROPERTIES.md @@ -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 | diff --git a/test/invariant/fuzz/FuzzTest.t.sol b/test/invariant/fuzz/FuzzTest.t.sol index 570d4776..223d6a70 100644 --- a/test/invariant/fuzz/FuzzTest.t.sol +++ b/test/invariant/fuzz/FuzzTest.t.sol @@ -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"); @@ -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 {} } diff --git a/test/invariant/fuzz/Setup.t.sol b/test/invariant/fuzz/Setup.t.sol index 456c0e71..f5de1dc6 100644 --- a/test/invariant/fuzz/Setup.t.sol +++ b/test/invariant/fuzz/Setup.t.sol @@ -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"); @@ -47,8 +57,8 @@ contract Setup is Actors { vm.prank(protocolDeployer); allo.initialize(protocolDeployer, address(registry), payable(treasury), percentFee, baseFee, forwarder); - // Deploy base strategy - strategy_directAllocation = new DirectAllocationStrategy(address(allo)); + // Deploy strategies implementations + _initImplementations(address(allo)); // Deploy token token = ERC20(address(new FuzzERC20())); @@ -61,5 +71,99 @@ contract Setup is Actors { _addAnchorToActor(_ghost_actors[i], registry.getProfileById(_id).anchor, _id); } + + // 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]; + + 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)); + } } } diff --git a/test/invariant/fuzz/handlers/HandlerAllo.t.sol b/test/invariant/fuzz/handlers/HandlerAllo.t.sol index a49f3f47..dc229fb9 100644 --- a/test/invariant/fuzz/handlers/HandlerAllo.t.sol +++ b/test/invariant/fuzz/handlers/HandlerAllo.t.sol @@ -3,22 +3,31 @@ pragma solidity ^0.8.19; import {Setup} from "../Setup.t.sol"; import {IRegistry} from "contracts/core/Registry.sol"; -import {IAllo, 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]); // 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), @@ -27,7 +36,7 @@ contract HandlerAllo is Setup { allo.createPool, ( profile.id, - address(strategy_directAllocation), + _strategyImplementations[PoolStrategies(_seedPoolStrategy)], bytes(""), address(token), 0, @@ -36,12 +45,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(uint256 _idSeed, uint256 _metadataProtocol, string calldata _data) public { diff --git a/test/invariant/fuzz/handlers/HandlerStrategy.t.sol b/test/invariant/fuzz/handlers/HandlerStrategy.t.sol index f62be687..25a30011 100644 --- a/test/invariant/fuzz/handlers/HandlerStrategy.t.sol +++ b/test/invariant/fuzz/handlers/HandlerStrategy.t.sol @@ -17,7 +17,7 @@ contract HandlerStrategy is HandlerAllo { // Withdraw (bool succ,) = targetCall( - address(allo), 0, abi.encodeCall(strategy_directAllocation.withdraw, (_pool.token, _amount, _recipient)) + address(_pool.strategy), 0, abi.encodeCall(BaseStrategy.withdraw, (_pool.token, _amount, _recipient)) ); } } diff --git a/test/invariant/fuzz/helpers/Pools.t.sol b/test/invariant/fuzz/helpers/Pools.t.sol new file mode 100644 index 00000000..bafe028c --- /dev/null +++ b/test/invariant/fuzz/helpers/Pools.t.sol @@ -0,0 +1,70 @@ +// 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(PoolStrategies _strategy => address _implementation) internal _strategyImplementations; + + function _initImplementations(address _allo) internal { + _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 { + 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(PoolStrategies _strategy) internal returns (bool) { + for (uint256 i; i < ghost_poolIds.length; i++) { + if (_poolStrategy(ghost_poolIds[i]) == _strategy) return true; + } + + return false; + } +}