From 5fb7ab2f92e57cf607897b3a5cefe1ed8f939ffc Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Thu, 24 Mar 2022 17:07:08 +0900 Subject: [PATCH 1/9] test: Adjusted helpers --- test/helpers/block-traveller.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/helpers/block-traveller.ts b/test/helpers/block-traveller.ts index 7438497..1eff4f1 100644 --- a/test/helpers/block-traveller.ts +++ b/test/helpers/block-traveller.ts @@ -57,3 +57,17 @@ export async function increaseTo(targetTime: BigNumber): Promise { export async function latest(): Promise { return (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; } + +/** + * Start automine + */ +export async function pauseAutomine(): Promise { + await network.provider.send("evm_setAutomine", [false]); +} + +/** + * Resume automine + */ +export async function resumeAutomine(): Promise { + await network.provider.send("evm_setAutomine", [true]); +} From b5ad946e5f4edc4728a87e2966bd55bfcc1c5e9d Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Thu, 24 Mar 2022 17:10:04 +0900 Subject: [PATCH 2/9] test: Adjusted tests --- test/feeSharingSystem.test.ts | 3 +-- test/looksRareToken.test.ts | 25 +++++++++++++++++++++---- test/privateSaleWithFeeSharing.test.ts | 6 +++--- test/tokenDistributor.test.ts | 3 +-- test/tokenSplitter.test.ts | 2 +- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/test/feeSharingSystem.test.ts b/test/feeSharingSystem.test.ts index 7e09180..f60d465 100644 --- a/test/feeSharingSystem.test.ts +++ b/test/feeSharingSystem.test.ts @@ -3,10 +3,9 @@ import { ethers } from "hardhat"; import { BigNumber, constants, Contract, utils } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { defaultAbiCoder } from "ethers/lib/utils"; -import * as blockTraveller from "./helpers/block-traveller"; +import { advanceBlockTo } from "./helpers/block-traveller"; const { parseEther } = utils; -const { advanceBlockTo } = blockTraveller; describe("FeeSharingSystem", () => { let feeSharingSetter: Contract; diff --git a/test/looksRareToken.test.ts b/test/looksRareToken.test.ts index 53a3612..a1b2865 100644 --- a/test/looksRareToken.test.ts +++ b/test/looksRareToken.test.ts @@ -1,4 +1,4 @@ -import { expect } from "chai"; +import { assert, expect } from "chai"; import { BigNumber, constants, Contract, utils } from "ethers"; import { ethers } from "hardhat"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; @@ -19,18 +19,37 @@ describe("LooksRareToken", () => { cap = parseEther("1000000000"); const LooksRareToken = await ethers.getContractFactory("LooksRareToken"); - looksRareToken = await LooksRareToken.deploy(admin.address, premintAmount, cap); await looksRareToken.deployed(); }); describe("#1 - Regular user/owner interactions", async () => { + it("Post-deployment values are correct", async () => { + assert.deepEqual(await looksRareToken.SUPPLY_CAP(), cap); + assert.deepEqual(await looksRareToken.totalSupply(), premintAmount); + }); + it("Owner can mint", async () => { const valueToMint = parseEther("100000"); + await expect(looksRareToken.connect(admin).mint(admin.address, valueToMint)) + .to.emit(looksRareToken, "Transfer") + .withArgs(constants.AddressZero, admin.address, valueToMint); + }); + it("Owner cannot mint more than cap", async () => { + let valueToMint = cap.sub(premintAmount); await expect(looksRareToken.connect(admin).mint(admin.address, valueToMint)) .to.emit(looksRareToken, "Transfer") .withArgs(constants.AddressZero, admin.address, valueToMint); + + assert.deepEqual(await looksRareToken.totalSupply(), cap); + + valueToMint = BigNumber.from("1"); + await expect(looksRareToken.connect(admin).mint(admin.address, valueToMint)).not.to.emit( + looksRareToken, + "Transfer" + ); + assert.deepEqual(await looksRareToken.totalSupply(), cap); }); }); @@ -43,9 +62,7 @@ describe("LooksRareToken", () => { it("Cannot deploy if cap is greater than premint amount", async () => { const wrongCap = BigNumber.from("0"); - const LooksRareToken = await ethers.getContractFactory("LooksRareToken"); - await expect(LooksRareToken.deploy(admin.address, premintAmount, wrongCap)).to.be.revertedWith( "LOOKS: Premint amount is greater than cap" ); diff --git a/test/privateSaleWithFeeSharing.test.ts b/test/privateSaleWithFeeSharing.test.ts index 3e9c266..8524769 100644 --- a/test/privateSaleWithFeeSharing.test.ts +++ b/test/privateSaleWithFeeSharing.test.ts @@ -307,7 +307,7 @@ describe("PrivateSaleWithFeeSharing", () => { .to.emit(privateSale, "Withdraw") .withArgs(user.address, BigNumber.from("1"), amountLOOKSForTier1); - if (user != firstTierUser1 && user != firstTierUser2) { + if (user !== firstTierUser1 && user !== firstTierUser2) { await expect(tx).to.emit(privateSale, "Harvest").withArgs(user.address, expectedHarvestAmountForTier1Step2); } else { await expect(tx).to.not.emit(privateSale, "Harvest"); @@ -320,7 +320,7 @@ describe("PrivateSaleWithFeeSharing", () => { .to.emit(privateSale, "Withdraw") .withArgs(user.address, BigNumber.from("2"), amountLOOKSForTier2); - if (user != secondTierUser1 && user != secondTierUser2) { + if (user !== secondTierUser1 && user !== secondTierUser2) { await expect(tx).to.emit(privateSale, "Harvest").withArgs(user.address, expectedHarvestAmountForTier2Step2); } else { await expect(tx).to.not.emit(privateSale, "Harvest"); @@ -333,7 +333,7 @@ describe("PrivateSaleWithFeeSharing", () => { .to.emit(privateSale, "Withdraw") .withArgs(user.address, BigNumber.from("3"), amountLOOKSForTier3); - if (user != thirdTierUser1 && user != thirdTierUser2) { + if (user !== thirdTierUser1 && user !== thirdTierUser2) { await expect(tx).to.emit(privateSale, "Harvest").withArgs(user.address, expectedHarvestAmountForTier3Step2); } else { await expect(tx).to.not.emit(privateSale, "Harvest"); diff --git a/test/tokenDistributor.test.ts b/test/tokenDistributor.test.ts index f13a7fa..8de2dd5 100644 --- a/test/tokenDistributor.test.ts +++ b/test/tokenDistributor.test.ts @@ -2,10 +2,9 @@ import { assert, expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber, constants, Contract, utils } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import * as blockTraveller from "./helpers/block-traveller"; +import { advanceBlockTo } from "./helpers/block-traveller"; const { parseEther } = utils; -const { advanceBlockTo } = blockTraveller; describe("TokenDistributor", () => { let looksRareToken: Contract; diff --git a/test/tokenSplitter.test.ts b/test/tokenSplitter.test.ts index bf0fbd5..3af9f9a 100644 --- a/test/tokenSplitter.test.ts +++ b/test/tokenSplitter.test.ts @@ -1,6 +1,6 @@ import { assert, expect } from "chai"; import { ethers } from "hardhat"; -import { BigNumber, constants, Contract, utils } from "ethers"; +import { constants, Contract, utils } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; const { parseEther } = utils; From 73a857a826c6a8c7428e5718e4b9a1d59334fa5a Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Thu, 24 Mar 2022 17:11:43 +0900 Subject: [PATCH 3/9] test: Added tests for owner functions --- .../aggregatorFeeSharingWithUniswapV3.test.ts | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/test/aggregatorFeeSharingWithUniswapV3.test.ts b/test/aggregatorFeeSharingWithUniswapV3.test.ts index f6d427b..044887b 100644 --- a/test/aggregatorFeeSharingWithUniswapV3.test.ts +++ b/test/aggregatorFeeSharingWithUniswapV3.test.ts @@ -2,9 +2,8 @@ import { assert, expect } from "chai"; import { ethers } from "hardhat"; import { BigNumber, constants, Contract, utils } from "ethers"; import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import * as blockTraveller from "./helpers/block-traveller"; +import { advanceBlock, advanceBlockTo, pauseAutomine, resumeAutomine } from "./helpers/block-traveller"; -const { advanceBlockTo } = blockTraveller; const { parseEther } = utils; async function setupUsers( @@ -17,9 +16,7 @@ async function setupUsers( for (const user of users) { await looksRareToken.connect(admin).transfer(user.address, parseEther("200")); await looksRareToken.connect(user).approve(feeSharingSystem.address, constants.MaxUint256); - await looksRareToken.connect(user).approve(aggregator.address, constants.MaxUint256); - await aggregator.connect(user).deposit(parseEther("100")); } } @@ -41,11 +38,10 @@ describe("AggregatorFeeSharing", () => { beforeEach(async () => { accounts = await ethers.getSigners(); - admin = accounts[0]; const tokenSplitter = accounts[19]; - const premintReceiver = admin; + const premintReceiver = admin; const premintAmount = parseEther("6250"); const cap = parseEther("25000"); // 25,000 tokens @@ -95,7 +91,6 @@ describe("AggregatorFeeSharing", () => { rewardToken.address, tokenDistributor.address ); - await feeSharingSystem.deployed(); const minRewardDurationInBlocks = "30"; @@ -222,10 +217,10 @@ describe("AggregatorFeeSharing", () => { // Advanced to the end of the first fee-sharing await advanceBlockTo(BigNumber.from(await tokenDistributor.START_BLOCK()).add("49")); - // Withdraw all + // User1 withdraws all let tx = await aggregator.connect(user1).withdrawAll(); - // 50 WETH sold for 100 LOOKS + // 50 WETH is sold for 100 LOOKS await expect(tx) .to.emit(aggregator, "ConversionToLOOKS") .withArgs(parseEther("49.99999999999999980"), parseEther("99.9999999999999996")); @@ -297,7 +292,6 @@ describe("AggregatorFeeSharing", () => { await rewardToken.connect(admin).transfer(aggregator.address, parseEther("0.999")); const tx = await aggregator.connect(admin).harvestAndSellAndCompound(); - // Amount is equal to the threshold to trigger the selling await expect(tx).to.emit(aggregator, "ConversionToLOOKS").withArgs(parseEther("0.999"), parseEther("0.999")); // Amount is lower than threshold to trigger the deposit @@ -341,6 +335,20 @@ describe("AggregatorFeeSharing", () => { await expect(aggregator.connect(admin).harvestAndSellAndCompound()).to.be.revertedWith("Harvest: No share"); }); + it("Cannot harvest if already harvested in same block", async () => { + const [user1, user2, user3] = [accounts[1], accounts[2], accounts[3]]; + await setupUsers(feeSharingSystem, looksRareToken, aggregator, admin, [user1, user2, user3]); + await pauseAutomine(); + await aggregator.connect(admin).harvestAndSellAndCompound(); + const tx = await aggregator.connect(admin).harvestAndSellAndCompound(); + const pendingBlock = await ethers.provider.send("eth_getBlockByNumber", ["pending", false]); + // Verify there are 2+ txs in the mempool + assert.isAtLeast(pendingBlock.transactions.length, 2); + await advanceBlock(); + await resumeAutomine(); + await expect(tx.wait()).to.be.reverted; + }); + it("Faulty router doesn't throw revertion on harvesting operations if it fails to sell", async () => { const MockFaultyUniswapV3Router = await ethers.getContractFactory("MockFaultyUniswapV3Router"); const faultyUniswapRouter = await MockFaultyUniswapV3Router.deploy(); @@ -379,19 +387,35 @@ describe("AggregatorFeeSharing", () => { await expect(tx).to.emit(aggregator, "HarvestStop"); }); - it("Owner can update threshold", async () => { + it("Owner can update threshold amount", async () => { const tx = await aggregator.connect(admin).updateThresholdAmount(parseEther("5")); await expect(tx).to.emit(aggregator, "NewThresholdAmount").withArgs(parseEther("5")); }); + it("Owner can adjust buffer block only within limits", async () => { + const MAXIMUM_HARVEST_BUFFER_BLOCKS = await aggregator.MAXIMUM_HARVEST_BUFFER_BLOCKS(); + const tx = await aggregator.connect(admin).updateHarvestBufferBlocks(MAXIMUM_HARVEST_BUFFER_BLOCKS); + await expect(tx).to.emit(aggregator, "NewHarvestBufferBlocks").withArgs(MAXIMUM_HARVEST_BUFFER_BLOCKS); + + await expect( + aggregator.connect(admin).updateHarvestBufferBlocks(MAXIMUM_HARVEST_BUFFER_BLOCKS.add("1")) + ).to.be.revertedWith("Owner: Must be below MAXIMUM_HARVEST_BUFFER_BLOCKS"); + }); + it("Owner can reset maximum allowance for LOOKS token", async () => { const tx = await aggregator.connect(admin).checkAndAdjustLOOKSTokenAllowanceIfRequired(); - await expect(tx) .to.emit(looksRareToken, "Approval") .withArgs(aggregator.address, feeSharingSystem.address, constants.MaxUint256); }); + it("Owner can reset maximum allowance for reward token", async () => { + const tx = await aggregator.connect(admin).checkAndAdjustRewardTokenAllowanceIfRequired(); + await expect(tx) + .to.emit(rewardToken, "Approval") + .withArgs(aggregator.address, uniswapRouter.address, constants.MaxUint256); + }); + it("Owner can pause/unpause", async () => { let tx = await aggregator.connect(admin).pause(); await expect(tx).to.emit(aggregator, "Paused"); @@ -413,6 +437,14 @@ describe("AggregatorFeeSharing", () => { ); }); + it("Owner can update minPriceLOOKSInWETH", async () => { + // 1 LOOKS is at least equal to 0.01 WETH + // 1 WETH is at most equal to 100 LOOKS + const tx = await aggregator.connect(admin).updateMinPriceOfLOOKSInWETH(parseEther("0.01")); + await expect(tx).to.emit(aggregator, "NewMinimumPriceOfLOOKSInWETH").withArgs(parseEther("0.01")); + assert.deepEqual(await aggregator.minPriceLOOKSInWETH(), parseEther("0.01")); + }); + it("Only owner can call functions for onlyOwner", async () => { const [user1, user2, user3] = [accounts[1], accounts[2], accounts[3]]; await setupUsers(feeSharingSystem, looksRareToken, aggregator, admin, [user1, user2, user3]); @@ -426,6 +458,9 @@ describe("AggregatorFeeSharing", () => { await expect(aggregator.connect(user1).updateThresholdAmount("10")).to.be.revertedWith( "Ownable: caller is not the owner" ); + await expect(aggregator.connect(user1).updateMinPriceOfLOOKSInWETH(parseEther("0.01"))).to.be.revertedWith( + "Ownable: caller is not the owner" + ); await expect(aggregator.connect(user1).pause()).to.be.revertedWith("Ownable: caller is not the owner"); await expect(aggregator.connect(user1).unpause()).to.be.revertedWith("Ownable: caller is not the owner"); await expect(aggregator.connect(user1).startHarvest()).to.be.revertedWith("Ownable: caller is not the owner"); @@ -433,7 +468,9 @@ describe("AggregatorFeeSharing", () => { await expect(aggregator.connect(user1).checkAndAdjustLOOKSTokenAllowanceIfRequired()).to.be.revertedWith( "Ownable: caller is not the owner" ); - + await expect(aggregator.connect(user1).checkAndAdjustRewardTokenAllowanceIfRequired()).to.be.revertedWith( + "Ownable: caller is not the owner" + ); await expect(aggregator.connect(user1).stopHarvest()).to.be.revertedWith("Ownable: caller is not the owner"); }); }); From df83d1bc39ac439b73b48ec457e596775affb4d4 Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Sun, 27 Mar 2022 15:36:26 +0900 Subject: [PATCH 4/9] fix: Fixed variable name --- .../AggregatorFeeSharingWithUniswapV3.sol | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/AggregatorFeeSharingWithUniswapV3.sol b/contracts/AggregatorFeeSharingWithUniswapV3.sol index d7a2aac..310456d 100644 --- a/contracts/AggregatorFeeSharingWithUniswapV3.sol +++ b/contracts/AggregatorFeeSharingWithUniswapV3.sol @@ -49,8 +49,8 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard // Last user action block uint256 public lastHarvestBlock; - // Minimum price of LOOKS (in WETH) multiplied 1e18 (e.g., 0.0004 ETH --> 4e14) - uint256 public minPriceLOOKSInWETH; + // Maximum price of LOOKS (in WETH) multiplied 1e18 (e.g., 0.0004 ETH --> 4e14) + uint256 public maxPriceLOOKSInWETH; // Threshold amount (in rewardToken) uint256 public thresholdAmount; @@ -67,7 +67,7 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard event HarvestStart(); event HarvestStop(); event NewHarvestBufferBlocks(uint256 harvestBufferBlocks); - event NewMinimumPriceOfLOOKSInWETH(uint256 minPriceLOOKSInWETH); + event NewMaximumPriceOfLOOKSInWETH(uint256 maxPriceLOOKSInWETH); event NewThresholdAmount(uint256 thresholdAmount); event NewTradingFeeUniswapV3(uint24 tradingFeeUniswapV3); event Withdraw(address indexed user, uint256 amount); @@ -210,14 +210,14 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard } /** - * @notice Update minimum price of LOOKS in WETH - * @param _newMinPriceLOOKSInWETH new minimum price of LOOKS in ETH times 1e18 + * @notice Update maximum price of LOOKS in WETH + * @param _newMaxPriceLOOKSInWETH new maximum price of LOOKS in WETH times 1e18 * @dev Only callable by owner */ - function updateMinPriceOfLOOKSInWETH(uint256 _newMinPriceLOOKSInWETH) external onlyOwner { - minPriceLOOKSInWETH = _newMinPriceLOOKSInWETH; + function updateMaxPriceOfLOOKSInWETH(uint256 _newMaxPriceLOOKSInWETH) external onlyOwner { + maxPriceLOOKSInWETH = _newMaxPriceLOOKSInWETH; - emit NewMinimumPriceOfLOOKSInWETH(_newMinPriceLOOKSInWETH); + emit NewMaximumPriceOfLOOKSInWETH(_newMaxPriceLOOKSInWETH); } /** @@ -328,7 +328,7 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard * @return whether the transaction went through */ function _sellRewardTokenToLOOKS(uint256 _amount) internal returns (bool) { - uint256 amountOutMinimum = minPriceLOOKSInWETH != 0 ? (_amount * minPriceLOOKSInWETH) / 1e18 : 0; + uint256 amountOutMinimum = maxPriceLOOKSInWETH != 0 ? (_amount * 1e18) / maxPriceLOOKSInWETH : 0; // Set the order parameters ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams( From 726e8205284be70d6ea4e0791eef5ca09eb54ce1 Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Sun, 27 Mar 2022 15:37:18 +0900 Subject: [PATCH 5/9] test: Added more tests --- .../AggregatorFeeSharingWithUniswapV3.t.sol | 19 ++++++-- contracts/test/utils/MockUniswapV3Router.sol | 12 ++++- .../aggregatorFeeSharingWithUniswapV3.test.ts | 44 ++++++++++++++++--- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol index 72ec582..2bddf4e 100644 --- a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol +++ b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol @@ -89,8 +89,8 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { aggregatorFeeSharingWithUniswapV3.updateThresholdAmount(_parseEtherWithFloating(5, 1)); aggregatorFeeSharingWithUniswapV3.updateHarvestBufferBlocks(10); - // 7. Distribute LOOKS to user accounts (from the premint) - address[4] memory users = [address(1), address(2), address(3), address(4)]; + // 7. Distribute LOOKS (from the premint) to user accounts + address[4] memory users = [user1, user2, user3, user4]; for (uint256 i = 0; i < users.length; i++) { cheats.prank(_PREMINT_RECEIVER); @@ -125,7 +125,7 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), _parseEther(0)); } - function testDepositAndWithdrawSameBlock(uint8 x, uint16 numberBlocks) public asPrankedUser(user1) { + function testSameBlockDepositAndWithdraw(uint8 x, uint16 numberBlocks) public asPrankedUser(user1) { uint256 amountDeposit = _parseEther(x); cheats.assume(amountDeposit >= aggregatorFeeSharingWithUniswapV3.MINIMUM_DEPOSIT_LOOKS()); cheats.roll(_START_BLOCK + uint256(numberBlocks)); @@ -135,4 +135,17 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { aggregatorFeeSharingWithUniswapV3.withdrawAll(); assertEq(looksRareToken.balanceOf(user1), currentBalanceUser1 + amountDeposit); } + + function testSameBlockMultipleDeposits(uint8 x, uint16 numberBlocks) public { + uint256 amountDeposit = _parseEther(x); + cheats.assume(amountDeposit >= aggregatorFeeSharingWithUniswapV3.MINIMUM_DEPOSIT_LOOKS()); + cheats.roll(_START_BLOCK + uint256(numberBlocks)); + + cheats.prank(user1); + aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); + + cheats.prank(user2); + aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); + assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), aggregatorFeeSharingWithUniswapV3.userInfo(user2)); + } } diff --git a/contracts/test/utils/MockUniswapV3Router.sol b/contracts/test/utils/MockUniswapV3Router.sol index 73c9f3c..81e5288 100644 --- a/contracts/test/utils/MockUniswapV3Router.sol +++ b/contracts/test/utils/MockUniswapV3Router.sol @@ -14,6 +14,8 @@ contract MockUniswapV3Router is ISwapRouter { uint256 public multiplier; + event SlippageError(); + constructor() { // Useless logic not to use an abstract contract DEPLOYER = msg.sender; @@ -29,9 +31,15 @@ contract MockUniswapV3Router is ISwapRouter { override returns (uint256 amountOut) { - IERC20(params.tokenIn).safeTransferFrom(msg.sender, address(this), params.amountIn); amountOut = (params.amountIn * multiplier) / PRECISION_MULTIPLIER; - IERC20(params.tokenOut).transfer(msg.sender, amountOut); + + if (amountOut < params.amountOutMinimum) { + emit SlippageError(); + amountOut = 0; + } else { + IERC20(params.tokenIn).safeTransferFrom(msg.sender, address(this), params.amountIn); + IERC20(params.tokenOut).transfer(msg.sender, amountOut); + } return amountOut; } diff --git a/test/aggregatorFeeSharingWithUniswapV3.test.ts b/test/aggregatorFeeSharingWithUniswapV3.test.ts index 044887b..4417deb 100644 --- a/test/aggregatorFeeSharingWithUniswapV3.test.ts +++ b/test/aggregatorFeeSharingWithUniswapV3.test.ts @@ -110,6 +110,7 @@ describe("AggregatorFeeSharing", () => { const MockUniswapV3Router = await ethers.getContractFactory("MockUniswapV3Router"); uniswapRouter = await MockUniswapV3Router.deploy(); + // 1 WETH is sold for 2 LOOKS await uniswapRouter.connect(admin).setMultiplier("20000"); // Transfer 2,250 LOOKS to mock router @@ -117,6 +118,7 @@ describe("AggregatorFeeSharing", () => { const AggregatorFeeSharingWithUniswapV3 = await ethers.getContractFactory("AggregatorFeeSharingWithUniswapV3"); aggregator = await AggregatorFeeSharingWithUniswapV3.deploy(feeSharingSystem.address, uniswapRouter.address); + await aggregator.deployed(); }); describe("#1 - Regular user/admin interactions", async () => { @@ -286,7 +288,7 @@ describe("AggregatorFeeSharing", () => { it("Harvest doesn't trigger reinvesting if amount received is less than 1 LOOKS", async () => { const [user1, user2, user3] = [accounts[1], accounts[2], accounts[3]]; await setupUsers(feeSharingSystem, looksRareToken, aggregator, admin, [user1, user2, user3]); - // 1 WETH --> 1 LOOKS + // 1 WETH is sold for 1 LOOKS await uniswapRouter.setMultiplier("10000"); await aggregator.connect(admin).updateThresholdAmount(parseEther("0.999")); await rewardToken.connect(admin).transfer(aggregator.address, parseEther("0.999")); @@ -297,6 +299,34 @@ describe("AggregatorFeeSharing", () => { // Amount is lower than threshold to trigger the deposit await expect(tx).not.to.emit(feeSharingSystem, "Deposit"); }); + + it("Slippage protections work as expected", async () => { + const [user1, user2, user3] = [accounts[1], accounts[2], accounts[3]]; + await setupUsers(feeSharingSystem, looksRareToken, aggregator, admin, [user1, user2, user3]); + + // 1 WETH must be sold at least for 100 LOOKS + await aggregator.connect(admin).updateMaxPriceOfLOOKSInWETH(parseEther("0.01")); + + // 1 WETH is sold for 100 LOOKS + await uniswapRouter.setMultiplier("1000000"); + + // Transfer 1 LOOKS + await rewardToken.connect(admin).transfer(aggregator.address, parseEther("1")); + let tx = await aggregator.connect(admin).harvestAndSellAndCompound(); + + // Amount is same as threshold... it doesn't trigger the error + await expect(tx).not.to.emit(uniswapRouter, "SlippageError"); + + // 1 WETH is now sold for 99.999 LOOKS + await uniswapRouter.setMultiplier("999999"); + + // Transfer 1 LOOKS + await rewardToken.connect(admin).transfer(aggregator.address, parseEther("1")); + tx = await aggregator.connect(admin).harvestAndSellAndCompound(); + + // Amount is lower than threshold to trigger the deposit + await expect(tx).to.emit(uniswapRouter, "SlippageError"); + }); }); describe("#2 - Revertions work as expected", async () => { @@ -438,11 +468,11 @@ describe("AggregatorFeeSharing", () => { }); it("Owner can update minPriceLOOKSInWETH", async () => { - // 1 LOOKS is at least equal to 0.01 WETH - // 1 WETH is at most equal to 100 LOOKS - const tx = await aggregator.connect(admin).updateMinPriceOfLOOKSInWETH(parseEther("0.01")); - await expect(tx).to.emit(aggregator, "NewMinimumPriceOfLOOKSInWETH").withArgs(parseEther("0.01")); - assert.deepEqual(await aggregator.minPriceLOOKSInWETH(), parseEther("0.01")); + // 1 LOOKS is at most equal to 0.01 WETH + // 1 WETH is at least equal to 100 LOOKS + const tx = await aggregator.connect(admin).updateMaxPriceOfLOOKSInWETH(parseEther("0.01")); + await expect(tx).to.emit(aggregator, "NewMaximumPriceOfLOOKSInWETH").withArgs(parseEther("0.01")); + assert.deepEqual(await aggregator.maxPriceOfLOOKSInWETH(), parseEther("0.01")); }); it("Only owner can call functions for onlyOwner", async () => { @@ -458,7 +488,7 @@ describe("AggregatorFeeSharing", () => { await expect(aggregator.connect(user1).updateThresholdAmount("10")).to.be.revertedWith( "Ownable: caller is not the owner" ); - await expect(aggregator.connect(user1).updateMinPriceOfLOOKSInWETH(parseEther("0.01"))).to.be.revertedWith( + await expect(aggregator.connect(user1).updateMaxPriceOfLOOKSInWETH(parseEther("0.01"))).to.be.revertedWith( "Ownable: caller is not the owner" ); await expect(aggregator.connect(user1).pause()).to.be.revertedWith("Ownable: caller is not the owner"); From ad0e4a7051e84dd944b6a5acd34529bfb7ed67f5 Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Sun, 27 Mar 2022 15:37:34 +0900 Subject: [PATCH 6/9] config: Adjusted Foundry fuzzing runs to 1000 --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index 44e516f..ce0ac69 100644 --- a/foundry.toml +++ b/foundry.toml @@ -14,7 +14,7 @@ ffi = false force = false fuzz_max_global_rejects = 65536 fuzz_max_local_rejects = 1024 -fuzz_runs = 256 +fuzz_runs = 1000 gas_limit = 9223372036854775807 gas_price = 0 gas_reports = ['*'] From 92b939b45f1e4bf035763a05ec5e253099e4250a Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Sun, 27 Mar 2022 15:43:17 +0900 Subject: [PATCH 7/9] refactor: Adjusted event names --- contracts/AggregatorFeeSharingWithUniswapV3.sol | 4 ++-- test/aggregatorFeeSharingWithUniswapV3.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/AggregatorFeeSharingWithUniswapV3.sol b/contracts/AggregatorFeeSharingWithUniswapV3.sol index 310456d..053376c 100644 --- a/contracts/AggregatorFeeSharingWithUniswapV3.sol +++ b/contracts/AggregatorFeeSharingWithUniswapV3.sol @@ -67,7 +67,7 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard event HarvestStart(); event HarvestStop(); event NewHarvestBufferBlocks(uint256 harvestBufferBlocks); - event NewMaximumPriceOfLOOKSInWETH(uint256 maxPriceLOOKSInWETH); + event NewMaximumPriceLOOKSInWETH(uint256 maxPriceLOOKSInWETH); event NewThresholdAmount(uint256 thresholdAmount); event NewTradingFeeUniswapV3(uint24 tradingFeeUniswapV3); event Withdraw(address indexed user, uint256 amount); @@ -217,7 +217,7 @@ contract AggregatorFeeSharingWithUniswapV3 is Ownable, Pausable, ReentrancyGuard function updateMaxPriceOfLOOKSInWETH(uint256 _newMaxPriceLOOKSInWETH) external onlyOwner { maxPriceLOOKSInWETH = _newMaxPriceLOOKSInWETH; - emit NewMaximumPriceOfLOOKSInWETH(_newMaxPriceLOOKSInWETH); + emit NewMaximumPriceLOOKSInWETH(_newMaxPriceLOOKSInWETH); } /** diff --git a/test/aggregatorFeeSharingWithUniswapV3.test.ts b/test/aggregatorFeeSharingWithUniswapV3.test.ts index 4417deb..95a2477 100644 --- a/test/aggregatorFeeSharingWithUniswapV3.test.ts +++ b/test/aggregatorFeeSharingWithUniswapV3.test.ts @@ -471,8 +471,8 @@ describe("AggregatorFeeSharing", () => { // 1 LOOKS is at most equal to 0.01 WETH // 1 WETH is at least equal to 100 LOOKS const tx = await aggregator.connect(admin).updateMaxPriceOfLOOKSInWETH(parseEther("0.01")); - await expect(tx).to.emit(aggregator, "NewMaximumPriceOfLOOKSInWETH").withArgs(parseEther("0.01")); - assert.deepEqual(await aggregator.maxPriceOfLOOKSInWETH(), parseEther("0.01")); + await expect(tx).to.emit(aggregator, "NewMaximumPriceLOOKSInWETH").withArgs(parseEther("0.01")); + assert.deepEqual(await aggregator.maxPriceLOOKSInWETH(), parseEther("0.01")); }); it("Only owner can call functions for onlyOwner", async () => { From 264d593501c3eaa4666e889ffadeaedb3be96fe8 Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:29:07 +0900 Subject: [PATCH 8/9] test: Added Solidity scenario tests --- .../AggregatorFeeSharingWithUniswapV3.t.sol | 256 +++++++++++++++++- contracts/test/TestHelpers.sol | 20 +- 2 files changed, 266 insertions(+), 10 deletions(-) diff --git a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol index 2bddf4e..0c61a70 100644 --- a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol +++ b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.7; -import {DSTest} from "../../lib/ds-test/src/test.sol"; - import {LooksRareToken} from "../LooksRareToken.sol"; import {TokenDistributor} from "../TokenDistributor.sol"; import {FeeSharingSystem} from "../FeeSharingSystem.sol"; +import {FeeSharingSetter} from "../FeeSharingSetter.sol"; + import {AggregatorFeeSharingWithUniswapV3} from "../AggregatorFeeSharingWithUniswapV3.sol"; import {MockERC20} from "./utils/MockERC20.sol"; import {MockUniswapV3Router} from "./utils/MockUniswapV3Router.sol"; @@ -20,10 +20,11 @@ abstract contract TestParameters { uint256 internal _START_BLOCK; } -contract AggregatorTest is DSTest, TestParameters, TestHelpers { +contract AggregatorTest is TestParameters, TestHelpers { LooksRareToken public looksRareToken; TokenDistributor public tokenDistributor; FeeSharingSystem public feeSharingSystem; + FeeSharingSetter public feeSharingSetter; AggregatorFeeSharingWithUniswapV3 public aggregatorFeeSharingWithUniswapV3; MockUniswapV3Router public uniswapRouter; MockERC20 public rewardToken; @@ -79,17 +80,18 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { address(tokenDistributor) ); - // 6. Aggregator deployment + // 6. FeeSharingSetter deployment (distribution period is set at 100 blocks) + feeSharingSetter = new FeeSharingSetter(address(feeSharingSystem), 30, 1000, 100); + feeSharingSetter.grantRole(feeSharingSetter.OPERATOR_ROLE(), feeSharingSystem.owner()); + feeSharingSystem.transferOwnership(address(feeSharingSetter)); + + // 7. Aggregator deployment aggregatorFeeSharingWithUniswapV3 = new AggregatorFeeSharingWithUniswapV3( address(feeSharingSystem), address(uniswapRouter) ); - aggregatorFeeSharingWithUniswapV3.startHarvest(); - aggregatorFeeSharingWithUniswapV3.updateThresholdAmount(_parseEtherWithFloating(5, 1)); - aggregatorFeeSharingWithUniswapV3.updateHarvestBufferBlocks(10); - - // 7. Distribute LOOKS (from the premint) to user accounts + // 8. Distribute LOOKS (from the premint) to user accounts address[4] memory users = [user1, user2, user3, user4]; for (uint256 i = 0; i < users.length; i++) { @@ -114,15 +116,24 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { uint256 currentBalanceUser1 = looksRareToken.balanceOf(user1); assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), _parseEther(100)); assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), _parseEther(100)); + assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharePriceInPrimeShare(), 1e18); // Time travel by 1 block cheats.roll(_START_BLOCK + 1); assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), _parseEther(130)); + assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharePriceInLOOKS(), _parseEtherWithFloating(13, 1)); aggregatorFeeSharingWithUniswapV3.withdrawAll(); // 200 LOOKS + 130 LOOKS = 330 LOOKS assertEq(looksRareToken.balanceOf(user1), _parseEther(130) + currentBalanceUser1); assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), _parseEther(0)); + + aggregatorFeeSharingWithUniswapV3.deposit(_parseEther(100)); + aggregatorFeeSharingWithUniswapV3.deposit(_parseEther(100)); + + cheats.roll(_START_BLOCK + 50); + assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), _parseEther(200)); + assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), _parseEther(200 + 49 * 30)); } function testSameBlockDepositAndWithdraw(uint8 x, uint16 numberBlocks) public asPrankedUser(user1) { @@ -148,4 +159,231 @@ contract AggregatorTest is DSTest, TestParameters, TestHelpers { aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), aggregatorFeeSharingWithUniswapV3.userInfo(user2)); } + + function testScenarioWithoutRouter() public { + uint256 amountDeposit = _parseEther(100); + cheats.roll(_START_BLOCK + 5); + + /** 1. Initial deposits at startBlock + 5 + */ + address[4] memory users = [user1, user2, user3, user4]; + for (uint256 i = 0; i < users.length; i++) { + cheats.prank(users[i]); + aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); + } + assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), aggregatorFeeSharingWithUniswapV3.userInfo(user2)); + + /** 2. Time travel to startBlock + 20 (15 blocks later) + * User1 withdraws funds + */ + cheats.roll(_START_BLOCK + 20); + + uint256 currentBalanceUser = looksRareToken.balanceOf(user1); + cheats.prank(user1); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // 15 blocks at 30 LOOKS/block = 450 LOOKS + // 450 / 4 = 112.5 LOOKS for user + assertEq( + looksRareToken.balanceOf(user1), + currentBalanceUser + _parseEtherWithFloating(1125, 1) + amountDeposit + ); + + /** 3. Time travel to startBlock + 100 (80 blocks later) + * User2 checks the value of her shares + */ + cheats.roll(_START_BLOCK + 100); + + // 80 blocks at 30 LOOKS/blocks = 2400 LOOKS + // 800 LOOKS for user + // Total value of the shares = 800 LOOKS + 112.5 LOOKS + 100 LOOKS = 1012.5 + // @dev To deal with minor precision losses due to division, we look at the boundaries + assertQuasiEq( + aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user2), + _parseEtherWithFloating(10125, 1) + ); + + /** 4. Time travel to startBlock + 170 (70 blocks later) + * User2 withdraws all + */ + cheats.roll(_START_BLOCK + 170); + currentBalanceUser = looksRareToken.balanceOf(user2); + cheats.prank(user2); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // Previous value of shares of user2 = 1012.5 LOOKS (see above) + // 70 blocks at 15 LOOKS/block = 1050 LOOKS + // 1050 LOOKS / 3 = 350 LOOKS for user + // Total = 1362.5 LOOKS + assertQuasiEq(looksRareToken.balanceOf(user2), currentBalanceUser + _parseEtherWithFloating(13625, 1)); + + /** 5. Time travel to startBlock + 400 (230 blocks later) + * User3 withdraws all + */ + cheats.roll(_START_BLOCK + 400); + currentBalanceUser = looksRareToken.balanceOf(user3); + cheats.prank(user3); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // Previous value of shares of user2 = 1362.5 LOOKS (see above) + // 30 blocks at 15 LOOKS/block = 450 LOOKS + // 100 blocks at 7.5 LOOKS/block = 750 LOOKS + // 100 blocks at 3.75 LOOKS/block = 375 LOOKS + // 1575 LOOKS / 2 = 787.5 LOOKS + // Total = 2150 LOOKS + assertQuasiEq(looksRareToken.balanceOf(user3), currentBalanceUser + _parseEther(2150)); + + /** 6. Time travel to startBlock + 400 (230 blocks later) + * User4 withdraws all + */ + cheats.roll(_START_BLOCK + 450); + currentBalanceUser = looksRareToken.balanceOf(user4); + cheats.prank(user4); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // Should be same as user3 since LOOKS distribution is stopped + assertQuasiEq(looksRareToken.balanceOf(user4), currentBalanceUser + _parseEther(2150)); + + // Verify the final supply is equal to the cap - supply not minted (for first 5 blocks) + assertEq(looksRareToken.totalSupply(), _parseEther(_CAP) - _parseEther(500)); + } + + function testScenarioWithRouter() public { + /** 0. Initial set up + */ + cheats.roll(_START_BLOCK); + + // Add 1000 WETH for distribution for next 100 blocks (10 WETH per block) + rewardToken.mint(address(feeSharingSetter), _parseEther(1000)); + feeSharingSetter.updateRewards(); + feeSharingSetter.setNewRewardDurationInBlocks(300); // This will be adjusted for the second period + assertEq(feeSharingSystem.currentRewardPerBlock(), _parseEther(10)); + + aggregatorFeeSharingWithUniswapV3.startHarvest(); + aggregatorFeeSharingWithUniswapV3.updateThresholdAmount(_parseEtherWithFloating(5, 1)); // 1 WETH + aggregatorFeeSharingWithUniswapV3.updateHarvestBufferBlocks(20); + uniswapRouter.setMultiplier(15000); // 1 WETH = 1.5 LOOKS + aggregatorFeeSharingWithUniswapV3.updateMaxPriceOfLOOKSInWETH(_parseEtherWithFloating(667, 3)); // 1 LOOKS = 0.667 WETH + + // Transfer 4000 LOOKS to mock router + cheats.prank(_PREMINT_RECEIVER); + looksRareToken.transfer(address(uniswapRouter), _parseEther(4000)); + + uint256 amountDeposit = _parseEther(100); + cheats.roll(_START_BLOCK + 5); + + /** 1. Initial deposits at startBlock + 5 + */ + address[4] memory users = [user1, user2, user3, user4]; + for (uint256 i = 0; i < users.length; i++) { + cheats.prank(users[i]); + aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); + } + + /** 2. Time travel to startBlock + 20 (15 blocks later) + * User1 withdraws funds + */ + cheats.roll(_START_BLOCK + 20); + + uint256 currentBalanceUser = looksRareToken.balanceOf(user1); + cheats.prank(user1); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // 15 blocks at 30 LOOKS/block = 450 LOOKS + // + 150 WETH sold at 1 WETH = 1.5 LOOKS --> 225 LOOKS + // 675 / 4 = 168.75 LOOKS for user + // @dev 50 WETH are lost to the fee sharing system contract since nobody was staking for the first 5 blocks + assertEq( + looksRareToken.balanceOf(user1), + currentBalanceUser + _parseEtherWithFloating(16875, 2) + amountDeposit + ); + + // 400 prime shares --> (450 + 225 + 4 * 100) = 1075 + // 1 prime share is worth 1075 / 400 = 2.6875 + assertEq(aggregatorFeeSharingWithUniswapV3.calculateSharePriceInLOOKS(), _parseEtherWithFloating(26875, 4)); + + // 400 shares --> (450 + 4 * 100) = 850 + // 1 share is worth 850 / 400 = 2.125 + assertEq(feeSharingSystem.calculateSharePriceInLOOKS(), _parseEtherWithFloating(2125, 3)); + + // 1 prime share is worth ~ 1.264 shares + assertEq( + aggregatorFeeSharingWithUniswapV3.calculateSharePriceInPrimeShare(), + (aggregatorFeeSharingWithUniswapV3.calculateSharePriceInLOOKS() * 1e18) / + feeSharingSystem.calculateSharePriceInLOOKS() + ); + + // User1 decides to re-deposit the exact same amount as the one earned + cheats.prank(user1); + aggregatorFeeSharingWithUniswapV3.deposit(_parseEtherWithFloating(16875, 2) + amountDeposit); + + assertEq( + aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user1), + aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user2) + ); + + /** 3. Time travel to startBlock + 100 (80 blocks later) + * User1 withdraws + */ + cheats.roll(_START_BLOCK + 100); + assertEq(feeSharingSystem.periodEndBlock(), _START_BLOCK + 100); + + currentBalanceUser = looksRareToken.balanceOf(user1); + cheats.prank(user1); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + + // Previous value of shares of user1 = 168.75 LOOKS (see above) + // 80 blocks at 30 LOOKS/block = 2400 LOOKS + // + 800 WETH sold at 1 WETH = 1.5 LOOKS --> 1200 LOOKS + // 3600 / 4 = 900 LOOKS for user + assertQuasiEq( + looksRareToken.balanceOf(user1), + currentBalanceUser + _parseEtherWithFloating(106875, 2) + amountDeposit + ); + + assertEq(feeSharingSystem.lastRewardBlock(), _START_BLOCK + 100); + + /** 4. Start of new reward period over 300 blocks + */ + + // Add 1500 WETH for distribution for next 300 blocks (5 WETH per block) + cheats.roll(_START_BLOCK + 101); + rewardToken.mint(address(feeSharingSetter), _parseEther(1500)); + feeSharingSetter.updateRewards(); + assertEq(feeSharingSystem.currentRewardPerBlock(), _parseEther(5)); + + /** 5. Time travel to the end of the LOOKS staking/fee-sharing period + * All users withdraw their funds + */ + cheats.roll(_START_BLOCK + 401); + + // @dev currentBalanceUser is same for user2/user3/user4 + currentBalanceUser = looksRareToken.balanceOf(user2); + + // All users (except user1) withdraw + for (uint256 i = 1; i < users.length; i++) { + cheats.prank(users[i]); + aggregatorFeeSharingWithUniswapV3.withdrawAll(); + } + + // Previous value of shares of user1 = 1068.75 LOOKS (see above) + // 100 blocks at 15 LOOKS/block = 1500 LOOKS + // 100 blocks at 7.5 LOOKS/block = 750 LOOKS + // 100 blocks at 3.75 LOOKS/block = 375 LOOKS + // 2625 LOOKS + // 1500 WETH sold for 1 WETH = 1.5 LOOKS --> 2250 LOOKS + // Total = 4875 LOOKS --> 1625 LOOKS per user + assertQuasiEq( + looksRareToken.balanceOf(user2), + _parseEther(1625) + _parseEtherWithFloating(106875, 2) + amountDeposit + currentBalanceUser + ); + assertQuasiEq(looksRareToken.balanceOf(user2), looksRareToken.balanceOf(user3)); + assertQuasiEq(looksRareToken.balanceOf(user3), looksRareToken.balanceOf(user4)); + + // There should be around 50 WETH left in the fee sharing contract (for the first 5 blocks without user staking) + assertQuasiEq(rewardToken.balanceOf(address(feeSharingSystem)), _parseEther(50)); + + // Verify the final supply is equal to the cap - supply not minted (for first 5 blocks) + assertEq(looksRareToken.totalSupply(), _parseEther(_CAP) - _parseEther(500)); + } } diff --git a/contracts/test/TestHelpers.sol b/contracts/test/TestHelpers.sol index b62c8d4..d8bf103 100644 --- a/contracts/test/TestHelpers.sol +++ b/contracts/test/TestHelpers.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.0; import {ICheatCodes} from "./ICheatCodes.sol"; +import {DSTest} from "../../lib/ds-test/src/test.sol"; -abstract contract TestHelpers { +abstract contract TestHelpers is DSTest { ICheatCodes public cheats = ICheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); address public user1 = address(1); @@ -22,6 +23,23 @@ abstract contract TestHelpers { cheats.stopPrank(); } + function assertQuasiEq(uint256 a, uint256 b) public { + require(a >= 1e18 || b >= 1e18, "Error: a & b must be > 1e18"); + + // 0.000001 % precision tolerance + uint256 PRECISION_LOSS = 1e9; + + if (a == b) { + assertEq(a, b); + } else if (a > b) { + assertGt(a, b); + assertLt(a - PRECISION_LOSS, b); + } else if (a < b) { + assertGt(a, b - PRECISION_LOSS); + assertLt(a, b); + } + } + function _parseEther(uint256 value) internal pure returns (uint256) { return value * 1e18; } From 7cdfe5e8db707fa392386fc8f4f68769bb863d8b Mon Sep 17 00:00:00 2001 From: 0xJurassicPunk <90075915+0xJurassicPunk@users.noreply.github.com> Date: Mon, 28 Mar 2022 15:36:42 +0900 Subject: [PATCH 9/9] test: Adjusted tests --- .../AggregatorFeeSharingWithUniswapV3.t.sol | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol index 0c61a70..4c9c1a1 100644 --- a/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol +++ b/contracts/test/AggregatorFeeSharingWithUniswapV3.t.sol @@ -30,10 +30,10 @@ contract AggregatorTest is TestParameters, TestHelpers { MockERC20 public rewardToken; function setUp() public { - // 0. Mock WETH + // 0. Mock WETH deployment rewardToken = new MockERC20("WETH", "Wrapped Ether"); - // 1. Mock Uniswap v3 Router + // 1. Mock UniswapV3Router deployment uniswapRouter = new MockUniswapV3Router(); // 2. LooksRareToken deployment @@ -80,7 +80,7 @@ contract AggregatorTest is TestParameters, TestHelpers { address(tokenDistributor) ); - // 6. FeeSharingSetter deployment (distribution period is set at 100 blocks) + // 6. FeeSharingSetter deployment (w/ distribution period is set at 100 blocks) feeSharingSetter = new FeeSharingSetter(address(feeSharingSystem), 30, 1000, 100); feeSharingSetter.grantRole(feeSharingSetter.OPERATOR_ROLE(), feeSharingSystem.owner()); feeSharingSystem.transferOwnership(address(feeSharingSetter)); @@ -164,7 +164,7 @@ contract AggregatorTest is TestParameters, TestHelpers { uint256 amountDeposit = _parseEther(100); cheats.roll(_START_BLOCK + 5); - /** 1. Initial deposits at startBlock + 5 + /** 1. Initial deposits at startBlock + 5 */ address[4] memory users = [user1, user2, user3, user4]; for (uint256 i = 0; i < users.length; i++) { @@ -173,7 +173,7 @@ contract AggregatorTest is TestParameters, TestHelpers { } assertEq(aggregatorFeeSharingWithUniswapV3.userInfo(user1), aggregatorFeeSharingWithUniswapV3.userInfo(user2)); - /** 2. Time travel to startBlock + 20 (15 blocks later) + /** 2. Time travel to startBlock + 20 (15 blocks later) * User1 withdraws funds */ cheats.roll(_START_BLOCK + 20); @@ -189,21 +189,21 @@ contract AggregatorTest is TestParameters, TestHelpers { currentBalanceUser + _parseEtherWithFloating(1125, 1) + amountDeposit ); - /** 3. Time travel to startBlock + 100 (80 blocks later) + /** 3. Time travel to startBlock + 100 (80 blocks later) * User2 checks the value of her shares */ cheats.roll(_START_BLOCK + 100); // 80 blocks at 30 LOOKS/blocks = 2400 LOOKS // 800 LOOKS for user - // Total value of the shares = 800 LOOKS + 112.5 LOOKS + 100 LOOKS = 1012.5 + // Total value of the shares = 800 LOOKS + 112.5 LOOKS + 100 LOOKS = 1012.5 LOOKS // @dev To deal with minor precision losses due to division, we look at the boundaries assertQuasiEq( aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user2), _parseEtherWithFloating(10125, 1) ); - /** 4. Time travel to startBlock + 170 (70 blocks later) + /** 4. Time travel to startBlock + 170 (70 blocks later) * User2 withdraws all */ cheats.roll(_START_BLOCK + 170); @@ -217,7 +217,7 @@ contract AggregatorTest is TestParameters, TestHelpers { // Total = 1362.5 LOOKS assertQuasiEq(looksRareToken.balanceOf(user2), currentBalanceUser + _parseEtherWithFloating(13625, 1)); - /** 5. Time travel to startBlock + 400 (230 blocks later) + /** 5. Time travel to startBlock + 400 (230 blocks later) * User3 withdraws all */ cheats.roll(_START_BLOCK + 400); @@ -233,7 +233,7 @@ contract AggregatorTest is TestParameters, TestHelpers { // Total = 2150 LOOKS assertQuasiEq(looksRareToken.balanceOf(user3), currentBalanceUser + _parseEther(2150)); - /** 6. Time travel to startBlock + 400 (230 blocks later) + /** 6. Time travel to startBlock + 400 (230 blocks later) * User4 withdraws all */ cheats.roll(_START_BLOCK + 450); @@ -244,12 +244,12 @@ contract AggregatorTest is TestParameters, TestHelpers { // Should be same as user3 since LOOKS distribution is stopped assertQuasiEq(looksRareToken.balanceOf(user4), currentBalanceUser + _parseEther(2150)); - // Verify the final supply is equal to the cap - supply not minted (for first 5 blocks) + // Verify the final total supply is equal to the cap - supply not minted (for first 5 blocks) assertEq(looksRareToken.totalSupply(), _parseEther(_CAP) - _parseEther(500)); } function testScenarioWithRouter() public { - /** 0. Initial set up + /** 0. Initial set up */ cheats.roll(_START_BLOCK); @@ -272,7 +272,7 @@ contract AggregatorTest is TestParameters, TestHelpers { uint256 amountDeposit = _parseEther(100); cheats.roll(_START_BLOCK + 5); - /** 1. Initial deposits at startBlock + 5 + /** 1. Initial deposits at startBlock + 5 */ address[4] memory users = [user1, user2, user3, user4]; for (uint256 i = 0; i < users.length; i++) { @@ -280,7 +280,7 @@ contract AggregatorTest is TestParameters, TestHelpers { aggregatorFeeSharingWithUniswapV3.deposit(amountDeposit); } - /** 2. Time travel to startBlock + 20 (15 blocks later) + /** 2. Time travel to startBlock + 20 (15 blocks later) * User1 withdraws funds */ cheats.roll(_START_BLOCK + 20); @@ -292,7 +292,7 @@ contract AggregatorTest is TestParameters, TestHelpers { // 15 blocks at 30 LOOKS/block = 450 LOOKS // + 150 WETH sold at 1 WETH = 1.5 LOOKS --> 225 LOOKS // 675 / 4 = 168.75 LOOKS for user - // @dev 50 WETH are lost to the fee sharing system contract since nobody was staking for the first 5 blocks + // @dev 50 WETH are lost to the fee sharing system contract since no user was staking for the first 5 blocks assertEq( looksRareToken.balanceOf(user1), currentBalanceUser + _parseEtherWithFloating(16875, 2) + amountDeposit @@ -322,7 +322,7 @@ contract AggregatorTest is TestParameters, TestHelpers { aggregatorFeeSharingWithUniswapV3.calculateSharesValueInLOOKS(user2) ); - /** 3. Time travel to startBlock + 100 (80 blocks later) + /** 3. Time travel to startBlock + 100 (80 blocks later) * User1 withdraws */ cheats.roll(_START_BLOCK + 100); @@ -343,7 +343,7 @@ contract AggregatorTest is TestParameters, TestHelpers { assertEq(feeSharingSystem.lastRewardBlock(), _START_BLOCK + 100); - /** 4. Start of new reward period over 300 blocks + /** 4. Start of new reward period over 300 blocks */ // Add 1500 WETH for distribution for next 300 blocks (5 WETH per block) @@ -352,8 +352,8 @@ contract AggregatorTest is TestParameters, TestHelpers { feeSharingSetter.updateRewards(); assertEq(feeSharingSystem.currentRewardPerBlock(), _parseEther(5)); - /** 5. Time travel to the end of the LOOKS staking/fee-sharing period - * All users withdraw their funds + /** 5. Time travel to the end of the LOOKS staking/fee-sharing period + * All 3 users withdraw their funds */ cheats.roll(_START_BLOCK + 401); @@ -383,7 +383,7 @@ contract AggregatorTest is TestParameters, TestHelpers { // There should be around 50 WETH left in the fee sharing contract (for the first 5 blocks without user staking) assertQuasiEq(rewardToken.balanceOf(address(feeSharingSystem)), _parseEther(50)); - // Verify the final supply is equal to the cap - supply not minted (for first 5 blocks) + // Verify the final total supply is equal to the cap - supply not minted (for first 5 blocks) assertEq(looksRareToken.totalSupply(), _parseEther(_CAP) - _parseEther(500)); } }