diff --git a/.github/workflows/forge-test.yml b/.github/workflows/forge-test.yml index e26237c24..53c066cec 100644 --- a/.github/workflows/forge-test.yml +++ b/.github/workflows/forge-test.yml @@ -27,6 +27,11 @@ jobs: uses: foundry-rs/foundry-toolchain@v1.0.9 with: version: nightly + + - name: Run Smock + run: | + bun smock + id: smock - name: Run Forge Format run: | diff --git a/contracts/strategies/BaseStrategy.sol b/contracts/strategies/BaseStrategy.sol index 638f5bc82..afe89aeb8 100644 --- a/contracts/strategies/BaseStrategy.sol +++ b/contracts/strategies/BaseStrategy.sol @@ -178,7 +178,7 @@ abstract contract BaseStrategy is IBaseStrategy { /// @notice Checks if the '_sender' is a pool manager. /// @dev Reverts if the '_sender' is not a pool manager. /// @param _sender The address to check if they are a pool manager - function _checkOnlyPoolManager(address _sender) internal view { + function _checkOnlyPoolManager(address _sender) internal view virtual { if (!allo.isPoolManager(poolId, _sender)) revert BaseStrategy_UNAUTHORIZED(); } diff --git a/contracts/strategies/extensions/allocate/AllocationExtension.sol b/contracts/strategies/extensions/allocate/AllocationExtension.sol new file mode 100644 index 000000000..685a4f9bc --- /dev/null +++ b/contracts/strategies/extensions/allocate/AllocationExtension.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {IAllocationExtension} from "contracts/strategies/extensions/allocate/IAllocationExtension.sol"; +import {BaseStrategy} from "contracts/strategies/BaseStrategy.sol"; + +abstract contract AllocationExtension is BaseStrategy, IAllocationExtension { + /// ================================ + /// ========== Storage ============= + /// ================================ + + /// @notice The start and end times for allocations + uint64 public allocationStartTime; + uint64 public allocationEndTime; + + /// @notice Defines if the strategy is sending Metadata struct in the data parameter + bool public isUsingAllocationMetadata; + + /// @notice token -> isAllowed + mapping(address => bool) public allowedTokens; + + /// =============================== + /// ========= Initialize ========== + /// =============================== + + /// @notice This initializes the Alocation Extension + /// @dev This function MUST be called by the 'initialize' function in the strategy. + /// @param _allowedTokens The allowed tokens + /// @param _allocationStartTime The start time for the allocation period + /// @param _allocationEndTime The end time for the allocation period + /// @param _isUsingAllocationMetadata Defines if the strategy is sending Metadata struct in the data parameter + function __AllocationExtension_init( + address[] memory _allowedTokens, + uint64 _allocationStartTime, + uint64 _allocationEndTime, + bool _isUsingAllocationMetadata + ) internal virtual { + if (_allowedTokens.length == 0) { + // all tokens + allowedTokens[address(0)] = true; + } else { + for (uint256 i; i < _allowedTokens.length; i++) { + allowedTokens[_allowedTokens[i]] = true; + } + } + + isUsingAllocationMetadata = _isUsingAllocationMetadata; + + _updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + } + + /// ==================================== + /// =========== Modifiers ============== + /// ==================================== + + /// @notice Modifier to check if allocation has ended + /// @dev Reverts if allocation has not ended + modifier onlyAfterAllocation() { + _checkOnlyAfterAllocation(); + _; + } + + /// @notice Modifier to check if allocation is active + /// @dev Reverts if allocation is not active + modifier onlyActiveAllocation() { + _checkOnlyActiveAllocation(); + _; + } + + /// @notice Modifier to check if allocation has started + /// @dev Reverts if allocation has started + modifier onlyBeforeAllocation() { + _checkBeforeAllocation(); + _; + } + + /// ==================================== + /// ============ Internal ============== + /// ==================================== + + /// @notice Checks if the allocator is valid + /// @param _allocator The allocator address + /// @return 'true' if the allocator is valid, otherwise 'false' + function _isValidAllocator(address _allocator) internal view virtual returns (bool); + + /// @notice Returns TRUE if the token is allowed + /// @param _token The token to check + function _isAllowedToken(address _token) internal view virtual returns (bool) { + // all tokens allowed + if (allowedTokens[address(0)]) return true; + + if (allowedTokens[_token]) return true; + + return false; + } + + /// @notice Sets the start and end dates for allocation. + /// @dev The 'msg.sender' must be a pool manager. + /// @param _allocationStartTime The start time for the allocation + /// @param _allocationEndTime The end time for the allocation + function _updateAllocationTimestamps(uint64 _allocationStartTime, uint64 _allocationEndTime) internal virtual { + if (_allocationStartTime > _allocationEndTime) revert INVALID_ALLOCATION_TIMESTAMPS(); + + allocationStartTime = _allocationStartTime; + allocationEndTime = _allocationEndTime; + + emit AllocationTimestampsUpdated(_allocationStartTime, _allocationEndTime, msg.sender); + } + + /// @dev Ensure the function is called before allocation start time + function _checkBeforeAllocation() internal virtual { + if (block.timestamp >= allocationStartTime) revert ALLOCATION_HAS_STARTED(); + } + + /// @dev Ensure the function is called during allocation times + function _checkOnlyActiveAllocation() internal virtual { + if (block.timestamp < allocationStartTime) revert ALLOCATION_NOT_ACTIVE(); + if (block.timestamp > allocationEndTime) revert ALLOCATION_NOT_ACTIVE(); + } + + /// @dev Ensure the function is called after allocation start time + function _checkOnlyAfterAllocation() internal virtual { + if (block.timestamp <= allocationEndTime) revert ALLOCATION_NOT_ENDED(); + } + + // ==================================== + // ==== External/Public Functions ===== + // ==================================== + + /// @notice Sets the start and end dates for allocation. + /// @dev The 'msg.sender' must be a pool manager. + /// @param _allocationStartTime The start time for the allocation + /// @param _allocationEndTime The end time for the allocation + function updateAllocationTimestamps(uint64 _allocationStartTime, uint64 _allocationEndTime) + external + virtual + onlyPoolManager(msg.sender) + { + _updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + } +} diff --git a/contracts/strategies/extensions/allocate/IAllocationExtension.sol b/contracts/strategies/extensions/allocate/IAllocationExtension.sol new file mode 100644 index 000000000..88f4d34f5 --- /dev/null +++ b/contracts/strategies/extensions/allocate/IAllocationExtension.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +interface IAllocationExtension { + /// @dev Error thrown when the allocation timestamps are invalid + error INVALID_ALLOCATION_TIMESTAMPS(); + + /// @dev Error thrown when trying to call the function when the allocation has started + error ALLOCATION_HAS_STARTED(); + + /// @dev Error thrown when trying to call the function when the allocation is not active + error ALLOCATION_NOT_ACTIVE(); + + /// @dev Error thrown when trying to call the function when the allocation has ended + error ALLOCATION_NOT_ENDED(); + + /// @notice Emitted when the allocation timestamps are updated + /// @param allocationStartTime The start time for the allocation period + /// @param allocationEndTime The end time for the allocation period + /// @param sender The sender of the transaction + event AllocationTimestampsUpdated(uint64 allocationStartTime, uint64 allocationEndTime, address sender); + + /// @notice The start time for the allocation period + function allocationStartTime() external view returns (uint64); + + /// @notice The end time for the allocation period + function allocationEndTime() external view returns (uint64); + + /// @notice Defines if the strategy is sending Metadata struct in the data parameter + function isUsingAllocationMetadata() external view returns (bool); + + /// @notice Returns TRUE if the token is allowed, FALSE otherwise + function allowedTokens(address _token) external view returns (bool); + + /// @notice Sets the start and end dates for allocation. + /// @param _allocationStartTime The start time for the allocation + /// @param _allocationEndTime The end time for the allocation + function updateAllocationTimestamps(uint64 _allocationStartTime, uint64 _allocationEndTime) external; +} diff --git a/foundry.toml b/foundry.toml index c231e728f..75d40cfea 100644 --- a/foundry.toml +++ b/foundry.toml @@ -39,8 +39,8 @@ allow_paths = ['node_modules'] [fmt] ignore = [ - 'contracts/strategies/_poc/qv-hackathon/SchemaResolver.sol', - 'contracts/strategies/_poc/direct-grants-simple/DirectGrantsSimpleStrategy.sol' + 'contracts/strategies/deprecated/_poc/qv-hackathon/SchemaResolver.sol', + 'contracts/strategies/deprecated/_poc/direct-grants-simple/DirectGrantsSimpleStrategy.sol' ] [rpc_endpoints] diff --git a/package.json b/package.json index d717cadf2..c0686b98f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "/////// deploy-test ///////": "echo 'deploy test scripts'", "create-profile": "npx hardhat run scripts/test/createProfile.ts --network", "create-pool": "npx hardhat run scripts/test/createPool.ts --network", - "smock": "smock-foundry --contracts contracts/core" + "smock": "smock-foundry --contracts contracts/core --contracts test/utils/mocks/" }, "devDependencies": { "@matterlabs/hardhat-zksync-deploy": "^1.2.1", diff --git a/test/unit/strategies/extensions/AllocationExtensionUnit.t.sol b/test/unit/strategies/extensions/AllocationExtensionUnit.t.sol new file mode 100644 index 000000000..78273365f --- /dev/null +++ b/test/unit/strategies/extensions/AllocationExtensionUnit.t.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import {Test} from "forge-std/Test.sol"; +import {MockMockAllocationExtension} from "test/smock/MockMockAllocationExtension.sol"; +import {IAllocationExtension} from "contracts/strategies/extensions/allocate/IAllocationExtension.sol"; + +contract AllocationExtension is Test { + MockMockAllocationExtension extension; + + event AllocationTimestampsUpdated(uint64 allocationStartTime, uint64 allocationEndTime, address sender); + + function setUp() public { + extension = new MockMockAllocationExtension(address(0)); + } + + function test___AllocationExtension_initWhenAllowedTokensArrayIsEmpty() external { + extension.call___AllocationExtension_init(new address[](0), 0, 0, false); + + // It should mark address zero as true + assertTrue(extension.allowedTokens(address(0))); + } + + function test___AllocationExtension_initWhenAllowedTokensArrayIsNotEmpty(address[] memory _tokens) external { + for (uint256 i; i < _tokens.length; i++) { + vm.assume(_tokens[i] != address(0)); + } + + extension.call___AllocationExtension_init(_tokens, 0, 0, false); + + // It should mark the tokens in the array as true + for (uint256 i; i < _tokens.length; i++) { + assertTrue(extension.allowedTokens(_tokens[i])); + } + } + + function test___AllocationExtension_initShouldSetIsUsingAllocationMetadata(bool _isUsingAllocationMetadata) + external + { + extension.call___AllocationExtension_init(new address[](0), 0, 0, _isUsingAllocationMetadata); + + // It should set isUsingAllocationMetadata + assertEq(extension.isUsingAllocationMetadata(), _isUsingAllocationMetadata); + } + + function test___AllocationExtension_initShouldCall_updateAllocationTimestamps( + uint64 _allocationStartTime, + uint64 _allocationEndTime + ) external { + extension.mock_call__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + + // It should call _updateAllocationTimestamps + extension.expectCall__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + + extension.call___AllocationExtension_init(new address[](0), _allocationStartTime, _allocationEndTime, false); + } + + function test__isAllowedTokenWhenAllTokensAllowed(address _tokenToCheck) external { + // Send empty array to allow all tokens + extension.call___AllocationExtension_init(new address[](0), 0, 0, false); + + // It should always return true + assertTrue(extension.call__isAllowedToken(_tokenToCheck)); + } + + function test__isAllowedTokenWhenTheTokenSentIsAllowed(address _tokenToCheck) external { + vm.assume(_tokenToCheck != address(0)); + + // Send array with only that token + address[] memory tokens = new address[](1); + tokens[0] = _tokenToCheck; + + extension.call___AllocationExtension_init(tokens, 0, 0, false); + + // It should return true + assertTrue(extension.call__isAllowedToken(_tokenToCheck)); + } + + function test__isAllowedTokenWhenTheTokenSentIsNotAllowed(address _tokenToCheck, address[] memory _allowedTokens) + external + { + vm.assume(_allowedTokens.length > 0); + for (uint256 i; i < _allowedTokens.length; i++) { + vm.assume(_allowedTokens[i] != address(0)); + vm.assume(_allowedTokens[i] != _tokenToCheck); + } + + extension.call___AllocationExtension_init(_allowedTokens, 0, 0, false); + + // It should return false + assertFalse(extension.call__isAllowedToken(_tokenToCheck)); + } + + function test__updateAllocationTimestampsRevertWhen_StartTimeIsBiggerThanEndTime( + uint64 _allocationStartTime, + uint64 _allocationEndTime + ) external { + vm.assume(_allocationStartTime > _allocationEndTime); + + // It should revert + vm.expectRevert(IAllocationExtension.INVALID_ALLOCATION_TIMESTAMPS.selector); + + extension.call__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + } + + function test__updateAllocationTimestampsWhenTimesAreCorrect(uint64 _allocationStartTime, uint64 _allocationEndTime) + external + { + vm.assume(_allocationStartTime < _allocationEndTime); + + // It should emit event + vm.expectEmit(); + emit AllocationTimestampsUpdated(_allocationStartTime, _allocationEndTime, address(this)); + + extension.call__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + + // It should update start and end time + assertEq(extension.allocationStartTime(), _allocationStartTime); + assertEq(extension.allocationEndTime(), _allocationEndTime); + } + + function test__checkBeforeAllocationRevertWhen_TimestampIsBiggerOrEqualThanStartTime( + uint64 _timestamp, + uint64 _allocationStartTime + ) external { + vm.assume(_timestamp >= _allocationStartTime); + + extension.call___AllocationExtension_init(new address[](0), _allocationStartTime, _allocationStartTime, false); + vm.warp(_timestamp); + + // It should revert + vm.expectRevert(IAllocationExtension.ALLOCATION_HAS_STARTED.selector); + + extension.call__checkBeforeAllocation(); + } + + function test__checkOnlyActiveAllocationRevertWhen_TimestampIsSmallerThanStartTime( + uint64 _timestamp, + uint64 _allocationStartTime + ) external { + vm.assume(_timestamp < _allocationStartTime); + + extension.call___AllocationExtension_init(new address[](0), _allocationStartTime, _allocationStartTime, false); + vm.warp(_timestamp); + + // It should revert + vm.expectRevert(IAllocationExtension.ALLOCATION_NOT_ACTIVE.selector); + + extension.call__checkOnlyActiveAllocation(); + } + + function test__checkOnlyActiveAllocationRevertWhen_TimestampIsBiggerThanEndTime( + uint64 _timestamp, + uint64 _allocationEndTime + ) external { + vm.assume(_timestamp > _allocationEndTime); + + extension.call___AllocationExtension_init(new address[](0), _allocationEndTime, _allocationEndTime, false); + vm.warp(_timestamp); + + // It should revert + vm.expectRevert(IAllocationExtension.ALLOCATION_NOT_ACTIVE.selector); + + extension.call__checkOnlyActiveAllocation(); + } + + function test__checkOnlyAfterAllocationRevertWhen_TimestampIsSmallerOrEqualThanEndTime( + uint64 _timestamp, + uint64 _allocationEndTime + ) external { + vm.assume(_timestamp <= _allocationEndTime); + + extension.call___AllocationExtension_init(new address[](0), _allocationEndTime, _allocationEndTime, false); + vm.warp(_timestamp); + + // It should revert + vm.expectRevert(IAllocationExtension.ALLOCATION_NOT_ENDED.selector); + + extension.call__checkOnlyAfterAllocation(); + } + + function test_UpdateAllocationTimestampsGivenSenderIsPoolManager( + uint64 _allocationStartTime, + uint64 _allocationEndTime + ) external { + extension.mock_call__checkOnlyPoolManager(address(this)); + extension.mock_call__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + + // It should call _updateAllocationTimestamps + extension.expectCall__updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + + extension.updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + } +} diff --git a/test/unit/strategies/extensions/AllocationExtensionUnit.tree b/test/unit/strategies/extensions/AllocationExtensionUnit.tree new file mode 100644 index 000000000..c240f2ea2 --- /dev/null +++ b/test/unit/strategies/extensions/AllocationExtensionUnit.tree @@ -0,0 +1,40 @@ +AllocationExtension::__AllocationExtension_init +├── When allowedTokens array is empty +│ └── It should mark address zero as true +├── When allowedTokens array is not empty +│ └── It should mark the tokens in the array as true +├── It should set isUsingAllocationMetadata +└── It should call _updateAllocationTimestamps + +AllocationExtension::_isAllowedToken +├── When all tokens allowed +│ └── It should always return true +├── When the token sent is allowed +│ └── It should return true +└── When the token sent is not allowed + └── It should return false + +AllocationExtension::_updateAllocationTimestamps +├── When start time is bigger than end time +│ └── It should revert +└── When times are correct + ├── It should update start and end time + └── It should emit event + +AllocationExtension::_checkBeforeAllocation +└── When timestamp is bigger or equal than start time + └── It should revert + +AllocationExtension::_checkOnlyActiveAllocation +├── When timestamp is smaller than start time +│ └── It should revert +└── When timestamp is bigger than end time + └── It should revert + +AllocationExtension::_checkOnlyAfterAllocation +└── When timestamp is smaller or equal than end time + └── It should revert + +AllocationExtension::updateAllocationTimestamps +└── Given sender is pool manager + └── It should call _updateAllocationTimestamps \ No newline at end of file diff --git a/test/utils/mocks/MockAllocationExtension.sol b/test/utils/mocks/MockAllocationExtension.sol new file mode 100644 index 000000000..e03218fc4 --- /dev/null +++ b/test/utils/mocks/MockAllocationExtension.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.19; + +import {AllocationExtension} from "contracts/strategies/extensions/allocate/AllocationExtension.sol"; +import {BaseStrategy} from "contracts/strategies/BaseStrategy.sol"; + +/// @dev This mock allows smock to override the functions of AllocationExtension abstract contract +contract MockAllocationExtension is BaseStrategy, AllocationExtension { + constructor(address _allo) BaseStrategy(_allo) {} + + function initialize(uint256 _poolId, bytes memory _data) external override { + __BaseStrategy_init(_poolId); + ( + address[] memory _allowedTokens, + uint64 _allocationStartTime, + uint64 _allocationEndTime, + bool _isUsingAllocationMetadata + ) = abi.decode(_data, (address[], uint64, uint64, bool)); + __AllocationExtension_init(_allowedTokens, _allocationStartTime, _allocationEndTime, _isUsingAllocationMetadata); + } + + function __AllocationExtension_init( + address[] memory _allowedTokens, + uint64 _allocationStartTime, + uint64 _allocationEndTime, + bool _isUsingAllocationMetadata + ) internal virtual override { + super.__AllocationExtension_init( + _allowedTokens, _allocationStartTime, _allocationEndTime, _isUsingAllocationMetadata + ); + } + + function _isAllowedToken(address _token) internal view virtual override returns (bool) { + return super._isAllowedToken(_token); + } + + function _updateAllocationTimestamps(uint64 _allocationStartTime, uint64 _allocationEndTime) + internal + virtual + override + { + super._updateAllocationTimestamps(_allocationStartTime, _allocationEndTime); + } + + function _checkBeforeAllocation() internal virtual override { + super._checkBeforeAllocation(); + } + + function _checkOnlyActiveAllocation() internal virtual override { + super._checkOnlyActiveAllocation(); + } + + function _checkOnlyAfterAllocation() internal virtual override { + super._checkOnlyAfterAllocation(); + } + + function _checkOnlyPoolManager(address _sender) internal view virtual override { + super._checkOnlyPoolManager(_sender); + } + + function _allocate(address[] memory, uint256[] memory, bytes memory, address) internal override {} + + function _distribute(address[] memory, bytes memory, address) internal override {} + + function _isValidAllocator(address) internal view override returns (bool) {} + + function _register(address[] memory, bytes memory, address) internal override returns (address[] memory) {} +}