diff --git a/README.md b/README.md index 138ccd2..75c2453 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ Maximize interoperability for your smart contracts with the integrations library | Origin | Contract | Released | Unit Tests | Audit | |-----|----------|-----------|------------|-------| -| | Uniswap V2 Fee Token | ✔ | ❌ | ❌ | +| | Uniswap V2 Fee Token | ✔ | ✔ | ❌ | | | Uniswap V2 AutoSwap Token | ✔ | ✔ | ❌ | -| | Balancer V2 Fee Token | ✔ | ❌ | ❌ | +| | Balancer V2 Fee Token | ✔ | ✔ | ❌ | | | Uniswap V3 Fee Token | ✔ | ✔ | ❌ | | | OpenZeppelin NFT Collection | ✔ | ❌ | ❌ | | | Azuki NFT Collection | ✔ | ❌ | ❌ | diff --git a/contracts/ERC20/BalancerInterfaces.sol b/contracts/ERC20/BalancerInterfaces.sol new file mode 100644 index 0000000..0427eb0 --- /dev/null +++ b/contracts/ERC20/BalancerInterfaces.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +// Filosofía Código Contracts based on Balancer + +pragma solidity ^0.8.0; + +import "./ERC20.sol"; + +struct JoinPoolRequest { + address[] assets; + uint256[] maxAmountsIn; + bytes userData; + bool fromInternalBalance; +} + +enum SwapKind { GIVEN_IN, GIVEN_OUT } + +struct SingleSwap { + bytes32 poolId; + SwapKind kind; + address assetIn; + address assetOut; + uint256 amount; + bytes userData; +} + +struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; +} + +interface IVault +{ + function joinPool( + bytes32 poolId, + address sender, + address recipient, + JoinPoolRequest memory request + ) external payable; + + + function swap( + SingleSwap memory singleSwap, + FundManagement memory funds, + uint256 limit, + uint256 deadline + ) external payable; +} + +interface IWeightedPool2TokensFactory { + function create( + string memory name, + string memory symbol, + IERC20[] memory tokens, + uint256[] memory weights, + uint256 swapFeePercentage, + bool oracleEnabled, + address owner + ) external returns (address); +} + +interface IWeightedPool +{ + function getPoolId() external view returns (bytes32); + function balanceOf(address account) external view returns (uint256); +} \ No newline at end of file diff --git a/contracts/ERC20/BalancerV2FeeToken.sol b/contracts/ERC20/BalancerV2FeeToken.sol index 3fe2a80..e9afdd5 100644 --- a/contracts/ERC20/BalancerV2FeeToken.sol +++ b/contracts/ERC20/BalancerV2FeeToken.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.0; import "./ERC20.sol"; +import "./BalancerInterfaces.sol"; abstract contract BalancerV2FeeToken is ERC20 { @@ -14,7 +15,6 @@ abstract contract BalancerV2FeeToken is ERC20 uint public feeDecimals = 2; address public balancerVault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; - constructor(string memory name, string memory symbol, uint totalSupply_, address tokenVaultAddress_, @@ -28,9 +28,9 @@ abstract contract BalancerV2FeeToken is ERC20 isTaxless[tokenVaultAddress] = true; isTaxless[address(0)] = true; - fees[0] = buyFee; - fees[1] = sellFee; - fees[2] = p2pFee; + fees.push(buyFee); + fees.push(sellFee); + fees.push(p2pFee); isFeeActive = true; } diff --git a/contracts/Examples/MyBalancerFeeToken.sol b/contracts/Examples/MyBalancerFeeToken.sol index 80d3a94..198b3d6 100644 --- a/contracts/Examples/MyBalancerFeeToken.sol +++ b/contracts/Examples/MyBalancerFeeToken.sol @@ -9,7 +9,7 @@ contract MyBalancerFeeToken is BalancerV2FeeToken "My Token", "MTKN", // Name and Symbol 1_000_000_000 ether, // 1 billion supply address(this), // Vault Address - 100, 200, 0) // Fees: 2% buy 1% sell 0% P2P + 100, 200, 50) // Fees: 2% buy 1% sell 0.5% P2P { } } \ No newline at end of file diff --git a/test/BalancerFeeToken.js b/test/BalancerFeeToken.js new file mode 100644 index 0000000..828d8c8 --- /dev/null +++ b/test/BalancerFeeToken.js @@ -0,0 +1,185 @@ +const { + time, + loadFixture, + } = require("@nomicfoundation/hardhat-network-helpers"); + const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); + const { expect } = require("chai"); + const { ethers } = require("hardhat"); + + describe("Balancer Fee Token", function () { + async function initSetup() { + const blockNumBefore = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(blockNumBefore); + const timestamp = blockBefore.timestamp; + + const [deployer, walletA, walletB] = await ethers.getSigners(); + + const MyBalancerFeeToken = await ethers.getContractFactory("MyBalancerFeeToken"); + const weightedPool2TokensFactory = await ethers.getContractAt("IWeightedPool2TokensFactory", "0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0"); + const myBalancerFeeToken = await MyBalancerFeeToken.deploy(); + const weth = await hre.ethers.getContractAt( + "IWETH", "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + ); + const vault = await hre.ethers.getContractAt( + "IVault", "0xBA12222222228d8Ba445958a75a0704d566BF2C8" + ); + + await weth.deposit({value: ethers.utils.parseEther("200")}) + await weth.connect(walletA).deposit({value: ethers.utils.parseEther("200")}) + + let token0; + let token1; + if(myBalancerFeeToken.address.toLowerCase() < weth.address.toLowerCase()) + { + token0 = myBalancerFeeToken.address.toLowerCase(); + token1 = weth.address.toLowerCase(); + }else + { + token0 = weth.address.toLowerCase(); + token1 = myBalancerFeeToken.address.toLowerCase(); + } + + weightedPoolAddress = await weightedPool2TokensFactory.callStatic.create( + "My Weighted Pool", // Name + "MWP", // Symbol + [token0, token1], // Tokens + [ethers.utils.parseEther("0.5"), ethers.utils.parseEther("0.5")], // Weights + ethers.utils.parseEther("0.01"), // Swap Fee Percentage + false, // Oracle Enabled + ethers.constants.AddressZero // Owner + ); + + await weightedPool2TokensFactory.create( + "My Weighted Pool", // Name + "MWP", // Symbol + [token0, token1], // Tokens + [ethers.utils.parseEther("0.5"), ethers.utils.parseEther("0.5")], // Weights + ethers.utils.parseEther("0.01"), // Swap Fee Percentage + false, // Oracle Enabled + ethers.constants.AddressZero // Owner + ); + + const weightedPool = await hre.ethers.getContractAt( + "IWeightedPool", weightedPoolAddress + ); + + return { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp }; + } + + async function addLiquidity() { + const { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp } = await loadFixture(initSetup); + + await weth.approve(vault.address, ethers.utils.parseEther("100")) + await myBalancerFeeToken.approve(vault.address, ethers.utils.parseEther("100000")) + + const JOIN_KIND_INIT = 0; + var initialBalances = [ethers.utils.parseEther("100"), ethers.utils.parseEther("100000")] + var tokens = [weth.address, myBalancerFeeToken.address] + if(myBalancerFeeToken.address < weth.address) + { + tokens = [myBalancerFeeToken.address, weth.address] + initialBalances = [ethers.utils.parseEther("100000"), ethers.utils.parseEther("100")] + } + const initUserData = + ethers.utils.defaultAbiCoder.encode(['uint256', 'uint256[]'], + [JOIN_KIND_INIT, initialBalances]); + var joinPoolRequest = [ + tokens, + initialBalances, // maxAmountsIn + initUserData, + false // fromInternalBalance + ] + + await vault.joinPool( + await weightedPool.getPoolId(), + deployer.address, + deployer.address, + joinPoolRequest + ) + + return { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp }; + } + + describe("Fee collection", function () { + it("Should collect fees on P2P", async function () { + const { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp } = await loadFixture(addLiquidity); + + await myBalancerFeeToken.transfer(walletA.address, ethers.utils.parseEther("100.0")) + await myBalancerFeeToken.connect(walletA).transfer(walletB.address, ethers.utils.parseEther("100.0")) + + expect( + await myBalancerFeeToken.balanceOf(await myBalancerFeeToken.tokenVaultAddress()) + ).to.greaterThan( + 0 + ); + }); + + it("Should collect fees on Buy", async function () { + const { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp } = await loadFixture(addLiquidity); + + const swap_kind = 0; // Single Swap + const swap_struct = { + poolId: await weightedPool.getPoolId(), + kind: swap_kind, + assetIn: weth.address, + assetOut: myBalancerFeeToken.address, + amount: ethers.utils.parseEther("0.01"), + userData: '0x' + }; + + const fund_struct = { + sender: walletA.address, + fromInternalBalance: false, + recipient: walletA.address, + toInternalBalance: false + }; + await weth.connect(walletA).approve(vault.address, ethers.utils.parseEther("100")) + await vault.connect(walletA).swap( + swap_struct, + fund_struct, + ethers.utils.parseEther("0.0"), // Limit + timestamp + 100); // Deadline + + expect( + await myBalancerFeeToken.balanceOf(await myBalancerFeeToken.tokenVaultAddress()) + ).to.greaterThan( + 0 + ); + }); + + it("Should collect fees on Sell", async function () { + const { myBalancerFeeToken, weth, weightedPool, vault, deployer, walletA, walletB, timestamp } = await loadFixture(addLiquidity); + + await myBalancerFeeToken.transfer(walletA.address, ethers.utils.parseEther("100.0")) + + const swap_kind = 0; // Single Swap + const swap_struct = { + poolId: await weightedPool.getPoolId(), + kind: swap_kind, + assetIn: myBalancerFeeToken.address, + assetOut: weth.address, + amount: ethers.utils.parseEther("100"), + userData: '0x' + }; + + const fund_struct = { + sender: walletA.address, + fromInternalBalance: false, + recipient: walletA.address, + toInternalBalance: false + }; + await myBalancerFeeToken.connect(walletA).approve(vault.address, ethers.utils.parseEther("100")) + await vault.connect(walletA).swap( + swap_struct, + fund_struct, + ethers.utils.parseEther("0.0"), // Limit + timestamp + 100); // Deadline + + expect( + await myBalancerFeeToken.balanceOf(await myBalancerFeeToken.tokenVaultAddress()) + ).to.greaterThan( + 0 + ); + }); + }); + }); \ No newline at end of file diff --git a/test/UniswapV2FeeToken.js b/test/UniswapV2FeeToken.js new file mode 100644 index 0000000..bd6c84e --- /dev/null +++ b/test/UniswapV2FeeToken.js @@ -0,0 +1,111 @@ +const { + time, + loadFixture, + } = require("@nomicfoundation/hardhat-network-helpers"); + const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); + const { expect } = require("chai"); + const { ethers } = require("hardhat"); + + describe("Uniswap V2 Fee Token", function () { + async function launchAndAddLiquidity() { + const blockNumBefore = await ethers.provider.getBlockNumber(); + const blockBefore = await ethers.provider.getBlock(blockNumBefore); + deadline = blockBefore.timestamp + 500; + + const [deployer, walletA, walletB] = await ethers.getSigners(); + + const MyUniswapV2FeeToken = await ethers.getContractFactory("MyUniswapV2FeeToken"); + const uniswapRouter = await ethers.getContractAt("ISwapRouter", "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"); + const myUniswapV2FeeToken = await MyUniswapV2FeeToken.deploy(); + const ERC20 = await hre.ethers.getContractFactory("ERC20") + const usdc = await ERC20.attach("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + + // Get some base tokens + await uniswapRouter.swapETHForExactTokens( + ethers.utils.parseUnits("2000.0",6), + ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", usdc.address], + deployer.address, + deadline, + {value: ethers.utils.parseEther("5")} + ) + + await myUniswapV2FeeToken.approve(uniswapRouter.address, ethers.utils.parseEther("500000")) + await usdc.approve(uniswapRouter.address, ethers.utils.parseUnits("1000",6)) + + await uniswapRouter.addLiquidity( + myUniswapV2FeeToken.address, + usdc.address, + ethers.utils.parseEther("500000"), + ethers.utils.parseUnits("1000",6), + ethers.utils.parseEther("0"), + ethers.utils.parseEther("0"), + deployer.address, + deadline) + return { uniswapRouter, myUniswapV2FeeToken, usdc, walletA, walletB, deadline }; + } + + describe("Fee collection", function () { + it("Should collect fees on P2P", async function () { + const { uniswapRouter, myUniswapV2FeeToken, usdc, walletA, walletB } = await loadFixture(launchAndAddLiquidity); + await myUniswapV2FeeToken.transfer(walletA.address, ethers.utils.parseEther("1000")) + await myUniswapV2FeeToken.connect(walletA).transfer(walletB.address, ethers.utils.parseEther("100")) + expect( + ethers.utils.parseEther("99.5") + ).to.equal( + await myUniswapV2FeeToken.balanceOf(walletB.address) + ); + expect( + ethers.utils.parseUnits("1000.0",6) + ).to.lessThan( + await usdc.balanceOf(await myUniswapV2FeeToken.feeReceiverAddress()) + ); + }); + + it("Should collect fees on Buy", async function () { + const { uniswapRouter, myUniswapV2FeeToken, usdc, walletA, walletB } = await loadFixture(launchAndAddLiquidity); + // First, let's give WalletA some USDC + await usdc.transfer(walletA.address, ethers.utils.parseUnits("10.0",6)) + // Now we swap + await usdc.connect(walletA).approve(uniswapRouter.address, ethers.utils.parseUnits("100.0",6)) + await uniswapRouter.connect(walletA).swapTokensForExactTokens( + ethers.utils.parseEther("100"), + ethers.utils.parseUnits("10.0",6), + [usdc.address, myUniswapV2FeeToken.address], + walletA.address, + deadline) + expect( + ethers.utils.parseEther("99") + ).to.equal( + await myUniswapV2FeeToken.balanceOf(walletA.address) + ); + expect( + ethers.utils.parseUnits("990.0",6) + ).to.lessThan( + await usdc.balanceOf(await myUniswapV2FeeToken.feeReceiverAddress()) + ); + }); + it("Should collect fees on Sell", async function () { + const { uniswapRouter, myUniswapV2FeeToken, usdc, walletA, walletB } = await loadFixture(launchAndAddLiquidity); + // First, let's give WalletA some Tokens + await myUniswapV2FeeToken.transfer(walletA.address, ethers.utils.parseEther("1000")) + // Now we swap + await myUniswapV2FeeToken.connect(walletA).approve(uniswapRouter.address, ethers.utils.parseEther("1000.0")) + await uniswapRouter.connect(walletA).swapExactTokensForTokensSupportingFeeOnTransferTokens( + ethers.utils.parseEther("1000"), + 0, + [myUniswapV2FeeToken.address, usdc.address], + walletA.address, + deadline) + expect( + ethers.utils.parseEther("0.0") + ).to.lessThan( + await usdc.balanceOf(await myUniswapV2FeeToken.feeReceiverAddress()) + ); + expect( + ethers.utils.parseUnits("1000.0",6) + ).to.lessThan( + await usdc.balanceOf(await myUniswapV2FeeToken.feeReceiverAddress()) + ); + }); + }); + }); \ No newline at end of file