From 3a3abb9ac64a14efefaaef4c373ce900322821c9 Mon Sep 17 00:00:00 2001 From: turupawn Date: Wed, 28 Sep 2022 14:48:31 -0600 Subject: [PATCH] actions --- .env.example | 1 + .github/workflows/unit-tests.yml | 25 +++ contracts/ERC20/BibliotecaInterfaces.sol | 12 ++ contracts/ERC20/UniswapV3Interfaces.sol | 125 +++++++++++++ hardhat.config.js | 10 +- package-lock.json | 13 +- package.json | 3 + test/Lock.js | 126 ------------- test/UniswapV3FeeToken.js | 221 +++++++++++++++++++++++ 9 files changed, 405 insertions(+), 131 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/unit-tests.yml create mode 100644 contracts/ERC20/BibliotecaInterfaces.sol delete mode 100644 test/Lock.js create mode 100644 test/UniswapV3FeeToken.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..522dd96 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOURAPIHERE \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..d7880c4 --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,25 @@ +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [14.x] + + steps: + - uses: actions/checkout@v3 + - name: Waffle Unit Tests + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: export MAINNET_RPC_URL=${{ secrets.MAINNET_RPC_URL }}; npx hardhat test \ No newline at end of file diff --git a/contracts/ERC20/BibliotecaInterfaces.sol b/contracts/ERC20/BibliotecaInterfaces.sol new file mode 100644 index 0000000..fbc172a --- /dev/null +++ b/contracts/ERC20/BibliotecaInterfaces.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +// Filosofía Código Contracts based on OpenZeppelin (token/ERC20/ERC20.sol) + +pragma solidity ^0.8.0; + +import "./ERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint amount) external; +} \ No newline at end of file diff --git a/contracts/ERC20/UniswapV3Interfaces.sol b/contracts/ERC20/UniswapV3Interfaces.sol index 8237a1b..7679f66 100644 --- a/contracts/ERC20/UniswapV3Interfaces.sol +++ b/contracts/ERC20/UniswapV3Interfaces.sol @@ -3,11 +3,136 @@ pragma solidity ^0.8.0; +struct ExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + address recipient; + uint deadline; + uint amountIn; + uint amountOutMinimum; + uint160 sqrtPriceLimitX96; +} + +interface IUniswapV3Router { + function exactInputSingle(ExactInputSingleParams calldata params) + external + payable + returns (uint amountOut); +} + interface INonfungiblePositionManager { + function createAndInitializePoolIfNecessary( address token0, address token1, uint24 fee, uint160 sqrtPriceX96 ) external payable returns (address pool); + + struct MintParams { + address token0; + address token1; + uint24 fee; + int24 tickLower; + int24 tickUpper; + uint amount0Desired; + uint amount1Desired; + uint amount0Min; + uint amount1Min; + address recipient; + uint deadline; + } + + function mint(MintParams calldata params) + external + payable + returns ( + uint tokenId, + uint128 liquidity, + uint amount0, + uint amount1 + ); + + struct IncreaseLiquidityParams { + uint tokenId; + uint amount0Desired; + uint amount1Desired; + uint amount0Min; + uint amount1Min; + uint deadline; + } + + function increaseLiquidity(IncreaseLiquidityParams calldata params) + external + payable + returns ( + uint128 liquidity, + uint amount0, + uint amount1 + ); + + struct DecreaseLiquidityParams { + uint tokenId; + uint128 liquidity; + uint amount0Min; + uint amount1Min; + uint deadline; + } + + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + returns (uint amount0, uint amount1); + + struct CollectParams { + uint tokenId; + address recipient; + uint128 amount0Max; + uint128 amount1Max; + } + + function collect(CollectParams calldata params) + external + payable + returns (uint amount0, uint amount1); +} + +abstract contract IUniswapV3Pool +{ + struct Slot0 { + // the current price + uint160 sqrtPriceX96; + // the current tick + int24 tick; + // the most-recently updated index of the observations array + uint16 observationIndex; + // the current maximum number of observations that are being stored + uint16 observationCardinality; + // the next maximum number of observations to store, triggered in observations.write + uint16 observationCardinalityNext; + // the current protocol fee as a percentage of the swap fee taken on withdrawal + // represented as an integer denominator (1/x)% + uint8 feeProtocol; + // whether the pool is locked + bool unlocked; + } + //Slot0 public slot0; + + uint24 public fee; + //int24 public tickSpacing; + + + function slot0( + ) external virtual view returns + ( + uint160 sqrtPriceX96, + int24 tick, + uint16 observationIndex, + uint16 observationCardinality, + uint16 observationCardinalityNext, + uint8 feeProtocol, + bool unlocked); + + function tickSpacing() external virtual view returns (int24); } \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index 86913e7..bc6a10c 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -1,6 +1,14 @@ require("@nomicfoundation/hardhat-toolbox"); +require('dotenv').config() /** @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.17", -}; + networks: { + hardhat: { + forking: { + url: process.env.RPC_URL, + } + } + } +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 490297e..c3ef58e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,8 @@ { - "name": "hardhat-project", - "requires": true, + "name": "biblioteca", + "version": "0.0.8", "lockfileVersion": 1, + "requires": true, "dependencies": { "@ethersproject/abi": { "version": "5.7.0", @@ -2056,6 +2057,11 @@ "path-type": "^4.0.0" } }, + "dotenv": { + "version": "16.0.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.2.tgz", + "integrity": "sha512-JvpYKUmzQhYoIFgK2MOnF3bciIZoItIIoryihy0rIA+H4Jy0FmgyKYAHCTN98P5ybGSJcIFbh6QKeJdtZd1qhA==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -6203,6 +6209,5 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true } - }, - "version": "0.0.8" + } } diff --git a/package.json b/package.json index bf71a8d..8d37dbf 100644 --- a/package.json +++ b/package.json @@ -29,5 +29,8 @@ "hardhat-gas-reporter": "^1.0.9", "solidity-coverage": "^0.8.2", "typechain": "^8.1.0" + }, + "dependencies": { + "dotenv": "^16.0.2" } } diff --git a/test/Lock.js b/test/Lock.js deleted file mode 100644 index 6a161e6..0000000 --- a/test/Lock.js +++ /dev/null @@ -1,126 +0,0 @@ -const { - time, - loadFixture, -} = require("@nomicfoundation/hardhat-network-helpers"); -const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); -const { expect } = require("chai"); - -describe("Lock", function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. - async function deployOneYearLockFixture() { - const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60; - const ONE_GWEI = 1_000_000_000; - - const lockedAmount = ONE_GWEI; - const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS; - - // Contracts are deployed using the first signer/account by default - const [owner, otherAccount] = await ethers.getSigners(); - - const Lock = await ethers.getContractFactory("Lock"); - const lock = await Lock.deploy(unlockTime, { value: lockedAmount }); - - return { lock, unlockTime, lockedAmount, owner, otherAccount }; - } - - describe("Deployment", function () { - it("Should set the right unlockTime", async function () { - const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.unlockTime()).to.equal(unlockTime); - }); - - it("Should set the right owner", async function () { - const { lock, owner } = await loadFixture(deployOneYearLockFixture); - - expect(await lock.owner()).to.equal(owner.address); - }); - - it("Should receive and store the funds to lock", async function () { - const { lock, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); - - expect(await ethers.provider.getBalance(lock.address)).to.equal( - lockedAmount - ); - }); - - it("Should fail if the unlockTime is not in the future", async function () { - // We don't use the fixture here because we want a different deployment - const latestTime = await time.latest(); - const Lock = await ethers.getContractFactory("Lock"); - await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith( - "Unlock time should be in the future" - ); - }); - }); - - describe("Withdrawals", function () { - describe("Validations", function () { - it("Should revert with the right error if called too soon", async function () { - const { lock } = await loadFixture(deployOneYearLockFixture); - - await expect(lock.withdraw()).to.be.revertedWith( - "You can't withdraw yet" - ); - }); - - it("Should revert with the right error if called from another account", async function () { - const { lock, unlockTime, otherAccount } = await loadFixture( - deployOneYearLockFixture - ); - - // We can increase the time in Hardhat Network - await time.increaseTo(unlockTime); - - // We use lock.connect() to send a transaction from another account - await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith( - "You aren't the owner" - ); - }); - - it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () { - const { lock, unlockTime } = await loadFixture( - deployOneYearLockFixture - ); - - // Transactions are sent using the first signer by default - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).not.to.be.reverted; - }); - }); - - describe("Events", function () { - it("Should emit an event on withdrawals", async function () { - const { lock, unlockTime, lockedAmount } = await loadFixture( - deployOneYearLockFixture - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()) - .to.emit(lock, "Withdrawal") - .withArgs(lockedAmount, anyValue); // We accept any value as `when` arg - }); - }); - - describe("Transfers", function () { - it("Should transfer the funds to the owner", async function () { - const { lock, unlockTime, lockedAmount, owner } = await loadFixture( - deployOneYearLockFixture - ); - - await time.increaseTo(unlockTime); - - await expect(lock.withdraw()).to.changeEtherBalances( - [owner, lock], - [lockedAmount, -lockedAmount] - ); - }); - }); - }); -}); diff --git a/test/UniswapV3FeeToken.js b/test/UniswapV3FeeToken.js new file mode 100644 index 0000000..6096c67 --- /dev/null +++ b/test/UniswapV3FeeToken.js @@ -0,0 +1,221 @@ +const { + time, + loadFixture, +} = require("@nomicfoundation/hardhat-network-helpers"); +const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); +const { expect } = require("chai"); +const { ethers } = require("hardhat"); + +const { BigNumber } = require('bignumber.js') +const Q192 = BigNumber(2).exponentiatedBy(192) + +function calculateSqrtPriceX96(price, token0Dec, token1Dec) +{ + price = BigNumber(price).shiftedBy(token1Dec - token0Dec) + ratioX96 = price.multipliedBy(Q192) + sqrtPriceX96 = ratioX96.sqrt() + return sqrtPriceX96 +} + +function getNearestUsableTick(currentTick,space) { + if(currentTick == 0){ + return 0 + } + direction = (currentTick >= 0) ? 1 : -1 + currentTick *= direction + nearestTick = (currentTick%space <= space/2) ? currentTick - (currentTick%space) : currentTick + (space-(currentTick%space)) + nearestTick *= direction + + return nearestTick +} + +describe("Uniswap V3 Fee Token", function () { + async function deployFixture() { + const [deployer, user1, user2, user3] = await ethers.getSigners(); + + const MyUniswapV3FeeToken = await ethers.getContractFactory("MyUniswapV3FeeToken"); + const myUniswapV3FeeToken = await MyUniswapV3FeeToken.deploy(); + + const nonfungiblePositionManager = await hre.ethers.getContractAt( + "INonfungiblePositionManager", "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" + ); + + const weth = await hre.ethers.getContractAt( + "IWETH", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ); + + const router = await hre.ethers.getContractAt( + "IUniswapV3Router", "0xE592427A0AEce92De3Edee1F18E0157C05861564" + ); + + await weth.deposit({value: ethers.utils.parseEther("200")}) + await weth.connect(user1).deposit({value: ethers.utils.parseEther("200")}) + + return { myUniswapV3FeeToken, nonfungiblePositionManager, router, weth, deployer, user1, user2, user3 }; + } + + async function addLiquidityFixiture() { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, weth, deployer, user1, user2, user3 } = await loadFixture(deployFixture); + + const pool = await hre.ethers.getContractAt( + "IUniswapV3Pool", await myUniswapV3FeeToken.pool4() + ); + + let slot0 = await pool.slot0() + let tickSpacing = parseInt(await pool.tickSpacing()) + let nearestTick = getNearestUsableTick(parseInt(slot0.tick),tickSpacing) + + if(myUniswapV3FeeToken.address < weth.address) + { + token0 = myUniswapV3FeeToken.address + token1 = weth.address + amount0Desired = "100000" + amount1Desired = "100" + }else + { + token0 = weth.address + token1 = myUniswapV3FeeToken.address + amount0Desired = "100" + amount1Desired = "100000" + } + + mintParams = { + token0: token0, + token1: token1, + fee: await pool.fee(), + tickLower: nearestTick - tickSpacing * 10, + tickUpper: nearestTick + tickSpacing * 10, + amount0Desired: ethers.utils.parseEther(amount0Desired), + amount1Desired: ethers.utils.parseEther(amount1Desired), + amount0Min: 0, + amount1Min: 0, + recipient: deployer.address, + deadline: "2662503213" + }; + + await myUniswapV3FeeToken.approve(nonfungiblePositionManager.address, ethers.utils.parseEther("1000000")) + await weth.approve(nonfungiblePositionManager.address, ethers.utils.parseEther("100")) + await nonfungiblePositionManager.connect(deployer).mint( + mintParams + ); + + return { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 }; + } + + describe("User 1 buys 1 eth worth of tokens", function () { + it("User 1 should get about 1000 tokens", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + var user1TokenBeforeBuy = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(user1.address)) + buyParams = { + tokenIn: weth.address, + tokenOut: myUniswapV3FeeToken.address, + fee: await pool.fee(), + recipient: user1.address, + deadline: "2662503213", + amountIn: ethers.utils.parseEther("1"), + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + } + await weth.connect(user1).approve(router.address, ethers.utils.parseEther("1")) + await router.connect(user1).exactInputSingle(buyParams) + var user1TokenAfterBuy = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(user1.address)) + expect(user1TokenAfterBuy - user1TokenBeforeBuy).to.above(900); + }); + + it("Vault should collect about 10 tokens in fees", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + var vaultTokenBeforeBuy = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + buyParams = { + tokenIn: weth.address, + tokenOut: myUniswapV3FeeToken.address, + fee: await pool.fee(), + recipient: user1.address, + deadline: "2662503213", + amountIn: ethers.utils.parseEther("1"), + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + } + await weth.connect(user1).approve(router.address, ethers.utils.parseEther("1")) + await router.connect(user1).exactInputSingle(buyParams) + var vaultTokenAfterBuy = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + expect(vaultTokenAfterBuy - vaultTokenBeforeBuy).to.above(9); + }); + }); + + describe("User1 gets 1000 tokens and then sells it", function () { + it("User1 should get around 1 ether", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + + await myUniswapV3FeeToken.transfer(user1.address, ethers.utils.parseEther("900")) + var user1WEthBeforeSell = ethers.utils.formatEther(await weth.balanceOf(user1.address)) + + sellParams = { + tokenIn: myUniswapV3FeeToken.address, + tokenOut: weth.address, + fee: await pool.fee(), + recipient: user1.address, + deadline: "2662503213", + amountIn: ethers.utils.parseEther("900"), + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + } + + await myUniswapV3FeeToken.connect(user1).approve(router.address, ethers.utils.parseEther("1000")) + await router.connect(user1).exactInputSingle(sellParams) + + var user1WEthAfterSell = ethers.utils.formatEther(await weth.balanceOf(user1.address)) + expect(user1WEthAfterSell - user1WEthBeforeSell).to.above(0.9); + }); + + it("Vault should not get any tokens", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + + await myUniswapV3FeeToken.transfer(user1.address, ethers.utils.parseEther("900")) + + var vaultTokenBeforeSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + + sellParams = { + tokenIn: myUniswapV3FeeToken.address, + tokenOut: weth.address, + fee: await pool.fee(), + recipient: user1.address, + deadline: "2662503213", + amountIn: ethers.utils.parseEther("900"), + amountOutMinimum: 0, + sqrtPriceLimitX96: 0 + } + + await myUniswapV3FeeToken.connect(user1).approve(router.address, ethers.utils.parseEther("1000")) + await router.connect(user1).exactInputSingle(sellParams) + + var vaultTokenAfterSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + expect(vaultTokenAfterSell - vaultTokenBeforeSell).to.equal(0); + }); + }); + + describe("User1 get 1000 tokens then send them to User2", function () { + it("User2 should get 980 tokens", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + + await myUniswapV3FeeToken.transfer(user1.address, ethers.utils.parseEther("1000")) + + var user2TokenBeforeSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(user2.address)) + await myUniswapV3FeeToken.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000")) + var user2TokenAfterSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(user2.address)) + + expect(user2TokenAfterSell - user2TokenBeforeSell).to.equal(980); + }); + + it("Vault should get 20 tokens", async function () { + const { myUniswapV3FeeToken, nonfungiblePositionManager, router, pool, weth, deployer, user1, user2, user3 } = await loadFixture(addLiquidityFixiture); + + await myUniswapV3FeeToken.transfer(user1.address, ethers.utils.parseEther("1000")) + + var vaultTokenBeforeSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + await myUniswapV3FeeToken.connect(user1).transfer(user2.address, ethers.utils.parseEther("1000")) + var vaultTokenAfterSell = ethers.utils.formatEther(await myUniswapV3FeeToken.balanceOf(await myUniswapV3FeeToken.tokenVaultAddress())) + + expect(vaultTokenAfterSell - vaultTokenBeforeSell).to.equal(20); + }); + }); +}); \ No newline at end of file