From e7c00c1835c2cf757612e248a497fe51c088c465 Mon Sep 17 00:00:00 2001 From: Miguel de Elias Date: Sat, 25 May 2024 01:04:02 -0300 Subject: [PATCH] chore: add horizon unit tests --- packages/horizon/test/GraphBase.t.sol | 12 +- .../horizon/test/escrow/GraphEscrow.t.sol | 2 +- packages/horizon/test/escrow/collect.t.sol | 8 +- .../horizon/test/payments/GraphPayments.t.sol | 8 +- ...aking.t.sol => HorizonStakingShared.t.sol} | 30 +++- .../horizon/test/staking/HorizonStaking.t.sol | 86 ++++++++++++ .../test/staking/delegation/delegate.t.sol | 42 ++++++ .../test/staking/delegation/undelegate.t.sol | 76 ++++++++++ .../test/staking/delegation/withdraw.t.sol | 72 ++++++++++ .../test/staking/operator/operator.t.sol | 14 ++ .../test/staking/provision/deprovision.t.sol | 98 +++++++++++++ .../test/staking/provision/provision.t.sol | 93 +++++++++++++ .../test/staking/provision/reprovision.t.sol | 131 ++++++++++++++++++ .../horizon/test/staking/slash/slash.t.sol | 87 ++++++++++++ .../horizon/test/staking/stake/stake.t.sol | 29 ++++ .../horizon/test/staking/stake/unstake.t.sol | 74 ++++++++++ packages/horizon/test/staking/thaw/thaw.t.sol | 118 ++++++++++++++++ packages/horizon/test/utils/Constants.sol | 6 + packages/horizon/test/utils/Users.sol | 1 + 19 files changed, 971 insertions(+), 16 deletions(-) rename packages/horizon/test/shared/horizon-staking/{HorizonStaking.t.sol => HorizonStakingShared.t.sol} (64%) create mode 100644 packages/horizon/test/staking/HorizonStaking.t.sol create mode 100644 packages/horizon/test/staking/delegation/delegate.t.sol create mode 100644 packages/horizon/test/staking/delegation/undelegate.t.sol create mode 100644 packages/horizon/test/staking/delegation/withdraw.t.sol create mode 100644 packages/horizon/test/staking/operator/operator.t.sol create mode 100644 packages/horizon/test/staking/provision/deprovision.t.sol create mode 100644 packages/horizon/test/staking/provision/provision.t.sol create mode 100644 packages/horizon/test/staking/provision/reprovision.t.sol create mode 100644 packages/horizon/test/staking/slash/slash.t.sol create mode 100644 packages/horizon/test/staking/stake/stake.t.sol create mode 100644 packages/horizon/test/staking/stake/unstake.t.sol create mode 100644 packages/horizon/test/staking/thaw/thaw.t.sol diff --git a/packages/horizon/test/GraphBase.t.sol b/packages/horizon/test/GraphBase.t.sol index f657d8b3e..27835c54a 100644 --- a/packages/horizon/test/GraphBase.t.sol +++ b/packages/horizon/test/GraphBase.t.sol @@ -54,11 +54,13 @@ abstract contract GraphBaseTest is Test, Constants { indexer: createUser("indexer"), operator: createUser("operator"), gateway: createUser("gateway"), - verifier: createUser("verifier") + verifier: createUser("verifier"), + delegator: createUser("delegator") }); // Deploy protocol contracts deployProtocolContracts(); + setupProtocol(); unpauseProtocol(); // Label contracts @@ -147,11 +149,15 @@ abstract contract GraphBaseTest is Test, Constants { proxyAdmin.upgrade(stakingProxy, address(stakingBase)); proxyAdmin.acceptProxy(stakingBase, stakingProxy); staking = IHorizonStaking(address(stakingProxy)); - vm.stopPrank(); + } + + function setupProtocol() private { + vm.startPrank(users.governor); + staking.setMaxThawingPeriod(MAX_THAWING_PERIOD); } function unpauseProtocol() private { - vm.prank(users.governor); + vm.startPrank(users.governor); controller.setPaused(false); } diff --git a/packages/horizon/test/escrow/GraphEscrow.t.sol b/packages/horizon/test/escrow/GraphEscrow.t.sol index a98eb3322..70c96466f 100644 --- a/packages/horizon/test/escrow/GraphEscrow.t.sol +++ b/packages/horizon/test/escrow/GraphEscrow.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import "forge-std/Test.sol"; -import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStaking.t.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; contract GraphEscrowTest is HorizonStakingSharedTest { diff --git a/packages/horizon/test/escrow/collect.t.sol b/packages/horizon/test/escrow/collect.t.sol index d7557353a..04d71252d 100644 --- a/packages/horizon/test/escrow/collect.t.sol +++ b/packages/horizon/test/escrow/collect.t.sol @@ -8,7 +8,10 @@ import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; contract GraphEscrowCollectTest is GraphEscrowTest { - function testCollect_Tokens(uint256 amount, uint256 tokensDataService) public useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + function testCollect_Tokens( + uint256 amount, + uint256 tokensDataService + ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { uint256 tokensProtocol = amount * protocolPaymentCut / MAX_PPM; uint256 tokensDelegatoion = amount * delegationFeeCut / MAX_PPM; vm.assume(tokensDataService < amount - tokensProtocol - tokensDelegatoion); @@ -16,10 +19,9 @@ contract GraphEscrowCollectTest is GraphEscrowTest { vm.startPrank(users.gateway); escrow.approveCollector(users.verifier, amount); _depositTokens(amount); - vm.stopPrank(); uint256 indexerPreviousBalance = token.balanceOf(users.indexer); - vm.prank(users.verifier); + vm.startPrank(users.verifier); escrow.collect(IGraphPayments.PaymentTypes.QueryFee, users.gateway, users.indexer, amount, subgraphDataServiceAddress, tokensDataService); uint256 indexerBalance = token.balanceOf(users.indexer); diff --git a/packages/horizon/test/payments/GraphPayments.t.sol b/packages/horizon/test/payments/GraphPayments.t.sol index e4fa18b97..4449bbdcc 100644 --- a/packages/horizon/test/payments/GraphPayments.t.sol +++ b/packages/horizon/test/payments/GraphPayments.t.sol @@ -5,14 +5,14 @@ import "forge-std/Test.sol"; import { IGraphPayments } from "../../contracts/interfaces/IGraphPayments.sol"; -import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStaking.t.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; contract GraphPaymentsTest is HorizonStakingSharedTest { function testCollect( uint256 amount, uint256 tokensDataService - ) public useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { + ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { uint256 tokensProtocol = amount * protocolPaymentCut / MAX_PPM; uint256 tokensDelegatoion = amount * delegationFeeCut / MAX_PPM; vm.assume(tokensDataService < amount - tokensProtocol - tokensDelegatoion); @@ -42,7 +42,11 @@ contract GraphPaymentsTest is HorizonStakingSharedTest { function testCollect_RevertWhen_InsufficientAmount( uint256 amount, uint256 tokensDataService +<<<<<<< HEAD ) public useProvision(amount, 0, 0) useDelegationFeeCut(IGraphPayments.PaymentTypes.QueryFee, delegationFeeCut) { +======= + ) public useIndexer useProvision(amount, 0, 0) useDelegationFeeCut(0, delegationFeeCut) { +>>>>>>> 9fa803b7 (chore: staking tests refactor) vm.assume(tokensDataService <= 10_000_000_000 ether); vm.assume(tokensDataService > amount); diff --git a/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol similarity index 64% rename from packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol rename to packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol index 7fd428e40..2ee0d5764 100644 --- a/packages/horizon/test/shared/horizon-staking/HorizonStaking.t.sol +++ b/packages/horizon/test/shared/horizon-staking/HorizonStakingShared.t.sol @@ -8,10 +8,24 @@ import { IGraphPayments } from "../../../contracts/interfaces/IGraphPayments.sol abstract contract HorizonStakingSharedTest is GraphBaseTest { + modifier useIndexer() { + vm.startPrank(users.indexer); + _; + vm.stopPrank(); + } + + modifier assumeProvisionTokens(uint256 tokens) { + vm.assume(tokens > MIN_PROVISION_SIZE); + vm.assume(tokens <= 10_000_000_000 ether); + _; + } + modifier useProvision(uint256 tokens, uint32 maxVerifierCut, uint64 thawingPeriod) { vm.assume(tokens <= 10_000_000_000 ether); - vm.assume(tokens > 1e18); - _createProvision(tokens, maxVerifierCut, thawingPeriod); + vm.assume(tokens > MIN_PROVISION_SIZE); + vm.assume(maxVerifierCut <= MAX_MAX_VERIFIER_CUT); + vm.assume(thawingPeriod <= MAX_THAWING_PERIOD); + _createProvision(subgraphDataServiceAddress, tokens, maxVerifierCut, thawingPeriod); _; } @@ -28,22 +42,24 @@ abstract contract HorizonStakingSharedTest is GraphBaseTest { /* Helpers */ - function _createProvision(uint256 tokens, uint32 maxVerifierCut, uint64 thawingPeriod) internal { - vm.startPrank(users.indexer); + function _createProvision( + address dataServiceAddress, + uint256 tokens, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) internal { token.approve(address(staking), tokens); staking.stakeTo(users.indexer, tokens); staking.provision( users.indexer, - subgraphDataServiceAddress, + dataServiceAddress, tokens, maxVerifierCut, thawingPeriod ); - vm.stopPrank(); } function _setDelegationFeeCut(IGraphPayments.PaymentTypes paymentType, uint256 cut) internal { - vm.prank(users.indexer); staking.setDelegationFeeCut(users.indexer, subgraphDataServiceAddress, paymentType, cut); } } diff --git a/packages/horizon/test/staking/HorizonStaking.t.sol b/packages/horizon/test/staking/HorizonStaking.t.sol new file mode 100644 index 000000000..431ce0296 --- /dev/null +++ b/packages/horizon/test/staking/HorizonStaking.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IHorizonStakingTypes } from "../../contracts/interfaces/IHorizonStakingTypes.sol"; +import { HorizonStakingSharedTest } from "../shared/horizon-staking/HorizonStakingShared.t.sol"; + +contract HorizonStakingTest is HorizonStakingSharedTest, IHorizonStakingTypes { + + modifier useOperator() { + vm.startPrank(users.indexer); + staking.setOperator(users.operator, subgraphDataServiceAddress, true); + vm.startPrank(users.operator); + _; + vm.stopPrank(); + } + + modifier useStake(uint256 amount) { + vm.assume(amount > MIN_PROVISION_SIZE); + approve(address(staking), amount); + staking.stake(amount); + _; + } + + modifier useStakeTo(address to, uint256 amount) { + vm.assume(amount > MIN_PROVISION_SIZE); + _stakeTo(to, amount); + _; + } + + modifier useThawRequest(uint256 thawAmount) { + vm.assume(thawAmount > 0); + _createThawRequest(thawAmount); + _; + } + + modifier useThawAndDeprovision(uint256 amount, uint64 thawingPeriod) { + vm.assume(amount > 0); + _createThawRequest(amount); + skip(thawingPeriod + 1); + _deprovision(amount); + _; + } + + modifier useDelegation(uint256 delegationAmount) { + address msgSender; + (, msgSender,) = vm.readCallers(); + vm.assume(delegationAmount > MIN_DELEGATION); + vm.assume(delegationAmount <= 10_000_000_000 ether); + vm.startPrank(users.delegator); + _delegate(delegationAmount); + vm.startPrank(msgSender); + _; + } + + function _stakeTo(address to, uint256 amount) internal { + approve(address(staking), amount); + staking.stakeTo(to, amount); + } + + function _createThawRequest(uint256 thawAmount) internal returns (bytes32) { + return staking.thaw(users.indexer, subgraphDataServiceAddress, thawAmount); + } + + function _deprovision(uint256 amount) internal { + staking.deprovision(users.indexer, subgraphDataServiceAddress, amount); + } + + function _delegate(uint256 amount) internal { + token.approve(address(staking), amount); + staking.delegate(users.indexer, subgraphDataServiceAddress, amount, 0); + } + + function _getDelegation() internal view returns (Delegation memory) { + return staking.getDelegation(users.delegator, users.indexer, subgraphDataServiceAddress); + } + + function _undelegate(uint256 shares) internal { + staking.undelegate(users.indexer, subgraphDataServiceAddress, shares); + } + + function _getDelegationPool() internal view returns (DelegationPool memory) { + return staking.getDelegationPool(users.indexer, subgraphDataServiceAddress); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/delegation/delegate.t.sol b/packages/horizon/test/staking/delegation/delegate.t.sol new file mode 100644 index 000000000..279c2f7f7 --- /dev/null +++ b/packages/horizon/test/staking/delegation/delegate.t.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingDelegateTest is HorizonStakingTest { + + function testDelegate_Tokens( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + uint256 delegatedTokens = staking.getDelegatedTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(delegatedTokens, delegationAmount); + } + + function testDelegate_RevertWhen_ZeroTokens( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(users.delegator); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, 0, 0); + } + + function testDelegate_RevertWhen_BelowMinimum( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) { + vm.startPrank(users.delegator); + delegationAmount = bound(delegationAmount, 1, MIN_DELEGATION - 1); + token.approve(address(staking), delegationAmount); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + MIN_DELEGATION, + delegationAmount + ); + vm.expectRevert(expectedError); + staking.delegate(users.indexer, subgraphDataServiceAddress, delegationAmount, 0); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/delegation/undelegate.t.sol b/packages/horizon/test/staking/delegation/undelegate.t.sol new file mode 100644 index 000000000..73c7a913e --- /dev/null +++ b/packages/horizon/test/staking/delegation/undelegate.t.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingUndelegateTest is HorizonStakingTest { + + function testUndelegate_Tokens( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + // TODO: maybe create a changePrank + vm.stopPrank(); + vm.startPrank(users.delegator); + Delegation memory delegation = _getDelegation(); + _undelegate(delegation.shares); + + Delegation memory thawingDelegation = _getDelegation(); + ThawRequest memory thawRequest = staking.getThawRequest(thawingDelegation.lastThawRequestId); + + assertEq(thawRequest.shares, delegation.shares); + } + + function testUndelegate_RevertWhen_ZeroTokens( + uint256 amount, + uint256 delegationAmount + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + // TODO: maybe create a changePrank + vm.stopPrank(); + vm.startPrank(users.delegator); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + _undelegate(0); + } + + function testUndelegate_RevertWhen_OverUndelegation( + uint256 amount, + uint256 delegationAmount, + uint256 overDelegationShares + ) public useIndexer useProvision(amount, 0, 0) useDelegation(delegationAmount) { + // TODO: maybe create a changePrank + vm.stopPrank(); + vm.startPrank(users.delegator); + Delegation memory delegation = _getDelegation(); + vm.assume(overDelegationShares > delegation.shares); + + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + overDelegationShares, + delegation.shares + ); + vm.expectRevert(expectedError); + _undelegate(overDelegationShares); + } + + function testUndelegate_RevertWhen_UndelegateLeavesInsufficientTokens( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, 0) + useDelegation(delegationAmount) + { + // TODO: maybe create a changePrank + vm.stopPrank(); + vm.startPrank(users.delegator); + uint256 minShares = delegationAmount - MIN_DELEGATION + 1; + withdrawShares = bound(withdrawShares, minShares, delegationAmount - 1); + + vm.expectRevert("!minimum-delegation"); + _undelegate(withdrawShares); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/delegation/withdraw.t.sol b/packages/horizon/test/staking/delegation/withdraw.t.sol new file mode 100644 index 000000000..593a2dae5 --- /dev/null +++ b/packages/horizon/test/staking/delegation/withdraw.t.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingWithdrawDelegationTest is HorizonStakingTest { + + modifier useUndelegate(uint256 shares) { + vm.stopPrank(); + vm.startPrank(users.delegator); + Delegation memory delegation = _getDelegation(); + shares = bound(shares, 1, delegation.shares); + + if (shares != delegation.shares) { + DelegationPool memory pool = _getDelegationPool(); + uint256 tokens = (shares * (pool.tokens - pool.tokensThawing)) / pool.shares; + uint256 newTokensThawing = pool.tokensThawing + tokens; + uint256 remainingTokens = (delegation.shares * (pool.tokens - newTokensThawing)) / pool.shares; + vm.assume(remainingTokens >= MIN_DELEGATION); + } + + _undelegate(shares); + _; + } + + function _withdrawDelegated() private { + staking.withdrawDelegated(users.indexer, subgraphDataServiceAddress, address(0x0), 0); + } + + function _expectedTokensFromThawRequest(ThawRequest memory thawRequest) private view returns (uint256) { + DelegationPool memory pool = _getDelegationPool(); + return (thawRequest.shares * pool.tokensThawing) / pool.sharesThawing; + } + + function testWithdrawDelegation_Tokens( + uint256 delegationAmount, + uint256 withdrawShares + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + useUndelegate(withdrawShares) + { + Delegation memory thawingDelegation = _getDelegation(); + ThawRequest memory thawRequest = staking.getThawRequest(thawingDelegation.lastThawRequestId); + + skip(thawRequest.thawingUntil + 1); + + uint256 previousBalance = token.balanceOf(users.delegator); + uint256 expectedTokens = _expectedTokensFromThawRequest(thawRequest); + _withdrawDelegated(); + + uint256 newBalance = token.balanceOf(users.delegator); + assertEq(newBalance - previousBalance, expectedTokens); + } + + function testWithdrawDelegation_RevertWhen_NotThawing( + uint256 delegationAmount + ) + public + useIndexer + useProvision(10_000_000 ether, 0, MAX_THAWING_PERIOD) + useDelegation(delegationAmount) + { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNotEnoughThawedTokens()"); + vm.expectRevert(expectedError); + _withdrawDelegated(); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/operator/operator.t.sol b/packages/horizon/test/staking/operator/operator.t.sol new file mode 100644 index 000000000..6fffe0cf3 --- /dev/null +++ b/packages/horizon/test/staking/operator/operator.t.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingOperatorTest is HorizonStakingTest { + + function testOperator_SetOperator() public useIndexer { + staking.setOperator(users.operator, subgraphDataServiceAddress, true); + assertTrue(staking.isAuthorized(users.operator, users.indexer, subgraphDataServiceAddress)); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/provision/deprovision.t.sol b/packages/horizon/test/staking/provision/deprovision.t.sol new file mode 100644 index 000000000..3afb3a7b1 --- /dev/null +++ b/packages/horizon/test/staking/provision/deprovision.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingDeprovisionTest is HorizonStakingTest { + + function testDeprovision_Tokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useThawRequest(amount) { + skip(thawingPeriod + 1); + + _deprovision(amount); + uint256 idleStake = staking.getIdleStake(users.indexer); + assertEq(idleStake, amount); + } + + function testDeprovision_OperatorMovingTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useOperator useProvision(amount, maxVerifierCut, thawingPeriod) useThawRequest(amount) { + skip(thawingPeriod + 1); + + _deprovision(amount); + uint256 idleStake = staking.getIdleStake(users.indexer); + assertEq(idleStake, amount); + } + + function testDeprovision_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useThawRequest(amount) { + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.operator, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + _deprovision(amount); + } + + function testDeprovision_RevertWhen_ZeroTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useThawRequest(amount) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + _deprovision(0); + } + + function testDeprovision_RevertWhen_NoThawingTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNotEnoughThawedTokens()"); + vm.expectRevert(expectedError); + _deprovision(amount); + } + + function testDeprovision_RevertWhen_StillThawing( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) useThawRequest(amount) { + vm.assume(thawingPeriod > 0); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingStillThawing(uint256,uint256)", + block.timestamp, + block.timestamp + thawingPeriod + ); + vm.expectRevert(expectedError); + _deprovision(amount); + } + + function testDeprovision_RevertWhen_NotEnoughThawedTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod, + uint256 deprovisionAmount + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.assume(deprovisionAmount > amount); + skip(thawingPeriod + 1); + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNotEnoughThawedTokens()"); + vm.expectRevert(expectedError); + _deprovision(deprovisionAmount); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/provision/provision.t.sol b/packages/horizon/test/staking/provision/provision.t.sol new file mode 100644 index 000000000..ee9591205 --- /dev/null +++ b/packages/horizon/test/staking/provision/provision.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingProvisionTest is HorizonStakingTest { + + function testProvision_Create( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, amount); + } + + function testProvision_RevertWhen_InsufficientTokens(uint256 amount) public useIndexer useStake(1000 ether) { + vm.assume(amount < MIN_PROVISION_SIZE); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInsufficientTokens(uint256,uint256)", MIN_PROVISION_SIZE, amount); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, 0); + } + + function testProvision_RevertWhen_MaxVerifierCutTooHigh( + uint256 amount, + uint32 maxVerifierCut + ) public useIndexer useStake(amount) { + vm.assume(amount > MIN_PROVISION_SIZE); + vm.assume(maxVerifierCut > MAX_MAX_VERIFIER_CUT); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingMaxVerifierCutExceeded(uint32,uint32)", MAX_MAX_VERIFIER_CUT, maxVerifierCut); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, maxVerifierCut, 0); + } + + function testProvision_RevertWhen_ThawingPeriodTooHigh( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useStake(amount) { + vm.assume(amount > MIN_PROVISION_SIZE); + vm.assume(thawingPeriod > MAX_THAWING_PERIOD); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingMaxThawingPeriodExceeded(uint64,uint64)", MAX_THAWING_PERIOD, thawingPeriod); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, 0, thawingPeriod); + } + + function testProvision_RevertWhen_ThereIsNoIdleStake( + uint256 amount, + uint256 provisionTokens + ) public useIndexer useStake(amount) { + vm.assume(amount > MIN_PROVISION_SIZE); + vm.assume(provisionTokens > amount); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInsufficientCapacity()"); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, provisionTokens, 0, 0); + } + + function testProvision_OperatorAddTokensToProvision( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod, + uint256 tokensToAdd + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + tokensToAdd = bound(tokensToAdd, 1, type(uint256).max - amount); + // Set operator + staking.setOperator(users.operator, subgraphDataServiceAddress, true); + + // Add more tokens to the provision + vm.startPrank(users.operator); + _stakeTo(users.indexer, tokensToAdd); + staking.addToProvision(users.indexer, subgraphDataServiceAddress, tokensToAdd); + + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, amount + tokensToAdd); + } + + function testProvision_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, maxVerifierCut, thawingPeriod) { + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.operator, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + staking.provision(users.indexer, subgraphDataServiceAddress, amount, maxVerifierCut, thawingPeriod); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/provision/reprovision.t.sol b/packages/horizon/test/staking/provision/reprovision.t.sol new file mode 100644 index 000000000..2c10e9060 --- /dev/null +++ b/packages/horizon/test/staking/provision/reprovision.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingReprovisionTest is HorizonStakingTest { + + address private newDataService = makeAddr("newDataService"); + + function _reprovision(uint256 amount) private { + staking.reprovision(users.indexer, subgraphDataServiceAddress, newDataService, amount); + } + + function testReprovision_MovingTokens( + uint64 thawingPeriod, + uint256 provisionAmount + ) + public + useIndexer + useProvision(provisionAmount, 0, thawingPeriod) + useThawRequest(provisionAmount) + { + skip(thawingPeriod + 1); + + _createProvision(newDataService, MIN_PROVISION_SIZE, 0, thawingPeriod); + _reprovision(provisionAmount); + uint256 idleStake = staking.getIdleStake(users.indexer); + assertEq(idleStake, 0 ether); + + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, newDataService); + assertEq(provisionTokens, provisionAmount + MIN_PROVISION_SIZE); + } + + function testReprovision_OperatorMovingTokens( + uint64 thawingPeriod, + uint256 provisionAmount + ) + public + useOperator + useProvision(provisionAmount, 0, thawingPeriod) + useThawRequest(provisionAmount) + { + skip(thawingPeriod + 1); + + // Switch to indexer to set operator for new data service + vm.startPrank(users.indexer); + staking.setOperator(users.operator, newDataService, true); + + // Switch back to operator + vm.startPrank(users.operator); + _createProvision(newDataService, MIN_PROVISION_SIZE, 0, thawingPeriod); + _reprovision(provisionAmount); + uint256 idleStake = staking.getIdleStake(users.indexer); + assertEq(idleStake, 0 ether); + + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, newDataService); + assertEq(provisionTokens, provisionAmount + MIN_PROVISION_SIZE); + } + + function testReprovision_RevertWhen_OperatorNotAuthorizedForNewDataService( + uint256 provisionAmount + ) + public + useOperator + useProvision(provisionAmount, 0, 0) + useThawRequest(provisionAmount) + { + // Switch to indexer to create new provision + vm.startPrank(users.indexer); + _createProvision(newDataService, MIN_PROVISION_SIZE, 0, 0); + + // Switch back to operator + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.operator, + users.indexer, + newDataService + ); + vm.expectRevert(expectedError); + _reprovision(provisionAmount); + } + + function testReprovision_RevertWhen_NoThawingTokens( + uint256 amount + ) public useIndexer useProvision(amount, 0, 0) { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNotEnoughThawedTokens()"); + vm.expectRevert(expectedError); + _reprovision(amount); + } + + function testReprovision_RevertWhen_StillThawing( + uint64 thawingPeriod, + uint256 provisionAmount + ) + public + useIndexer + useProvision(provisionAmount, 0, thawingPeriod) + useThawRequest(provisionAmount) + { + vm.assume(thawingPeriod > 0); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingStillThawing(uint256,uint256)", + block.timestamp, + block.timestamp + thawingPeriod + ); + vm.expectRevert(expectedError); + _reprovision(provisionAmount); + } + + function testReprovision_RevertWhen_NotEnoughThawedTokens( + uint64 thawingPeriod, + uint256 provisionAmount, + uint256 newProvisionAmount + ) + public + useIndexer + useProvision(provisionAmount, 0, thawingPeriod) + useThawRequest(provisionAmount) + { + newProvisionAmount = bound(newProvisionAmount, provisionAmount + 1, type(uint256).max - provisionAmount); + skip(thawingPeriod + 1); + + _createProvision(newDataService, newProvisionAmount, 0, thawingPeriod); + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingNotEnoughThawedTokens()"); + vm.expectRevert(expectedError); + _reprovision(newProvisionAmount); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/slash/slash.t.sol b/packages/horizon/test/staking/slash/slash.t.sol new file mode 100644 index 000000000..2b3c8c5b1 --- /dev/null +++ b/packages/horizon/test/staking/slash/slash.t.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingSlashTest is HorizonStakingTest { + + modifier useDelegationSlashingDisabled() { + address msgSender; + (, msgSender,) = vm.readCallers(); + vm.startPrank(users.governor); + staking.setDelegationSlashingEnabled(false); + vm.startPrank(msgSender); + _; + } + + function _slash(uint256 amount, uint256 verifierCutAmount) private { + staking.slash(users.indexer, amount, verifierCutAmount, subgraphDataServiceAddress); + } + + function testSlash_Tokens( + uint256 amount, + uint32 maxVerifierCut, + uint256 slashAmount, + uint256 verifierCutAmount + ) public useIndexer useProvision(amount, maxVerifierCut, 0) { + verifierCutAmount = bound(verifierCutAmount, 0, maxVerifierCut); + + // TODO: when slashing for low amounts there's an arithmetic underflow + slashAmount = bound(slashAmount, MIN_PROVISION_SIZE, amount); + + vm.startPrank(subgraphDataServiceAddress); + _slash(slashAmount, verifierCutAmount); + + uint256 provisionTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + assertEq(provisionTokens, amount - slashAmount); + + uint256 verifierTokens = token.balanceOf(subgraphDataServiceAddress); + assertEq(verifierTokens, verifierCutAmount); + } + + // TODO: Should be re-enabled when slashin is fixed to count for delegated tokens + // function testSlash_DelegationDisabled_SlashingOverProvisionTokens( + // uint256 amount, + // uint256 slashAmount, + // uint256 verifierCutAmount, + // uint256 delegationAmount + // ) public useIndexer useProvision(amount, 100000, 0) useDelegationSlashingDisabled { + // vm.assume(slashAmount > amount); + // vm.assume(delegationAmount > 0); + // uint32 delegationRatio = 5; + + // vm.stopPrank(); + // vm.startPrank(users.delegator); + // _delegate(delegationAmount); + + // vm.startPrank(subgraphDataServiceAddress); + // _slash(slashAmount, verifierCutAmount); + + // uint256 provisionProviderTokens = staking.getProviderTokensAvailable(users.indexer, subgraphDataServiceAddress); + // assertEq(provisionProviderTokens, 0 ether); + + // uint256 verifierTokens = token.balanceOf(address(subgraphDataServiceAddress)); + // assertEq(verifierTokens, verifierCutAmount); + + // uint256 delegatedTokens = staking.getTokensAvailable(users.indexer, subgraphDataServiceAddress, delegationRatio); + // assertEq(delegatedTokens, delegationAmount); + // } + + function testSlash_RevertWhen_NoProvision( + uint256 amount, + uint256 slashAmount + ) public useIndexer useStake(amount) { + // TODO: when slashing for low amounts there's an arithmetic underflow + slashAmount = bound(slashAmount, MIN_PROVISION_SIZE, amount); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + slashAmount, + 0 ether + ); + vm.expectRevert(expectedError); + vm.startPrank(subgraphDataServiceAddress); + _slash(slashAmount, 0); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/stake/stake.t.sol b/packages/horizon/test/staking/stake/stake.t.sol new file mode 100644 index 000000000..bcdec35c9 --- /dev/null +++ b/packages/horizon/test/staking/stake/stake.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingStakeTest is HorizonStakingTest { + + function testStake_Tokens(uint256 amount) public useIndexer useStake(amount) { + assertTrue(staking.getStake(address(users.indexer)) == amount); + } + + function testStake_RevertWhen_ZeroTokens() public useIndexer { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.stake(0); + } + + function testStakeTo_Tokens(uint256 amount) public useOperator useStakeTo(users.indexer, amount) { + assertTrue(staking.getStake(address(users.indexer)) == amount); + } + + function testStakeTo_RevertWhen_ZeroTokens() public useOperator { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.stakeTo(users.indexer, 0); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/stake/unstake.t.sol b/packages/horizon/test/staking/stake/unstake.t.sol new file mode 100644 index 000000000..6285ef9c0 --- /dev/null +++ b/packages/horizon/test/staking/stake/unstake.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingUnstakeTest is HorizonStakingTest { + + function testUnstake_Tokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) + public + useIndexer + useProvision(amount, maxVerifierCut, thawingPeriod) + useThawAndDeprovision(amount, thawingPeriod) + { + uint256 previousIndexerTokens = token.balanceOf(users.indexer); + staking.unstake(amount); + uint256 idleStake = staking.getIdleStake(users.indexer); + assertEq(idleStake, 0 ether); + + uint256 newIndexerBalance = token.balanceOf(users.indexer); + assertEq(newIndexerBalance - previousIndexerTokens, amount); + } + + function testUnstake_RevertWhen_ZeroTokens( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) + public + useIndexer + useProvision(amount, maxVerifierCut, thawingPeriod) + useThawAndDeprovision(amount, thawingPeriod) + { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + staking.unstake(0); + } + + function testUnstake_RevertWhen_NoIdleStake( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) + public + useIndexer + useProvision(amount, maxVerifierCut, thawingPeriod) + { + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInsufficientCapacity()"); + vm.expectRevert(expectedError); + staking.unstake(amount); + } + + function testUnstake_RevertWhen_NotDeprovision( + uint256 amount, + uint32 maxVerifierCut, + uint64 thawingPeriod + ) + public + useIndexer + useProvision(amount, maxVerifierCut, thawingPeriod) + useThawRequest(amount) + { + skip(thawingPeriod + 1); + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInsufficientCapacity()"); + vm.expectRevert(expectedError); + staking.unstake(amount); + } +} \ No newline at end of file diff --git a/packages/horizon/test/staking/thaw/thaw.t.sol b/packages/horizon/test/staking/thaw/thaw.t.sol new file mode 100644 index 000000000..e139f219d --- /dev/null +++ b/packages/horizon/test/staking/thaw/thaw.t.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; + +import { IHorizonStakingTypes } from "../../../contracts/interfaces/IHorizonStakingTypes.sol"; +import { HorizonStakingTest } from "../HorizonStaking.t.sol"; + +contract HorizonStakingThawTest is HorizonStakingTest { + + function testThaw_Tokens( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount); + bytes32 expectedThawRequestId = keccak256( + abi.encodePacked(users.indexer, subgraphDataServiceAddress, uint256(0)) + ); + bytes32 thawRequestId = _createThawRequest(thawAmount); + assertEq(thawRequestId, expectedThawRequestId); + + ThawRequest memory thawRequest = staking.getThawRequest(thawRequestId); + assertEq(thawRequest.shares, thawAmount); + assertEq(thawRequest.thawingUntil, block.timestamp + thawingPeriod); + } + + function testThaw_MultipleRequests( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount, + uint256 thawAmount2 + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + thawAmount = bound(thawAmount, 1, amount - 1); + thawAmount2 = bound(thawAmount2, 1, amount - thawAmount); + bytes32 thawRequestId = _createThawRequest(thawAmount); + bytes32 thawRequestId2 = _createThawRequest(thawAmount2); + + ThawRequest memory thawRequest = staking.getThawRequest(thawRequestId); + assertEq(thawRequest.shares, thawAmount); + assertEq(thawRequest.thawingUntil, block.timestamp + thawingPeriod); + assertEq(thawRequest.next, thawRequestId2); + + ThawRequest memory thawRequest2 = staking.getThawRequest(thawRequestId2); + uint256 totalThawAmount = thawAmount + thawAmount2; + assertEq(thawRequest2.shares, (thawRequest.shares * thawAmount2) / totalThawAmount); + assertEq(thawRequest2.thawingUntil, block.timestamp + thawingPeriod); + } + + function testThaw_OperatorCanStartThawing( + uint256 amount, + uint64 thawingPeriod + ) public useOperator useProvision(amount, 0, thawingPeriod) { + bytes32 thawRequestId = _createThawRequest(amount); + + ThawRequest memory thawRequest = staking.getThawRequest(thawRequestId); + assertEq(thawRequest.shares, amount); + assertEq(thawRequest.thawingUntil, block.timestamp + thawingPeriod); + } + + function testThaw_RevertWhen_OperatorNotAuthorized( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + vm.startPrank(users.operator); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingNotAuthorized(address,address,address)", + users.operator, + users.indexer, + subgraphDataServiceAddress + ); + vm.expectRevert(expectedError); + _createThawRequest(amount); + } + + function testThaw_RevertWhen_InsufficientTokensAvailable( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + vm.assume(thawAmount > amount); + bytes memory expectedError = abi.encodeWithSignature( + "HorizonStakingInsufficientTokens(uint256,uint256)", + thawAmount, + amount + ); + vm.expectRevert(expectedError); + _createThawRequest(thawAmount); + } + + function testThaw_RevertWhen_OverMaxThawRequests( + uint256 amount, + uint64 thawingPeriod, + uint256 thawAmount + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + // vm.assume(thawAmount > 0); + // vm.assume(amount / (MAX_THAW_REQUESTS + 1) > thawAmount); + thawAmount = bound(thawAmount, 1, amount / (MAX_THAW_REQUESTS + 1)); + + for (uint256 i = 0; i < 100; i++) { + _createThawRequest(thawAmount); + } + + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingTooManyThawRequests()"); + vm.expectRevert(expectedError); + _createThawRequest(thawAmount); + } + + function testThaw_RevertWhen_ThawingZeroTokens( + uint256 amount, + uint64 thawingPeriod + ) public useIndexer useProvision(amount, 0, thawingPeriod) { + uint256 thawAmount = 0 ether; + bytes memory expectedError = abi.encodeWithSignature("HorizonStakingInvalidZeroTokens()"); + vm.expectRevert(expectedError); + _createThawRequest(thawAmount); + } +} \ No newline at end of file diff --git a/packages/horizon/test/utils/Constants.sol b/packages/horizon/test/utils/Constants.sol index bcf3295d8..00c253bbd 100644 --- a/packages/horizon/test/utils/Constants.sol +++ b/packages/horizon/test/utils/Constants.sol @@ -9,4 +9,10 @@ abstract contract Constants { uint256 internal constant revokeCollectorThawingPeriod = 60; // GraphPayments parameters uint256 internal constant protocolPaymentCut = 10000; + // Staking constants + uint256 internal constant MAX_THAW_REQUESTS = 100; + uint256 internal constant MIN_PROVISION_SIZE = 1e18; + uint32 internal constant MAX_MAX_VERIFIER_CUT = 1000000; // 100% in parts per million + uint64 internal constant MAX_THAWING_PERIOD = 28 days; + uint256 internal constant MIN_DELEGATION = 1e18; } \ No newline at end of file diff --git a/packages/horizon/test/utils/Users.sol b/packages/horizon/test/utils/Users.sol index e86bf8faf..3329d69da 100644 --- a/packages/horizon/test/utils/Users.sol +++ b/packages/horizon/test/utils/Users.sol @@ -8,4 +8,5 @@ struct Users { address operator; address gateway; address verifier; + address delegator; } \ No newline at end of file