From 6f80a48d59d1838eda64e735191b86962feea169 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Mon, 19 Feb 2024 09:18:05 -0500 Subject: [PATCH] tests/foundry v2 (#57) * feat: merklV2 poc * merkl V2 contracts * feat: distribution creator concatenated * feat: delete distribution creator * feat: rework for wrapping new Merkl version in the contract * feat guardian governor * feat: distribution update * feat: add pagination in reward tokens * mock deployment merkl * feat: update creator * feat: change creator * base deployment * feat: wrapping campaigns * fix: DistributionCreator v2 * Iterations (#50) * fix: comments * fix updates * feat merkl V2 iterations * feat: checks on top * feat: last tests * feat: getDistribution getter (#51) * feat: chainId in campaignId (#52) * Default fees (#53) * fix updates * feat: campaign specific fees * change old distrib * feat: change access control * fix: signing * fix: signing (#54) * fix updates * feat: deployment * feat: deploy-v2 * feat: deployments everywhere * tests: update all tests for v2 * chore: revert hardhat config back to pre v2 * tests: add signAndCreateCampaign tests * tests: add all missings unit tests for DistributionCreator * chore: coverage ci and README * tests: correct amount computation in distributioncreation test * Scripts (#56) * feat: scripts for new owner * feat: script to test reward token * tests: revert to previous foundry tests * fix: default value for private key in hardhat c onfig * tests: Distributor foundry tests * tests: fork tests for distributions * tests: distributor claim tests * fix: forgot import in DistributioNCreators tests * chore: add chain uris to ci --------- Co-authored-by: Pablo Veyrat Co-authored-by: picodes Co-authored-by: Pablo Veyrat <50438397+sogipec@users.noreply.github.com> Co-authored-by: Picodes <41673773+Picodes@users.noreply.github.com> --- .github/workflows/ci-deep.yml | 2 + .github/workflows/ci.yml | 4 + test/foundry/unit/DistributionCreator.t.sol | 268 +++++++++- test/foundry/unit/Distributor.t.sol | 519 ++++++++++++++++++++ 4 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 test/foundry/unit/Distributor.t.sol diff --git a/.github/workflows/ci-deep.yml b/.github/workflows/ci-deep.yml index aa8d35d..1b14a58 100644 --- a/.github/workflows/ci-deep.yml +++ b/.github/workflows/ci-deep.yml @@ -153,6 +153,8 @@ jobs: run: yarn foundry:test env: ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} FOUNDRY_FUZZ_RUNS: ${{ github.event.inputs.fuzzRuns || '10000' }} - name: 'Add test summary' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb0377..00c638c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,6 +148,8 @@ jobs: env: FOUNDRY_FUZZ_RUNS: '5000' ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} - name: 'Add test summary' run: | @@ -184,6 +186,8 @@ jobs: run: "yarn ci:coverage" env: ETH_NODE_URI_OPTIMISM: ${{ secrets.ETH_NODE_URI_OPTIMISM }} + ETH_NODE_URI_ARBITRUM: ${{ secrets.ETH_NODE_URI_ARBITRUM }} + ETH_NODE_URI_MAINNET: ${{ secrets.ETH_NODE_URI_MAINNET }} - name: "Upload coverage report to Codecov" uses: "codecov/codecov-action@v3" diff --git a/test/foundry/unit/DistributionCreator.t.sol b/test/foundry/unit/DistributionCreator.t.sol index 0c71ed4..86b56db 100644 --- a/test/foundry/unit/DistributionCreator.t.sol +++ b/test/foundry/unit/DistributionCreator.t.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { DistributionParameters, CampaignParameters } from "../../../contracts/DistributionCreator.sol"; +import { DistributionParameters, CampaignParameters, RewardTokenAmounts } from "../../../contracts/DistributionCreator.sol"; import "../Fixture.t.sol"; contract DistributionCreatorTest is Fixture { @@ -701,4 +701,270 @@ contract Test_DistributionCreator_sign is DistributionCreatorTest { vm.expectRevert(InvalidSignature.selector); creator.sign(abi.encodePacked(r, s, v)); } +} + +contract Test_DistributionCreator_acceptConditions is DistributionCreatorTest { + function test_Success() public { + assertEq(creator.userSignatureWhitelist(bob), 0); + + vm.prank(bob); + creator.acceptConditions(); + + assertEq(creator.userSignatureWhitelist(bob), 1); + } +} + +contract Test_DistributionCreator_setFees is DistributionCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + vm.prank(alice); + creator.setFees(1e8); + } + + function test_RevertWhen_InvalidParam() public { + vm.expectRevert(InvalidParam.selector); + vm.prank(governor); + creator.setFees(1e9); + } + + function test_Success() public { + vm.prank(governor); + creator.setFees(2e8); + + assertEq(creator.defaultFees(), 2e8); + } +} + +contract Test_DistributionCreator_setNewDistributor is DistributionCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + vm.prank(alice); + creator.setNewDistributor(address(bob)); + } + + function test_RevertWhen_InvalidParam() public { + vm.expectRevert(InvalidParam.selector); + vm.prank(governor); + creator.setNewDistributor(address(0)); + } + + function test_Success() public { + vm.prank(governor); + creator.setNewDistributor(address(bob)); + + assertEq(address(creator.distributor()), address(bob)); + } +} + +contract Test_DistributionCreator_setUserFeeRebate is DistributionCreatorTest { + function test_RevertWhen_NotGovernorOrGuardian() public { + vm.expectRevert(NotGovernorOrGuardian.selector); + vm.prank(alice); + creator.setUserFeeRebate(alice, 1e8); + } + + function test_Success() public { + assertEq(creator.feeRebate(alice), 0); + + vm.prank(governor); + creator.setUserFeeRebate(alice, 2e8); + + assertEq(creator.feeRebate(alice), 2e8); + } +} + +contract Test_DistributionCreator_setFeeRecipient is DistributionCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + vm.prank(alice); + creator.setFeeRecipient(address(bob)); + } + + function test_Success() public { + vm.prank(governor); + creator.setFeeRecipient(address(bob)); + + assertEq(address(creator.feeRecipient()), address(bob)); + } +} + +contract Test_DistributionCreator_recoverFees is DistributionCreatorTest { + function test_RevertWhen_NotGovernor() public { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = angle; + + vm.expectRevert(NotGovernor.selector); + vm.prank(alice); + creator.recoverFees(tokens, address(bob)); + } + + function test_Success() public { + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = angle; + + uint256 balance = angle.balanceOf(address(bob)); + + vm.prank(governor); + creator.recoverFees(tokens, address(bob)); + + assertEq(angle.balanceOf(address(bob)), balance + 11e9); + } +} + +contract Test_DistributionCreator_getValidRewardTokens is DistributionCreatorTest { + function test_Success() public { + RewardTokenAmounts[] memory tokens = creator.getValidRewardTokens(); + + assertEq(tokens.length, 1); + assertEq(tokens[0].token, address(angle)); + assertEq(tokens[0].minimumAmountPerEpoch, 1e8); + } + + function test_SuccessSkip() public { + (RewardTokenAmounts[] memory tokens, uint256 i) = creator.getValidRewardTokens(1, 0); + + assertEq(tokens.length, 0); + assertEq(i, 1); + } +} + +contract Test_DistributionCreator_signAndCreateCampaign is DistributionCreatorTest { + function test_Success() public { + CampaignParameters memory campaign = CampaignParameters({ + campaignId: keccak256("TEST"), + creator: address(0), + campaignData: hex"ab", + rewardToken: address(angle), + amount: 1e8, + campaignType: 0, + startTimestamp: uint32(block.timestamp + 1), + duration: 3600 + }); + + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(2, creator.messageHash()); + + vm.startPrank(bob); + + angle.approve(address(creator), 1e8); + creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); + + vm.stopPrank(); + } + + address[] memory whitelist = new address[](1); + whitelist[0] = bob; + address[] memory blacklist = new address[](1); + blacklist[0] = charlie; + + bytes memory extraData = hex"ab"; + + // Additional asserts to check for correct behavior + bytes32 campaignId = bytes32( + keccak256( + abi.encodePacked( + block.chainid, + bob, + address(campaign.rewardToken), + uint32(campaign.campaignType), + uint32(campaign.startTimestamp), + uint32(campaign.duration), + campaign.campaignData + ) + ) + ); + ( + bytes32 fetchedCampaignId, + address fetchedCreator, + address fetchedRewardToken, + uint256 fetchedAmount, + uint32 fetchedCampaignType, + uint32 fetchedStartTimestamp, + uint32 fetchedDuration, + bytes memory fetchedCampaignData + ) = creator.campaignList(creator.campaignLookup(campaignId)); + assertEq(bob, fetchedCreator); + assertEq(address(angle), fetchedRewardToken); + assertEq(campaign.campaignType, fetchedCampaignType); + assertEq(campaign.startTimestamp, fetchedStartTimestamp); + assertEq(campaign.duration, fetchedDuration); + assertEq(extraData, fetchedCampaignData); + assertEq(campaignId, fetchedCampaignId); + assertEq(campaign.amount, fetchedAmount * 10 / 9); + } + + function test_InvalidSignature() public { + CampaignParameters memory campaign = CampaignParameters({ + campaignId: keccak256("TEST"), + creator: address(0), + campaignData: hex"ab", + rewardToken: address(angle), + amount: 1e8, + campaignType: 0, + startTimestamp: uint32(block.timestamp + 1), + duration: 3600 + }); + + { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(1, creator.messageHash()); + + vm.startPrank(bob); + + angle.approve(address(creator), 1e8); + vm.expectRevert(InvalidSignature.selector); + creator.signAndCreateCampaign(campaign, abi.encodePacked(r, s, v)); + + vm.stopPrank(); + } + } +} + +contract DistributionCreatorForkTest is Test { + DistributionCreator public creator; + + function setUp() public { + vm.createSelectFork(vm.envString("ETH_NODE_URI_ARBITRUM")); + + creator = DistributionCreator(0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd); + } +} + +contract Test_DistributionCreator_distribution is DistributionCreatorForkTest { + + function test_Success() public { + CampaignParameters memory distribution = creator.distribution(0); + + assertEq(distribution.campaignId, bytes32(0x7570c9deb1660ed82ff01f760b2883edb9bdb881933b0e4085854d0d717ea268)); + assertEq(distribution.creator, address(0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496)); + assertEq(distribution.rewardToken, address(0xE0688A2FE90d0f93F17f273235031062a210d691)); + assertEq(distribution.amount, 9700000000000000000000); + assertEq(distribution.campaignType, 2); + assertEq(distribution.startTimestamp, 1681380000); + assertEq(distribution.duration, 86400); + assertEq(distribution.campaignData, hex"000000000000000000000000149e36e72726e0bcea5c59d40df2c43f60f5a22d0000000000000000000000000000000000000000000000000000000000000bb800000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000013880000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000023078000000000000000000000000000000000000000000000000000000000000"); + } +} + +contract Test_DistributionCreator_getDistributionsBetweenEpochs is DistributionCreatorForkTest { + + function test_Success() public { + (DistributionParameters[] memory distributions,) = creator.getDistributionsBetweenEpochs(1681380000, 1681380000 + 3600, 0, type(uint32).max); + + assertEq(distributions.length, 1); + assertEq(distributions[0].uniV3Pool, address(0x149e36E72726e0BceA5c59d40df2c43F60f5A22D)); + assertEq(distributions[0].rewardToken, address(0xE0688A2FE90d0f93F17f273235031062a210d691)); + assertEq(distributions[0].amount, 9700000000000000000000); + assertEq(distributions[0].positionWrappers.length, 0); + assertEq(distributions[0].wrapperTypes.length, 0); + assertEq(distributions[0].propToken0, 2000); + assertEq(distributions[0].propToken1, 5000); + assertEq(distributions[0].propFees, 3000); + assertEq(distributions[0].isOutOfRangeIncentivized, 0); + assertEq(distributions[0].epochStart, 1681380000); + assertEq(distributions[0].numEpoch, 24); + assertEq(distributions[0].boostedReward, 0); + assertEq(distributions[0].boostingAddress, address(0)); + assertEq(distributions[0].rewardId, bytes32(0x7570c9deb1660ed82ff01f760b2883edb9bdb881933b0e4085854d0d717ea268)); + assertEq(distributions[0].additionalData, hex"290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"); + } } \ No newline at end of file diff --git a/test/foundry/unit/Distributor.t.sol b/test/foundry/unit/Distributor.t.sol new file mode 100644 index 0000000..0124a11 --- /dev/null +++ b/test/foundry/unit/Distributor.t.sol @@ -0,0 +1,519 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import { Distributor, MerkleTree } from "../../../contracts/Distributor.sol"; +import "../Fixture.t.sol"; + +contract DistributorCreatorTest is Fixture { + Distributor public distributor; + Distributor public distributorImpl; + + function setUp() public virtual override { + super.setUp(); + + distributorImpl = new Distributor(); + distributor = Distributor(deployUUPS(address(distributorImpl), hex"")); + distributor.initialize(ICore(address(coreBorrow))); + + vm.startPrank(governor); + distributor.setDisputeAmount(1e18); + distributor.setDisputePeriod(1 days); + distributor.setDisputeToken(angle); + vm.stopPrank(); + + angle.mint(address(alice), 100e18); + } + + function getRoot() public view returns (bytes32) { + return keccak256(abi.encodePacked("MERKLE_ROOT")); + } +} + +contract Test_Distributor_Initialize is DistributorCreatorTest { + Distributor d; + + function setUp() public override { + super.setUp(); + d = Distributor(deployUUPS(address(new Distributor()), hex"")); + } + + function test_RevertWhen_CalledOnImplem() public { + vm.expectRevert("Initializable: contract is already initialized"); + distributorImpl.initialize(ICore(address(0))); + } + + function test_RevertWhen_ZeroAddress() public { + vm.expectRevert(ZeroAddress.selector); + d.initialize(ICore(address(0))); + } + + function test_Success() public { + d.initialize(ICore(address(coreBorrow))); + + assertEq(address(coreBorrow), address(d.core())); + } +} + +contract Test_Distributor_toggleTrusted is DistributorCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + distributor.toggleTrusted(address(bob)); + } + + function test_Success() public { + vm.startPrank(governor); + distributor.toggleTrusted(bob); + assertEq(distributor.canUpdateMerkleRoot(bob), 1); + distributor.toggleTrusted(bob); + assertEq(distributor.canUpdateMerkleRoot(bob), 0); + vm.stopPrank(); + } +} + +contract Test_Distributor_toggleOperator is DistributorCreatorTest { + function test_RevertWhen_NotTrusted() public { + vm.expectRevert(NotTrusted.selector); + distributor.toggleOperator(bob, alice); + } + + function test_Success() public { + vm.prank(bob); + distributor.toggleOperator(bob, alice); + assertEq(distributor.operators(bob, alice), 1); + + vm.prank(governor); + distributor.toggleOperator(bob, alice); + assertEq(distributor.operators(bob, alice), 0); + vm.stopPrank(); + } +} + +contract Test_Distributor_toggleOnlyOperatorCanClaim is DistributorCreatorTest { + function test_RevertWhen_NotTrusted() public { + vm.expectRevert(NotTrusted.selector); + distributor.toggleOnlyOperatorCanClaim(bob); + } + + function test_Success() public { + vm.prank(governor); + distributor.toggleOnlyOperatorCanClaim(bob); + assertEq(distributor.onlyOperatorCanClaim(bob), 1); + + vm.prank(bob); + distributor.toggleOnlyOperatorCanClaim(bob); + assertEq(distributor.onlyOperatorCanClaim(bob), 0); + } +} + +contract Test_Distributor_recoverERC20 is DistributorCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + distributor.recoverERC20(address(0), address(0), 0); + } + + function test_Success() public { + uint256 amount = 1e18; + angle.mint(address(distributor), amount); + + vm.prank(governor); + distributor.recoverERC20(address(angle), address(governor), amount); + assertEq(angle.balanceOf(address(distributor)), 0); + assertEq(angle.balanceOf(address(governor)), amount); + } +} + +contract Test_Distributor_setDisputePeriod is DistributorCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + distributor.setDisputePeriod(0); + } + + function test_Success() public { + vm.prank(governor); + distributor.setDisputePeriod(1); + assertEq(distributor.disputePeriod(), 1); + } +} + +contract Test_Distributor_setDisputeToken is DistributorCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + distributor.setDisputeToken(angle); + } + + function test_Success() public { + vm.prank(governor); + distributor.setDisputeToken(angle); + assertEq(address(distributor.disputeToken()), address(angle)); + } +} + +contract Test_Distributor_setDisputeAmount is DistributorCreatorTest { + function test_RevertWhen_NotGovernor() public { + vm.expectRevert(NotGovernor.selector); + distributor.setDisputeAmount(0); + } + + function test_Success() public { + vm.prank(governor); + distributor.setDisputeAmount(1); + assertEq(distributor.disputeAmount(), 1); + } +} + +contract Test_Distributor_updateTree is DistributorCreatorTest { + function test_RevertWhen_NotTrusted() public { + vm.expectRevert(NotTrusted.selector); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + } + + function test_RevertWhen_DisputeOngoing() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + vm.expectRevert(NotTrusted.selector); + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + } + + function test_RevertWhen_DisputeNotFinished() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + vm.expectRevert(NotTrusted.selector); + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + } + + function test_Success() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + (bytes32 merkleRoot, bytes32 ipfsHash) = distributor.tree(); + assertEq(merkleRoot, getRoot()); + assertEq(ipfsHash, keccak256("IPFS_HASH")); + + (merkleRoot, ipfsHash) = distributor.lastTree(); + assertEq(merkleRoot, bytes32(0)); + assertEq(ipfsHash, bytes32(0)); + + vm.warp(distributor.endOfDisputePeriod() - 1); + merkleRoot = distributor.getMerkleRoot(); + assertEq(merkleRoot, bytes32(0)); + + vm.warp(distributor.endOfDisputePeriod() + 1); + merkleRoot = distributor.getMerkleRoot(); + assertEq(merkleRoot, getRoot()); + + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HAS")})); + + (merkleRoot, ipfsHash) = distributor.lastTree(); + assertEq(merkleRoot, getRoot()); + assertEq(ipfsHash, keccak256("IPFS_HASH")); + } +} + +contract Test_Distributor_revokeTree is DistributorCreatorTest { + function test_RevertWhen_NotGovernorOrGuardian() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.expectRevert(NotGovernorOrGuardian.selector); + distributor.revokeTree(); + } + + function test_RevertWhen_UnresolvedDispute() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + vm.expectRevert(UnresolvedDispute.selector); + vm.prank(governor); + distributor.revokeTree(); + } + + function test_Success() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.prank(governor); + distributor.revokeTree(); + + (bytes32 merkleRoot, bytes32 ipfsHash) = distributor.tree(); + (bytes32 lastMerkleRoot, bytes32 lastIpfsHash) = distributor.lastTree(); + + assertEq(merkleRoot, lastMerkleRoot); + assertEq(ipfsHash, lastIpfsHash); + assertEq(distributor.endOfDisputePeriod(), 0); + } +} + +contract Test_Distributor_disputeTree is DistributorCreatorTest { + function test_RevertWhen_UnresolvedDispute() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + vm.expectRevert(UnresolvedDispute.selector); + vm.prank(governor); + distributor.disputeTree("wrong"); + } + + function test_RevertWhen_InvalidDispute() public { + vm.expectRevert(InvalidDispute.selector); + vm.prank(governor); + distributor.disputeTree("wrong"); + } + + function test_Success() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + assertEq(distributor.disputer(), address(alice)); + } +} + +contract Test_Distributor_resolveDispute is DistributorCreatorTest { + function test_RevertWhen_NotGovernorOrGuardian() public { + vm.expectRevert(NotGovernorOrGuardian.selector); + distributor.resolveDispute(true); + } + + function test_RevertWhen_NoDispute() public { + vm.expectRevert(NoDispute.selector); + vm.prank(governor); + distributor.resolveDispute(true); + } + + function test_SuccessValid() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + uint256 balance = angle.balanceOf(address(alice)); + + vm.warp(distributor.endOfDisputePeriod() + 1); + vm.prank(governor); + distributor.resolveDispute(true); + + assertEq(distributor.disputer(), address(0)); + assertEq(distributor.endOfDisputePeriod(), 0); + (bytes32 merkleRoot, bytes32 ipfsHash) = distributor.tree(); + (bytes32 lastMerkleRoot, bytes32 lastIpfsHash) = distributor.lastTree(); + assertEq(merkleRoot, lastMerkleRoot); + assertEq(ipfsHash, lastIpfsHash); + assertEq(angle.balanceOf(address(alice)), balance + distributor.disputeAmount()); + } + + function test_SuccessInvalid() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() - 1); + vm.startPrank(alice); + angle.approve(address(distributor), distributor.disputeAmount()); + distributor.disputeTree("wrong"); + vm.stopPrank(); + + uint256 balance = angle.balanceOf(address(alice)); + uint256 governorBalance = angle.balanceOf(address(governor)); + + vm.warp(distributor.endOfDisputePeriod() + 1); + vm.prank(governor); + distributor.resolveDispute(false); + + assertEq(distributor.disputer(), address(0)); + (bytes32 merkleRoot, bytes32 ipfsHash) = distributor.tree(); + assertEq(merkleRoot, getRoot()); + assertEq(ipfsHash, keccak256("IPFS_HASH")); + assertEq(angle.balanceOf(address(alice)), balance); + assertEq(angle.balanceOf(address(governor)), governorBalance + distributor.disputeAmount()); + } +} + +contract Test_Distributor_claim is DistributorCreatorTest { + function test_RevertWhen_NotWhitelisted() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + vm.prank(bob); + distributor.toggleOnlyOperatorCanClaim(bob); + + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + proofs[0] = new bytes32[](1); + users[0] = bob; + tokens[0] = address(angle); + amounts[0] = 1e18; + + vm.expectRevert(NotWhitelisted.selector); + vm.prank(alice); + distributor.claim(users, tokens, amounts, proofs); + } + + function test_RevertWhen_InvalidLengths() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](0); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + + vm.expectRevert(InvalidLengths.selector); + distributor.claim(users, tokens, amounts, proofs); + + users = new address[](2); + vm.expectRevert(InvalidLengths.selector); + distributor.claim(users, tokens, amounts, proofs); + + users = new address[](1); + proofs = new bytes32[][](0); + vm.expectRevert(InvalidLengths.selector); + distributor.claim(users, tokens, amounts, proofs); + + proofs = new bytes32[][](1); + tokens = new address[](0); + vm.expectRevert(InvalidLengths.selector); + distributor.claim(users, tokens, amounts, proofs); + + tokens = new address[](1); + amounts = new uint256[](0); + vm.expectRevert(InvalidLengths.selector); + distributor.claim(users, tokens, amounts, proofs); + } + + function test_RevertWhen_InvalidProof() public { + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: getRoot(), ipfsHash: keccak256("IPFS_HASH")})); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + bytes32[][] memory proofs = new bytes32[][](1); + address[] memory users = new address[](1); + address[] memory tokens = new address[](1); + uint256[] memory amounts = new uint256[](1); + proofs[0] = new bytes32[](1); + users[0] = bob; + tokens[0] = address(angle); + amounts[0] = 1e18; + + vm.expectRevert(InvalidProof.selector); + distributor.claim(users, tokens, amounts, proofs); + } + + function test_SuccessGovernor() public { + console.log(alice, bob, address(angle), address(agEUR)); + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), ipfsHash: keccak256("IPFS_HASH")})); + + angle.mint(address(distributor), 1e18); + agEUR.mint(address(distributor), 5e17); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + bytes32[][] memory proofs = new bytes32[][](2); + address[] memory users = new address[](2); + address[] memory tokens = new address[](2); + uint256[] memory amounts = new uint256[](2); + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0x6f46ee2909b99367a0d9932a11f1bdb85c9354480c9de277d21086f9a8925c0a); + users[0] = alice; + tokens[0] = address(angle); + amounts[0] = 1e18; + proofs[1] = new bytes32[](1); + proofs[1][0] = bytes32(0x3a64e591d79db8530701e6f3dbdd95dc74681291b327d0ce4acc97024a61430c); + users[1] = bob; + tokens[1] = address(agEUR); + amounts[1] = 5e17; + + uint256 aliceBalance = angle.balanceOf(address(alice)); + uint256 bobBalance = agEUR.balanceOf(address(bob)); + + vm.prank(governor); + distributor.claim(users, tokens, amounts, proofs); + + assertEq(angle.balanceOf(address(alice)), aliceBalance + 1e18); + assertEq(agEUR.balanceOf(address(bob)), bobBalance + 5e17); + } + + function test_SuccessOperator() public { + console.log(alice, bob, address(angle), address(agEUR)); + vm.prank(governor); + distributor.updateTree(MerkleTree({merkleRoot: bytes32(0x0b70a97c062cb747158b89e27df5bbda859ba072232efcbe92e383e9d74b8555), ipfsHash: keccak256("IPFS_HASH")})); + + angle.mint(address(distributor), 1e18); + agEUR.mint(address(distributor), 5e17); + + vm.warp(distributor.endOfDisputePeriod() + 1); + + bytes32[][] memory proofs = new bytes32[][](2); + address[] memory users = new address[](2); + address[] memory tokens = new address[](2); + uint256[] memory amounts = new uint256[](2); + proofs[0] = new bytes32[](1); + proofs[0][0] = bytes32(0x6f46ee2909b99367a0d9932a11f1bdb85c9354480c9de277d21086f9a8925c0a); + users[0] = alice; + tokens[0] = address(angle); + amounts[0] = 1e18; + proofs[1] = new bytes32[](1); + proofs[1][0] = bytes32(0x3a64e591d79db8530701e6f3dbdd95dc74681291b327d0ce4acc97024a61430c); + users[1] = bob; + tokens[1] = address(agEUR); + amounts[1] = 5e17; + + uint256 aliceBalance = angle.balanceOf(address(alice)); + uint256 bobBalance = agEUR.balanceOf(address(bob)); + + vm.prank(alice); + distributor.toggleOperator(alice, bob); + + vm.prank(bob); + distributor.claim(users, tokens, amounts, proofs); + + assertEq(angle.balanceOf(address(alice)), aliceBalance + 1e18); + assertEq(agEUR.balanceOf(address(bob)), bobBalance + 5e17); + } +}