diff --git a/.gitignore b/.gitignore index c1d3f7151..f37062f32 100644 --- a/.gitignore +++ b/.gitignore @@ -2,10 +2,15 @@ node_modules/ contracts/.flattened .openzeppelin/.session .env -.vscode -build/ -deployments/ -typechain/ +build +env/* +coverage/* +coverage.json +cache/ +yarn.lock coverage/ coverage.json -bundle/ \ No newline at end of file +typechain +deployments/hardhat +deployments/localhost +.vscode diff --git a/contracts/DXswapFeeSplitter.sol b/contracts/DXswapFeeSplitter.sol new file mode 100644 index 000000000..490fae9d8 --- /dev/null +++ b/contracts/DXswapFeeSplitter.sol @@ -0,0 +1,192 @@ +pragma solidity =0.8.16; + +import './interfaces/IDXswapFactory.sol'; +import './interfaces/IDXswapPair.sol'; +import './interfaces/IWETH.sol'; +import './libraries/TransferHelperV2.sol'; + +contract DXswapFeeSplitter { + uint16 public maxSwapPriceImpact = 100; // uses default 1% as max allowed price impact for takeProtocolFee swap + address public owner; + address public nativeCurrencyWrapper; + address public ethReceiver; + address public fallbackReceiver; + IDXswapFactory public factory; + + // if needed set address of external project which can get % of total earned protocol fee + // % of total protocol fee to external project (100 means 1%) is within the range <0, 50> + struct ExternalFeeReceiver { + address externalReceiver; + uint16 feePercentage; + } + + mapping(address => ExternalFeeReceiver) public externalFeeReceivers; + + event TakeProtocolFee(address indexed sender, address indexed to, uint256 NumberOfPairs); + + constructor( + address _owner, + address _factory, + address _nativeCurrencyWrapper, + address _ethReceiver, + address _fallbackReceiver + ) { + owner = _owner; + factory = IDXswapFactory(_factory); + nativeCurrencyWrapper = _nativeCurrencyWrapper; + ethReceiver = _ethReceiver; + fallbackReceiver = _fallbackReceiver; + } + + receive() external payable {} + + // Take what was charged as protocol fee from the DXswap pair liquidity + function takeProtocolFee(IDXswapPair[] calldata pairs) external { + for (uint256 i = 0; i < pairs.length; i++) { + address token0 = pairs[i].token0(); + address token1 = pairs[i].token1(); + pairs[i].transfer(address(pairs[i]), pairs[i].balanceOf(address(this))); + (uint256 amount0, uint256 amount1) = pairs[i].burn(address(this)); + if (amount0 > 0) _takeTokenOrETH(address(pairs[i]), token0, amount0); + if (amount1 > 0) _takeTokenOrETH(address(pairs[i]), token1, amount1); + } + emit TakeProtocolFee(msg.sender, ethReceiver, pairs.length); + } + + // called by the owner to set the new owner + function transferOwnership(address newOwner) external { + require(msg.sender == owner, 'DXswapFeeSplitter: FORBIDDEN'); + owner = newOwner; + } + + // called by the owner to change receivers addresses + function changeReceivers(address _ethReceiver, address _fallbackReceiver) external { + require(msg.sender == owner, 'DXswapFeeSplitter: FORBIDDEN'); + ethReceiver = _ethReceiver; + fallbackReceiver = _fallbackReceiver; + } + + // Returns sorted token addresses, used to handle return values from pairs sorted in this order + function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { + require(tokenA != tokenB, 'DXswapFeeSplitter: IDENTICAL_ADDRESSES'); + (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + require(token0 != address(0), 'DXswapFeeSplitter: ZERO_ADDRESS'); + } + + // called by the owner to set maximum swap price impact allowed for single token-nativeCurrencyWrapper swap (within 0-100% range) + function setMaxSwapPriceImpact(uint16 _maxSwapPriceImpact) external { + require(msg.sender == owner, 'DXswapFeeSplitter: CALLER_NOT_OWNER'); + require(_maxSwapPriceImpact > 0 && _maxSwapPriceImpact < 10000, 'DXswapFeeSplitter: FORBIDDEN_PRICE_IMPACT'); + maxSwapPriceImpact = _maxSwapPriceImpact; + } + + // called by the owner to set external fee receiver address + function setExternalFeeReceiver(address _pair, address _externalReceiver) external { + require(msg.sender == owner, 'DXswapFeeSplitter: CALLER_NOT_OWNER'); + externalFeeReceivers[_pair].externalReceiver = _externalReceiver; + } + + // called by the owner to set fee percentage to external receiver + function setFeePercentageToExternalReceiver(address _pair, uint16 _feePercentageToExternalReceiver) external { + require(msg.sender == owner, 'DXswapFeeSplitter: CALLER_NOT_OWNER'); + IDXswapPair swapPair = IDXswapPair(_pair); + uint256 feeReceiverBalance = swapPair.balanceOf(address(this)); + if (feeReceiverBalance > 0) { + // withdraw accumulated fees before updating the split percentage + address token0 = swapPair.token0(); + address token1 = swapPair.token1(); + swapPair.transfer(address(swapPair), feeReceiverBalance); + (uint256 amount0, uint256 amount1) = swapPair.burn(address(this)); + if (amount0 > 0) _takeTokenOrETH(address(swapPair), token0, amount0); + if (amount1 > 0) _takeTokenOrETH(address(swapPair), token1, amount1); + emit TakeProtocolFee(msg.sender, ethReceiver, 1); + } + require(swapPair.balanceOf(address(this)) == 0, 'DXswapFeeSplitter: TOKENS_NOT_BURNED'); + + // fee percentage check + require( + _feePercentageToExternalReceiver >= 0 && _feePercentageToExternalReceiver <= 5000, + 'DXswapFeeSplitter: FORBIDDEN_FEE_PERCENTAGE_SPLIT' + ); + // update the split percentage for specific pair + externalFeeReceivers[_pair].feePercentage = _feePercentageToExternalReceiver; + } + + // Done with code form DXswapRouter and DXswapLibrary, removed the deadline argument + function _swapTokensForETH(uint256 amountIn, address fromToken) internal returns (uint256 amountOut) { + IDXswapPair pairToUse = IDXswapPair(factory.getPair(fromToken, nativeCurrencyWrapper)); + + (uint256 reserve0, uint256 reserve1, ) = pairToUse.getReserves(); + (uint256 reserveIn, uint256 reserveOut) = fromToken < nativeCurrencyWrapper + ? (reserve0, reserve1) + : (reserve1, reserve0); + + require(reserveIn > 0 && reserveOut > 0, 'DXswapFeeSplitter: INSUFFICIENT_LIQUIDITY'); // should never happen since pool was checked before + uint256 amountInWithFee = amountIn * (10000 - pairToUse.swapFee()); + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = reserveIn * 10000 + amountInWithFee; + amountOut = numerator / denominator; + + TransferHelper.safeTransfer(fromToken, address(pairToUse), amountIn); + + (uint256 amount0Out, uint256 amount1Out) = fromToken < nativeCurrencyWrapper + ? (uint256(0), amountOut) + : (amountOut, uint256(0)); + + pairToUse.swap(amount0Out, amount1Out, address(this), new bytes(0)); + } + + // Checks if LP has an extra external address which participates in the distrubution of protocol fee + // External Receiver address has to be defined and fee % > 0 to transfer tokens + function _splitAndTransferFee(address pair, address token, uint256 amount) internal { + address _externalFeeReceiver = externalFeeReceivers[pair].externalReceiver; + uint16 _percentFeeToExternalReceiver = externalFeeReceivers[pair].feePercentage; + + if (_percentFeeToExternalReceiver > 0 && _externalFeeReceiver != address(0)) { + uint256 feeToExternalReceiver = (amount * _percentFeeToExternalReceiver) / 10000; + uint256 feeToAvatarDAO = amount - feeToExternalReceiver; + if (token == nativeCurrencyWrapper) { + IWETH(nativeCurrencyWrapper).withdraw(amount); + TransferHelper.safeTransferETH(_externalFeeReceiver, feeToExternalReceiver); + TransferHelper.safeTransferETH(ethReceiver, feeToAvatarDAO); + } else { + TransferHelper.safeTransfer(token, _externalFeeReceiver, feeToExternalReceiver); + TransferHelper.safeTransfer(token, fallbackReceiver, feeToAvatarDAO); + } + } else { + if (token == nativeCurrencyWrapper) { + IWETH(nativeCurrencyWrapper).withdraw(amount); + TransferHelper.safeTransferETH(ethReceiver, amount); + } else { + TransferHelper.safeTransfer(token, fallbackReceiver, amount); + } + } + } + + // Convert tokens into ETH if possible, if not just transfer the token + function _takeTokenOrETH(address pair, address token, uint256 amount) internal { + if (token != nativeCurrencyWrapper && _isSwapPossible(token, nativeCurrencyWrapper, amount)) { + // If it is not nativeCurrencyWrapper and there is a direct path to nativeCurrencyWrapper, swap tokens + uint256 amountOut = _swapTokensForETH(amount, token); + _splitAndTransferFee(pair, nativeCurrencyWrapper, amountOut); + } else { + // If it is nativeCurrencyWrapper or there is not a direct path from token to nativeCurrencyWrapper, transfer tokens + _splitAndTransferFee(pair, token, amount); + } + } + + // Helper function to know if token-nativeCurrencyWrapper pool exists and has enough liquidity + function _isSwapPossible(address token0, address token1, uint256 amount) internal view returns (bool) { + address pair = factory.getPair(token0, token1); + if (pair == address(0)) return false; + + (uint256 reserve0, uint256 reserve1, ) = IDXswapPair(pair).getReserves(); + (uint256 reserveIn, uint256 reserveOut) = token0 < token1 ? (reserve0, reserve1) : (reserve1, reserve0); + if (reserveIn == 0 || reserveOut == 0) return false; + + uint256 priceImpact = (amount * 10000) / (reserveIn + amount); // simplified formula + if (priceImpact > maxSwapPriceImpact) return false; + + return true; + } +} diff --git a/contracts/libraries/TransferHelperV2.sol b/contracts/libraries/TransferHelperV2.sol new file mode 100644 index 000000000..6d3c8478b --- /dev/null +++ b/contracts/libraries/TransferHelperV2.sol @@ -0,0 +1,40 @@ +pragma solidity =0.8.16; + +// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false +library TransferHelper { + function safeApprove( + address token, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('approve(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: APPROVE_FAILED'); + } + + function safeTransfer( + address token, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('transfer(address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FAILED'); + } + + function safeTransferFrom( + address token, + address from, + address to, + uint256 value + ) internal { + // bytes4(keccak256(bytes('transferFrom(address,address,uint256)'))); + (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED'); + } + + function safeTransferETH(address to, uint256 msgValue) internal { + (bool success, ) = to.call{value: msgValue}(new bytes(0)); + require(success, 'TransferHelper: ETH_TRANSFER_FAILED'); + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts index f19c6161e..695f31089 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -18,7 +18,7 @@ const config: HardhatUserConfig = { solidity: { compilers: [ { - version: "0.8.15", + version: "0.8.16", settings: { optimizer: { enabled: true, diff --git a/test/DXswapFeeReceiver.spec.ts b/test/DXswapFeeReceiver.spec.ts index 969831731..7e3cf35b2 100644 --- a/test/DXswapFeeReceiver.spec.ts +++ b/test/DXswapFeeReceiver.spec.ts @@ -33,8 +33,8 @@ describe('DXswapFeeReceiver', () => { let pair23: DXswapPair let pair03: DXswapPair let pair24: DXswapPair - let wethPair: DXswapPair - let wethTkn0Pair: DXswapPair + let wethToken1Pair: DXswapPair + let wethToken0Pair: DXswapPair let WETH: WETH9 beforeEach('assign signers', async function () { @@ -54,13 +54,11 @@ describe('DXswapFeeReceiver', () => { token1 = fixture.token1 token2 = fixture.token2 token3 = fixture.token3 - token4 = fixture.token4 pair01 = fixture.dxswapPair01 pair23 = fixture.dxswapPair23 pair03 = fixture.dxswapPair03 - pair24 = fixture.dxswapPair24 - wethPair = fixture.wethToken1Pair - wethTkn0Pair = fixture.wethToken0Pair + wethToken1Pair = fixture.wethToken1Pair + wethToken0Pair = fixture.wethToken0Pair WETH = fixture.WETH }) @@ -72,13 +70,16 @@ describe('DXswapFeeReceiver', () => { } function getAmountOutSync( - reserve0: BigNumber, reserve1: BigNumber, usingToken0: boolean, amountIn: BigNumber, swapFee: BigNumber + reserve0: BigNumber, + reserve1: BigNumber, + usingToken0: boolean, + amountIn: BigNumber, + swapFee: BigNumber ) { const tokenInBalance = usingToken0 ? reserve0 : reserve1 const tokenOutBalance = usingToken0 ? reserve1 : reserve0 const amountInWithFee = amountIn.mul(FEE_DENOMINATOR.sub(swapFee)) - return amountInWithFee.mul(tokenOutBalance) - .div(tokenInBalance.mul(FEE_DENOMINATOR).add(amountInWithFee)) + return amountInWithFee.mul(tokenOutBalance).div(tokenInBalance.mul(FEE_DENOMINATOR).add(amountInWithFee)) } // Where token0-token1 and token1-WETH pairs exist @@ -93,9 +94,9 @@ describe('DXswapFeeReceiver', () => { await token3.transfer(pair03.address, tokenAmount) await pair03.mint(dxdao.address, overrides) - await token0.transfer(wethTkn0Pair.address, tokenAmount) - await WETH.transfer(wethTkn0Pair.address, wethAmount) - await wethTkn0Pair.mint(dxdao.address, overrides) + await token0.transfer(wethToken0Pair.address, tokenAmount) + await WETH.transfer(wethToken0Pair.address, wethAmount) + await wethToken0Pair.mint(dxdao.address, overrides) let amountOut = await getAmountOut(pair03, token0.address, amountIn); @@ -121,7 +122,7 @@ describe('DXswapFeeReceiver', () => { const token0FromProtocolFee = protocolFeeLPToknesReceived .mul(await token0.balanceOf(pair03.address)).div(await pair03.totalSupply()); - const wethFromToken0FromProtocolFee = await getAmountOut(wethTkn0Pair, token0.address, token0FromProtocolFee); + const wethFromToken0FromProtocolFee = await getAmountOut(wethToken0Pair, token0.address, token0FromProtocolFee); const protocolFeeReceiverBalanceBeforeTake = await provider.getBalance(protocolFeeReceiver.address) await feeReceiver.connect(dxdao).takeProtocolFee([pair03.address], overrides) @@ -143,47 +144,47 @@ describe('DXswapFeeReceiver', () => { const wethAmount = expandTo18Decimals(40); const amountIn = expandTo18Decimals(20); - await token1.transfer(wethPair.address, tokenAmount, overrides) - await WETH.transfer(wethPair.address, wethAmount, overrides) - await wethPair.mint(dxdao.address, overrides) + await token1.transfer(wethToken1Pair.address, tokenAmount, overrides) + await WETH.transfer(wethToken1Pair.address, wethAmount, overrides) + await wethToken1Pair.mint(dxdao.address, overrides) const token1IsFirstToken = (token1.address < WETH.address) - let amountOut = await getAmountOut(wethPair, token1.address, amountIn); - await token1.transfer(wethPair.address, amountIn, overrides) - await wethPair.swap( + let amountOut = await getAmountOut(wethToken1Pair, token1.address, amountIn); + await token1.transfer(wethToken1Pair.address, amountIn, overrides) + await wethToken1Pair.swap( token1IsFirstToken ? 0 : amountOut, token1IsFirstToken ? amountOut : 0, dxdao.address, '0x', overrides ) - amountOut = await getAmountOut(wethPair, WETH.address, amountIn); - await WETH.transfer(wethPair.address, amountIn, overrides) - await wethPair.swap( + amountOut = await getAmountOut(wethToken1Pair, WETH.address, amountIn); + await WETH.transfer(wethToken1Pair.address, amountIn, overrides) + await wethToken1Pair.swap( token1IsFirstToken ? amountOut : 0, token1IsFirstToken ? 0 : amountOut, dxdao.address, '0x', overrides ) - const protocolFeeToReceive = await calcProtocolFee(wethPair, factory); + const protocolFeeToReceive = await calcProtocolFee(wethToken1Pair, factory); - await token1.transfer(wethPair.address, expandTo18Decimals(10), overrides) - await WETH.transfer(wethPair.address, expandTo18Decimals(10), overrides) - await wethPair.mint(dxdao.address, overrides) + await token1.transfer(wethToken1Pair.address, expandTo18Decimals(10), overrides) + await WETH.transfer(wethToken1Pair.address, expandTo18Decimals(10), overrides) + await wethToken1Pair.mint(dxdao.address, overrides) - const protocolFeeLPToknesReceived = await wethPair.balanceOf(feeReceiver.address); + const protocolFeeLPToknesReceived = await wethToken1Pair.balanceOf(feeReceiver.address); expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) const token1FromProtocolFee = protocolFeeLPToknesReceived - .mul(await token1.balanceOf(wethPair.address)).div(await wethPair.totalSupply()); + .mul(await token1.balanceOf(wethToken1Pair.address)).div(await wethToken1Pair.totalSupply()); const wethFromProtocolFee = protocolFeeLPToknesReceived - .mul(await WETH.balanceOf(wethPair.address)).div(await wethPair.totalSupply()); + .mul(await WETH.balanceOf(wethToken1Pair.address)).div(await wethToken1Pair.totalSupply()); - const swapFee = BigNumber.from(await wethPair.swapFee()) - const token1ReserveBeforeSwap = (await token1.balanceOf(wethPair.address)).sub(token1FromProtocolFee) - const wethReserveBeforeSwap = (await WETH.balanceOf(wethPair.address)).sub(wethFromProtocolFee) + const swapFee = BigNumber.from(await wethToken1Pair.swapFee()) + const token1ReserveBeforeSwap = (await token1.balanceOf(wethToken1Pair.address)).sub(token1FromProtocolFee) + const wethReserveBeforeSwap = (await WETH.balanceOf(wethToken1Pair.address)).sub(wethFromProtocolFee) const wethFromToken1FromProtocolFee = await getAmountOutSync( token1IsFirstToken ? token1ReserveBeforeSwap : wethReserveBeforeSwap, token1IsFirstToken ? wethReserveBeforeSwap : token1ReserveBeforeSwap, @@ -194,11 +195,11 @@ describe('DXswapFeeReceiver', () => { const protocolFeeReceiverBalanceBeforeTake = await provider.getBalance(protocolFeeReceiver.address) - await feeReceiver.connect(dxdao).takeProtocolFee([wethPair.address], overrides) + await feeReceiver.connect(dxdao).takeProtocolFee([wethToken1Pair.address], overrides) expect(await token1.balanceOf(feeReceiver.address)).to.eq(0) expect(await WETH.balanceOf(feeReceiver.address)).to.eq(0) - expect(await wethPair.balanceOf(feeReceiver.address)).to.eq(0) + expect(await wethToken1Pair.balanceOf(feeReceiver.address)).to.eq(0) expect(await provider.getBalance(feeReceiver.address)).to.eq(0) expect((await provider.getBalance(protocolFeeReceiver.address))) @@ -264,103 +265,6 @@ describe('DXswapFeeReceiver', () => { .to.be.eq(protocolFeeReceiverBalance) }) - it( - 'should receive only tokens when extracting fee from both token2-tonken3 pair and token2-token4 pair', - async () => { - const tokenAmount = expandTo18Decimals(100); - const amountIn = expandTo18Decimals(50); - - await token2.transfer(pair23.address, tokenAmount) - await token3.transfer(pair23.address, tokenAmount) - await pair23.mint(dxdao.address, overrides) - - let amountOut = await getAmountOut(pair23, token2.address, amountIn); - await token2.transfer(pair23.address, amountIn) - await pair23.swap( - (token2.address < token3.address) ? 0 : amountOut, - (token2.address < token3.address) ? amountOut : 0, - dxdao.address, '0x', overrides - ) - - amountOut = await getAmountOut(pair23, token3.address, amountIn); - await token3.transfer(pair23.address, amountIn) - await pair23.swap( - (token2.address < token3.address) ? amountOut : 0, - (token2.address < token3.address) ? 0 : amountOut, - dxdao.address, '0x', overrides - ) - - let protocolFeeToReceive = await calcProtocolFee(pair23, factory); - - await token2.transfer(pair23.address, expandTo18Decimals(10)) - await token3.transfer(pair23.address, expandTo18Decimals(10)) - await pair23.mint(dxdao.address, overrides) - - const protocolFeeLPpair23 = await pair23.balanceOf(feeReceiver.address); - expect(protocolFeeLPpair23.div(ROUND_EXCEPTION)) - .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) - - await token2.transfer(pair24.address, tokenAmount) - await token4.transfer(pair24.address, tokenAmount) - await pair24.mint(dxdao.address, overrides) - - amountOut = await getAmountOut(pair24, token2.address, amountIn); - await token2.transfer(pair24.address, amountIn) - await pair24.swap( - (token2.address < token4.address) ? 0 : amountOut, - (token2.address < token4.address) ? amountOut : 0, - dxdao.address, '0x', overrides - ) - - amountOut = await getAmountOut(pair24, token4.address, amountIn); - await token4.transfer(pair24.address, amountIn) - await pair24.swap( - (token2.address < token4.address) ? amountOut : 0, - (token2.address < token4.address) ? 0 : amountOut, - dxdao.address, '0x', overrides - ) - - protocolFeeToReceive = await calcProtocolFee(pair24, factory); - - await token2.transfer(pair24.address, expandTo18Decimals(10)) - await token4.transfer(pair24.address, expandTo18Decimals(10)) - await pair24.mint(dxdao.address, overrides) - - const protocolFeeLPPair24 = await pair24.balanceOf(feeReceiver.address); - expect(protocolFeeLPPair24.div(ROUND_EXCEPTION)) - .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) - - const token2FromPair23 = protocolFeeLPpair23 - .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); - const token3FromPair23 = protocolFeeLPpair23 - .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); - const token2FromPair24 = protocolFeeLPPair24 - .mul(await token2.balanceOf(pair24.address)).div(await pair24.totalSupply()); - const token4FromPair24 = protocolFeeLPPair24 - .mul(await token4.balanceOf(pair24.address)).div(await pair24.totalSupply()); - - const protocolFeeReceiverBalance = await provider.getBalance(protocolFeeReceiver.address) - - await feeReceiver.connect(dxdao).takeProtocolFee([pair23.address, pair24.address], overrides) - - expect(await provider.getBalance(protocolFeeReceiver.address)).to.eq(protocolFeeReceiverBalance.toString()) - - expect(await token2.balanceOf(feeReceiver.address)).to.eq(0) - expect(await token3.balanceOf(feeReceiver.address)).to.eq(0) - expect(await token4.balanceOf(feeReceiver.address)).to.eq(0) - expect(await WETH.balanceOf(feeReceiver.address)).to.eq(0) - expect(await provider.getBalance(feeReceiver.address)).to.eq(0) - - expect((await provider.getBalance(protocolFeeReceiver.address))) - .to.be.eq(protocolFeeReceiverBalance) - expect((await token3.balanceOf(fallbackReceiver.address))) - .to.be.eq(token3FromPair23) - expect((await token4.balanceOf(fallbackReceiver.address))) - .to.be.eq(token4FromPair24) - expect((await token2.balanceOf(fallbackReceiver.address))) - .to.be.eq(token2FromPair23.add(token2FromPair24)) - }) - it( 'should only allow owner to transfer ownership', async () => { @@ -370,6 +274,7 @@ describe('DXswapFeeReceiver', () => { expect(await feeReceiver.owner()).to.be.eq(tokenOwner.address) }) + // Where pairs with weth don't exist it( 'should only allow owner to change receivers', async () => { diff --git a/test/DXswapFeeSplitter.spec.ts b/test/DXswapFeeSplitter.spec.ts new file mode 100644 index 000000000..107131fe0 --- /dev/null +++ b/test/DXswapFeeSplitter.spec.ts @@ -0,0 +1,1524 @@ +import '@nomiclabs/hardhat-ethers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { ethers } from "hardhat"; +import { BigNumber } from 'ethers' +import { pairFixture } from './shared/fixtures' +import { DXswapFactory, DXswapFeeSetter, DXswapFeeSplitter, DXswapPair, ERC20, WETH9 } from '../typechain' +import { calcProtocolFee, expandTo18Decimals } from './shared/utilities'; + +const FEE_DENOMINATOR = BigNumber.from(10).pow(4) +const ROUND_EXCEPTION = BigNumber.from(10).pow(4) + +const overrides = { + gasLimit: 9999999 +} + +describe('DXswapFeeSplitter', () => { + const provider = ethers.provider + let dxdao: SignerWithAddress + let tokenOwner: SignerWithAddress + let protocolfeeSplitter: SignerWithAddress + let fallbackReceiver: SignerWithAddress + let externalfeeSplitter: SignerWithAddress + let other: SignerWithAddress + let factory: DXswapFactory + let feeSplitter: DXswapFeeSplitter + let feeSetter: DXswapFeeSetter + + let token0: ERC20 + let token1: ERC20 + let token2: ERC20 + let token3: ERC20 + let pair01: DXswapPair + let pair23: DXswapPair + let pair03: DXswapPair + let wethToken0Pair: DXswapPair + let wethToken1Pair: DXswapPair + let WETH: WETH9 + + beforeEach('assign signers', async function () { + const signers = await ethers.getSigners() + dxdao = signers[0] + tokenOwner = signers[1] + protocolfeeSplitter = signers[2] + fallbackReceiver = signers[3] + other = signers[4] + externalfeeSplitter = signers[5] + }) + + beforeEach('deploy fixture', async () => { + const fixture = await pairFixture(provider, [dxdao, protocolfeeSplitter, fallbackReceiver]) + factory = fixture.dxswapFactory + feeSplitter = fixture.feeSplitter + feeSetter = fixture.feeSetter + token0 = fixture.token0 + token1 = fixture.token1 + token2 = fixture.token2 + token3 = fixture.token3 + pair01 = fixture.dxswapPair01 + pair23 = fixture.dxswapPair23 + pair03 = fixture.dxswapPair03 + WETH = fixture.WETH + wethToken0Pair = fixture.wethToken0Pair + wethToken1Pair = fixture.wethToken1Pair + }) + + beforeEach('set fee to', async function () { + await feeSetter.connect(dxdao).setFeeTo(feeSplitter.address, overrides); + }) + + async function getAmountOut(pair01: DXswapPair, tokenIn: string, amountIn: BigNumber) { + const [reserve0, reserve1] = await pair01.getReserves() + const token0 = await pair01.token0() + const swapFee = BigNumber.from(await pair01.swapFee()) + return getAmountOutSync(reserve0, reserve1, token0 === tokenIn, amountIn, swapFee) + } + + function getAmountOutSync( + reserve0: BigNumber, reserve1: BigNumber, usingToken0: boolean, amountIn: BigNumber, swapFee: BigNumber + ) { + const tokenInBalance = usingToken0 ? reserve0 : reserve1 + const tokenOutBalance = usingToken0 ? reserve1 : reserve0 + const amountInWithFee = amountIn.mul(FEE_DENOMINATOR.sub(swapFee)) + return amountInWithFee.mul(tokenOutBalance) + .div(tokenInBalance.mul(FEE_DENOMINATOR).add(amountInWithFee)) + } + describe('external receiver off', async () => { + it('feeTo, feeToSetter, allPairsLength, INIT_CODE_PAIR_HASH', async () => { + expect(await factory.feeTo()).to.eq(feeSplitter.address) + expect(await factory.feeToSetter()).to.eq(feeSetter.address) + expect(await factory.INIT_CODE_PAIR_HASH()).to.eq('0x9e43bdf627764c4a3e3e452d1b558fff8466adc4dc8a900396801d26f4c542f2') + }) + + it( + 'should only allow owner to set max price impact', + async () => { + await expect(feeSplitter.connect(other).setMaxSwapPriceImpact(500)) + .to.be.revertedWith('DXswapFeeSplitter: CALLER_NOT_OWNER') + await feeSplitter.connect(dxdao).setMaxSwapPriceImpact(500); + expect(await feeSplitter.maxSwapPriceImpact()).to.be.eq(500) + }) + + it( + 'should set max price impact within the range 0 - 10000', + async () => { + expect(await feeSplitter.maxSwapPriceImpact()).to.be.eq(100) + await expect(feeSplitter.connect(dxdao).setMaxSwapPriceImpact(0)) + .to.be.revertedWith('DXswapFeeSplitter: FORBIDDEN_PRICE_IMPACT') + await expect(feeSplitter.connect(dxdao).setMaxSwapPriceImpact(10000)) + .to.be.revertedWith('DXswapFeeSplitter: FORBIDDEN_PRICE_IMPACT') + await feeSplitter.connect(dxdao).setMaxSwapPriceImpact(500); + expect(await feeSplitter.maxSwapPriceImpact()).to.be.eq(500) + }) + + // Where token0-token1 and token1-WETH pairs exist + it( + 'should receive token0 to fallbackreceiver and ETH to ethReceiver when extracting fee from token0-token1', + async () => { + const tokenAmount = expandTo18Decimals(100); + const wethAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(10); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x') + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x') + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect(await token0.balanceOf(fallbackReceiver.address)) + .to.be.eq(token0FromProtocolFee) + expect(await provider.getBalance(protocolfeeSplitter.address)) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken1FromProtocolFee)) + }) + + it('should receive everything in ETH from one WETH-token1 pair', async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(20); + + await token1.transfer(wethToken1Pair.address, tokenAmount, overrides) + await WETH.transfer(wethToken1Pair.address, wethAmount, overrides) + await wethToken1Pair.mint(dxdao.address, overrides) + + const token1IsFirstToken = (token1.address < WETH.address) + + let amountOut = await getAmountOut(wethToken1Pair, token1.address, amountIn); + await token1.transfer(wethToken1Pair.address, amountIn, overrides) + await wethToken1Pair.swap( + token1IsFirstToken ? 0 : amountOut, + token1IsFirstToken ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(wethToken1Pair, WETH.address, amountIn); + await WETH.transfer(wethToken1Pair.address, amountIn, overrides) + await wethToken1Pair.swap( + token1IsFirstToken ? amountOut : 0, + token1IsFirstToken ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + const protocolFeeToReceive = await calcProtocolFee(wethToken1Pair, factory); + + await token1.transfer(wethToken1Pair.address, expandTo18Decimals(10), overrides) + await WETH.transfer(wethToken1Pair.address, expandTo18Decimals(10), overrides) + await wethToken1Pair.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await wethToken1Pair.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(wethToken1Pair.address)).div(await wethToken1Pair.totalSupply()); + const wethFromProtocolFee = protocolFeeLPToknesReceived + .mul(await WETH.balanceOf(wethToken1Pair.address)).div(await wethToken1Pair.totalSupply()); + + const swapFee = BigNumber.from(await wethToken1Pair.swapFee()) + const token1ReserveBeforeSwap = (await token1.balanceOf(wethToken1Pair.address)).sub(token1FromProtocolFee) + const wethReserveBeforeSwap = (await WETH.balanceOf(wethToken1Pair.address)).sub(wethFromProtocolFee) + const wethFromToken1FromProtocolFee = await getAmountOutSync( + token1IsFirstToken ? token1ReserveBeforeSwap : wethReserveBeforeSwap, + token1IsFirstToken ? wethReserveBeforeSwap : token1ReserveBeforeSwap, + token1IsFirstToken, + token1FromProtocolFee, + swapFee + ); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([wethToken1Pair.address], overrides) + + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await wethToken1Pair.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken1FromProtocolFee).add(wethFromProtocolFee)) + }) + + it( + 'should receive only tokens when extracting fee from tokenA-tokenB pair that has no path to WETH', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn); + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn); + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + const protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLPpair23 = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLPpair23.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token2FromProtocolFee = protocolFeeLPpair23 + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const token3FromProtocolFee = protocolFeeLPpair23 + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair23.address], overrides) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair23.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token2.balanceOf(fallbackReceiver.address))) + .to.be.eq((token2FromProtocolFee)) + expect((await token3.balanceOf(fallbackReceiver.address))) + .to.be.eq((token3FromProtocolFee)) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalance) + }) + + it( + 'should receive only tokens when extracting fee from both tokenA-tonkenB pair and tokenC-tokenD pair', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn); + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn); + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + let protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLPpair23 = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLPpair23.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + await token0.transfer(pair03.address, tokenAmount) + await token3.transfer(pair03.address, tokenAmount) + await pair03.mint(dxdao.address, overrides) + + amountOut = await getAmountOut(pair03, token0.address, amountIn); + await token0.transfer(pair03.address, amountIn) + await pair03.swap( + (token0.address < token3.address) ? 0 : amountOut, + (token0.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair03, token3.address, amountIn); + await token3.transfer(pair03.address, amountIn) + await pair03.swap( + (token0.address < token3.address) ? amountOut : 0, + (token0.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + protocolFeeToReceive = await calcProtocolFee(pair03, factory); + + await token0.transfer(pair03.address, expandTo18Decimals(10)) + await token3.transfer(pair03.address, expandTo18Decimals(10)) + await pair03.mint(dxdao.address, overrides) + + const protocolFeeLPPair03 = await pair03.balanceOf(feeSplitter.address); + expect(protocolFeeLPPair03.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token2FromPair23 = protocolFeeLPpair23 + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const token3FromPair23 = protocolFeeLPpair23 + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const token0FromPair03 = protocolFeeLPPair03 + .mul(await token0.balanceOf(pair03.address)).div(await pair03.totalSupply()); + const token3FromPair03 = protocolFeeLPPair03 + .mul(await token3.balanceOf(pair03.address)).div(await pair03.totalSupply()); + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair23.address, pair03.address], overrides) + + expect(await provider.getBalance(protocolfeeSplitter.address)).to.eq(protocolfeeSplitterBalance.toString()) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalance) + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.be.eq(token0FromPair03) + expect((await token2.balanceOf(fallbackReceiver.address))) + .to.be.eq(token2FromPair23) + expect((await token3.balanceOf(fallbackReceiver.address))) + .to.be.eq(token3FromPair23.add(token3FromPair03)) + }) + + it( + 'should only allow owner to transfer ownership', + async () => { + await expect(feeSplitter.connect(other).transferOwnership(other.address, overrides)) + .to.be.revertedWith('DXswapFeeSplitter: FORBIDDEN') + await feeSplitter.connect(dxdao).transferOwnership(tokenOwner.address, overrides); + expect(await feeSplitter.owner()).to.be.eq(tokenOwner.address) + }) + + it( + 'should only allow owner to change receivers', + async () => { + await expect(feeSplitter.connect(other).changeReceivers(other.address, other.address, overrides)) + .to.be.revertedWith('DXswapFeeSplitter: FORBIDDEN') + await feeSplitter.connect(dxdao).changeReceivers(other.address, other.address, overrides); + expect(await feeSplitter.ethReceiver()).to.be.eq(other.address) + expect(await feeSplitter.fallbackReceiver()).to.be.eq(other.address) + }) + + it('should send tokens if there is not any liquidity in the WETH pair', async () => { + const tokenAmount = expandTo18Decimals(100) + const amountIn = expandTo18Decimals(50) + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn) + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn) + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory) + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address) + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)).to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)) + .div(await pair01.totalSupply()) + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)) + .div(await pair01.totalSupply()) + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + + feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect(await provider.getBalance(protocolfeeSplitter.address)).to.be.eq(protocolfeeSplitterBalance) + expect(await token0.balanceOf(fallbackReceiver.address)).to.be.eq(token0FromProtocolFee) + expect(await token1.balanceOf(fallbackReceiver.address)).to.be.eq(token1FromProtocolFee) + }) + + // Where token0-token1 and token1-WETH pairs exist AND PRICE IMPACT TOO HIGH + it( + 'should receive token0 and token1 if price impact token1-weth pool is too high', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(1); + // add very small liquidity to weth-token1 pool + const wethTknAmountLowLP = BigNumber.from(1).mul(BigNumber.from(10).pow(15)); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, wethTknAmountLowLP) + await WETH.transfer(wethToken1Pair.address, wethTknAmountLowLP) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(15)) + await token1.transfer(pair01.address, expandTo18Decimals(15)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake) + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.be.eq(token0FromProtocolFee) + expect((await token1.balanceOf(fallbackReceiver.address))) + .to.be.eq(token1FromProtocolFee) + }) + }) + + describe('external receiver on', () => { + it( + 'should send tokenA & tokenB default 100% fee to dxdao and 0% fee to external receiver', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + + await feeSplitter.setExternalFeeReceiver(pair23.address, externalfeeSplitter.address) + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair23.address) + expect(percentFeeToExternalReceiver).to.eq(0) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + + + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn) + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn) + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + let protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLP = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLP.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const tokenAFromProtocolFee = protocolFeeLP + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const tokenBFromProtocolFee = protocolFeeLP + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + + const balanceTkn2 = await token2.balanceOf(fallbackReceiver.address) + const balanceTkn3 = await token3.balanceOf(fallbackReceiver.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair23.address], overrides) + + expect(await provider.getBalance(protocolfeeSplitter.address)).to.eq(protocolfeeSplitterBalance.toString()) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalance) + + expect((await token2.balanceOf(fallbackReceiver.address))) + .to.be.eq(balanceTkn2.add(tokenAFromProtocolFee)) + expect((await token2.balanceOf(externalfeeSplitter.address))) + .to.be.eq(0) + + expect((await token3.balanceOf(fallbackReceiver.address))) + .to.be.eq(balanceTkn3.add(tokenBFromProtocolFee)) + expect((await token3.balanceOf(externalfeeSplitter.address))) + .to.be.eq(0) + }) + + it( + 'should split protocol fee and send tokenA & tokenB to dxdao and external fee receiver', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + const newPercentFeeToExternalReceiver = 2000 //20% + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair23.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair23.address, newPercentFeeToExternalReceiver) + const [newExternalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair23.address) + expect(percentFeeToExternalReceiver).to.eq(newPercentFeeToExternalReceiver) + expect(newExternalReceiver).to.eq(externalfeeSplitter.address) + + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn) + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn) + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + let protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLP = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLP.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const tokenAFromProtocolFee = protocolFeeLP + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const tokenBFromProtocolFee = protocolFeeLP + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + + const tokenAExternal = tokenAFromProtocolFee.mul(percentFeeToExternalReceiver).div(10000); + const tokenBExternal = tokenBFromProtocolFee.mul(percentFeeToExternalReceiver).div(10000); + const tokenAfeeSplitter = tokenAFromProtocolFee.sub(tokenAExternal); + const tokenBfeeSplitter = tokenBFromProtocolFee.sub(tokenBExternal); + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address); + + await feeSplitter.connect(dxdao).takeProtocolFee([pair23.address], overrides) + + expect(await provider.getBalance(protocolfeeSplitter.address)).to.eq(protocolfeeSplitterBalance) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalance) + + expect((await token2.balanceOf(fallbackReceiver.address))) + .to.be.eq(tokenAfeeSplitter) + expect((await token2.balanceOf(externalfeeSplitter.address))) + .to.be.eq(tokenAExternal) + expect((await token3.balanceOf(fallbackReceiver.address))) + .to.be.eq(tokenBfeeSplitter) + expect((await token3.balanceOf(externalfeeSplitter.address))) + .to.be.eq(tokenBExternal) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should swap token0 & token1 to ETH and sent to ethReceiver when extracting fee from token0-token1', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token0.transfer(wethToken0Pair.address, tokenAmount) + await WETH.transfer(wethToken0Pair.address, wethAmount) + await wethToken0Pair.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(2)) + await token1.transfer(pair01.address, expandTo18Decimals(2)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken0FromProtocolFee = await getAmountOut(wethToken0Pair, token0.address, token0FromProtocolFee); + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token0.balanceOf(protocolfeeSplitter.address))) + .to.eq(0) + expect((await token1.balanceOf(protocolfeeSplitter.address))) + .to.eq(0) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken0FromProtocolFee).add(wethFromToken1FromProtocolFee)) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should receive token0 and ETH when extracting fee from token0-token1 and swap LPs exist but not enough liquidity', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + // add very small liquidity to weth-token0 pool + const wethTknAmountLowLP = BigNumber.from(1).mul(BigNumber.from(10).pow(6)); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token0.transfer(wethToken0Pair.address, wethTknAmountLowLP) + await WETH.transfer(wethToken0Pair.address, wethTknAmountLowLP) + await wethToken0Pair.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect(await token0.balanceOf(fallbackReceiver.address)) + .to.eq(token0FromProtocolFee) + expect(await token1.balanceOf(protocolfeeSplitter.address)) + .to.eq(0) + expect(await provider.getBalance(protocolfeeSplitter.address)) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken1FromProtocolFee)) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should receive token0 and ETH when extracting fee from token0-token1 and swap LPs exist but token reserve is 0', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + // add very small liquidity to weth-token0 pool + const wethTknAmountLowLP = BigNumber.from(1).mul(BigNumber.from(10).pow(6)); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + // dont transfer token0 to the pool and dont mint lp tokens + await WETH.transfer(wethToken0Pair.address, wethTknAmountLowLP) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.eq(token0FromProtocolFee) + expect((await token1.balanceOf(protocolfeeSplitter.address))) + .to.eq(0) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken1FromProtocolFee)) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should swap tkn0 & tkn1 to ETH and split fee when extracting from tkn0-tkn1', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + const newPercentFeeToExternalReceiver = 2000 //20% + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair01.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, newPercentFeeToExternalReceiver) + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(percentFeeToExternalReceiver).to.eq(newPercentFeeToExternalReceiver) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + + await token0.transfer(wethToken0Pair.address, tokenAmount) + await WETH.transfer(wethToken0Pair.address, wethAmount) + await wethToken0Pair.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken0FromProtocolFee = await getAmountOut(wethToken0Pair, token0.address, token0FromProtocolFee); + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + const externalfeeSplitterBalanceBeforeTake = await provider.getBalance(externalfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token0.balanceOf(protocolfeeSplitter.address))) + .to.eq(0) + expect((await token1.balanceOf(protocolfeeSplitter.address))) + .to.eq(0) + expect((await token0.balanceOf(externalfeeSplitter.address))) + .to.eq(0) + expect((await token1.balanceOf(externalfeeSplitter.address))) + .to.eq(0) + + const totalWethFromFees = wethFromToken0FromProtocolFee.add(wethFromToken1FromProtocolFee) + const wethToExternalReceiver = totalWethFromFees.mul(percentFeeToExternalReceiver).div(10000) + const wethToProtocolfeeSplitter = totalWethFromFees.sub(wethToExternalReceiver) + + expect((await provider.getBalance(protocolfeeSplitter.address)).div(ROUND_EXCEPTION)) + .to.be.eq((protocolfeeSplitterBalanceBeforeTake.add(wethToProtocolfeeSplitter)).div(ROUND_EXCEPTION)) + expect((await provider.getBalance(externalfeeSplitter.address)).div(ROUND_EXCEPTION)) + .to.be.eq((externalfeeSplitterBalanceBeforeTake.add(wethToExternalReceiver)).div(ROUND_EXCEPTION)) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should split tkn0 & tkn1 fee when extracting from tkn0-tkn1 and swap to weth impossible', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + const newPercentFeeToExternalReceiver = 2000 //20% + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair01.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, newPercentFeeToExternalReceiver) + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(percentFeeToExternalReceiver).to.eq(newPercentFeeToExternalReceiver) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const receiverBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + const externalfeeSplitterBalanceBeforeTake = await provider.getBalance(externalfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + const tkn0ToExternalReceiver = token0FromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tkn1ToExternalReceiver = token1FromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tkn0ToProtocolfeeSplitter = token0FromProtocolFee.sub(tkn0ToExternalReceiver) + const tkn1ToProtocolfeeSplitter = token1FromProtocolFee.sub(tkn1ToExternalReceiver) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + // send token0 and token1 to fallbackreceiver and external fee receiver + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.eq(tkn0ToProtocolfeeSplitter) + expect((await token1.balanceOf(fallbackReceiver.address))) + .to.eq(tkn1ToProtocolfeeSplitter) + expect((await token0.balanceOf(externalfeeSplitter.address))) + .to.eq(tkn0ToExternalReceiver) + expect((await token1.balanceOf(externalfeeSplitter.address))) + .to.eq(tkn1ToExternalReceiver) + + // should not change eth balance + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.eq(receiverBalanceBeforeTake) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.eq(protocolfeeSplitterBalanceBeforeTake) + expect((await provider.getBalance(externalfeeSplitter.address))) + .to.eq(externalfeeSplitterBalanceBeforeTake) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should update protocol fee and split tkn0 & tkn1 fee when extracting from tkn0-tkn1 and swap to weth impossible', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + const newPercentFeeToExternalReceiver = 2000 //20% + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair01.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, newPercentFeeToExternalReceiver) + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(percentFeeToExternalReceiver).to.eq(newPercentFeeToExternalReceiver) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + // change protocol fee + await feeSetter.connect(dxdao).setProtocolFee(20) + expect(await factory.protocolFeeDenominator()).to.eq(20) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + const externalfeeSplitterBalanceBeforeTake = await provider.getBalance(externalfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair01.address], overrides) + + const tkn0ToExternalReceiver = token0FromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tkn1ToExternalReceiver = token1FromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tkn0ToProtocolfeeSplitter = token0FromProtocolFee.sub(tkn0ToExternalReceiver) + const tkn1ToProtocolfeeSplitter = token1FromProtocolFee.sub(tkn1ToExternalReceiver) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + // send token0 and token1 to fallbackreceiver and external fee receiver + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.eq(tkn0ToProtocolfeeSplitter) + expect((await token1.balanceOf(fallbackReceiver.address))) + .to.eq(tkn1ToProtocolfeeSplitter) + expect((await token0.balanceOf(externalfeeSplitter.address))) + .to.eq(tkn0ToExternalReceiver) + expect((await token1.balanceOf(externalfeeSplitter.address))) + .to.eq(tkn1ToExternalReceiver) + + // should not change eth balance + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.eq(protocolfeeSplitterBalanceBeforeTake) + expect((await provider.getBalance(externalfeeSplitter.address))) + .to.eq(externalfeeSplitterBalanceBeforeTake) + }) + + // Where tokenA-tokenB, tokenC-tokenD and tokenC-WETH pairs exist + it( + 'should receive tokens 2,3 and ETH (token0 swapped) from pair 23, 03', + async () => { + const tokenAmount = expandTo18Decimals(100); + const wethAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + + await token0.transfer(wethToken0Pair.address, tokenAmount) + await WETH.transfer(wethToken0Pair.address, wethAmount) + await wethToken0Pair.mint(dxdao.address, overrides) + + // pair23 + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn) + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn) + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + let protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLPPair23 = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLPPair23.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + // pair03 + await token0.transfer(pair03.address, tokenAmount) + await token3.transfer(pair03.address, tokenAmount) + await pair03.mint(dxdao.address, overrides) + + amountOut = await getAmountOut(pair03, token2.address, amountIn) + await token0.transfer(pair03.address, amountIn) + await pair03.swap( + (token0.address < token3.address) ? 0 : amountOut, + (token0.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair03, token3.address, amountIn) + await token3.transfer(pair03.address, amountIn) + await pair03.swap( + (token0.address < token3.address) ? amountOut : 0, + (token0.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + protocolFeeToReceive = await calcProtocolFee(pair03, factory); + + await token0.transfer(pair03.address, expandTo18Decimals(10)) + await token3.transfer(pair03.address, expandTo18Decimals(10)) + await pair03.mint(dxdao.address, overrides) + + const protocolFeeLPPair03 = await pair03.balanceOf(feeSplitter.address); + expect(protocolFeeLPPair03.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const tokenAFromProtocolFee = protocolFeeLPPair23 + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const tokenBFromProtocolFee = protocolFeeLPPair23 + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const tokenCFromProtocolFee = protocolFeeLPPair03 + .mul(await token0.balanceOf(pair03.address)).div(await pair03.totalSupply()); + const tokenDFromProtocolFee = protocolFeeLPPair03 + .mul(await token3.balanceOf(pair03.address)).div(await pair03.totalSupply()); + + const wethFromToken0FromProtocolFee = await getAmountOut(wethToken0Pair, token0.address, tokenCFromProtocolFee); + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + + await expect(feeSplitter.connect(dxdao).takeProtocolFee([pair23.address, pair03.address], overrides) + ).to.emit(feeSplitter, 'TakeProtocolFee').withArgs(dxdao.address, protocolfeeSplitter.address, 2) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token2.balanceOf(fallbackReceiver.address))) + .to.be.eq(tokenAFromProtocolFee) + expect((await token3.balanceOf(fallbackReceiver.address))) + .to.be.eq(tokenBFromProtocolFee.add(tokenDFromProtocolFee)) + + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.eq(0) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalance.add(wethFromToken0FromProtocolFee)) + }) + + it( + 'should receive tkn0 and eth if split % was updated', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const wethFromToken1FromProtocolFee = await getAmountOut(wethToken1Pair, token1.address, token1FromProtocolFee); + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair01.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, 2000) + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(percentFeeToExternalReceiver).to.eq(2000) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.be.eq(token0FromProtocolFee) + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethFromToken1FromProtocolFee)) + }) + + // Where weth pairs don't exist + it( + 'should split and receive only tokens when extracting fee from tokenA-tokenB pair that has no path to WETH', + async () => { + const tokenAmount = expandTo18Decimals(100); + const amountIn = expandTo18Decimals(50); + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair23.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair23.address, 1000) + + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair23.address) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + expect(percentFeeToExternalReceiver).to.eq(1000) + + await token2.transfer(pair23.address, tokenAmount) + await token3.transfer(pair23.address, tokenAmount) + await pair23.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair23, token2.address, amountIn); + await token2.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? 0 : amountOut, + (token2.address < token3.address) ? amountOut : 0, + dxdao.address, '0x', overrides + ) + + amountOut = await getAmountOut(pair23, token3.address, amountIn); + await token3.transfer(pair23.address, amountIn) + await pair23.swap( + (token2.address < token3.address) ? amountOut : 0, + (token2.address < token3.address) ? 0 : amountOut, + dxdao.address, '0x', overrides + ) + + const protocolFeeToReceive = await calcProtocolFee(pair23, factory); + + await token2.transfer(pair23.address, expandTo18Decimals(10)) + await token3.transfer(pair23.address, expandTo18Decimals(10)) + await pair23.mint(dxdao.address, overrides) + + const protocolFeeLP = await pair23.balanceOf(feeSplitter.address); + expect(protocolFeeLP.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + const tokenAFromProtocolFee = protocolFeeLP + .mul(await token2.balanceOf(pair23.address)).div(await pair23.totalSupply()); + const tokenBFromProtocolFee = protocolFeeLP + .mul(await token3.balanceOf(pair23.address)).div(await pair23.totalSupply()); + + const protocolfeeSplitterBalance = await provider.getBalance(protocolfeeSplitter.address) + const externalBalance = await provider.getBalance(externalfeeSplitter.address) + + await feeSplitter.connect(dxdao).takeProtocolFee([pair23.address], overrides) + + const tknAExternalReceiver = tokenAFromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tknBExternalReceiver = tokenBFromProtocolFee.mul(percentFeeToExternalReceiver).div(10000) + const tknAProtocolfeeSplitter = tokenAFromProtocolFee.sub(tknAExternalReceiver) + const tknBProtocolfeeSplitter = tokenBFromProtocolFee.sub(tknBExternalReceiver) + + expect(await token2.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token3.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair23.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + expect(await provider.getBalance(protocolfeeSplitter.address)) + .to.be.eq(protocolfeeSplitterBalance) + expect(await provider.getBalance(externalfeeSplitter.address)) + .to.be.eq(externalBalance) + expect(await token2.balanceOf(fallbackReceiver.address)) + .to.be.eq(tknAProtocolfeeSplitter) + expect(await token3.balanceOf(fallbackReceiver.address)) + .to.be.eq(tknBProtocolfeeSplitter) + expect(await token2.balanceOf(externalfeeSplitter.address)) + .to.be.eq(tknAExternalReceiver) + expect(await token3.balanceOf(externalfeeSplitter.address)) + .to.be.eq(tknBExternalReceiver) + }) + + // Where token0-token1, token0-WETH and token1-WETH pairs exist + it( + 'should swap tokens, split and sent to ethReceiver when extracting fee from token0-token1', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + + // set external fee receiver + await feeSplitter.setExternalFeeReceiver(pair01.address, externalfeeSplitter.address) + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, 3000) + + const [externalReceiver, percentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(externalReceiver).to.eq(externalfeeSplitter.address) + expect(percentFeeToExternalReceiver).to.eq(3000) + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token0.transfer(wethToken0Pair.address, tokenAmount) + await WETH.transfer(wethToken0Pair.address, wethAmount) + await wethToken0Pair.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(pair01, token1.address, amountIn); + await token1.transfer(pair01.address, amountIn) + await pair01.swap(amountOut, 0, dxdao.address, '0x', overrides) + + // estimate protocol fee received + const protocolFeeToReceive = await calcProtocolFee(pair01, factory); + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + const protocolFeeLPToknesReceived = await pair01.balanceOf(feeSplitter.address); + expect(protocolFeeLPToknesReceived.div(ROUND_EXCEPTION)) + .to.be.eq(protocolFeeToReceive.div(ROUND_EXCEPTION)) + + // calculate tkn0 & tkn1 amount based on LP + const token0FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token0.balanceOf(pair01.address)).div(await pair01.totalSupply()); + const token1FromProtocolFee = protocolFeeLPToknesReceived + .mul(await token1.balanceOf(pair01.address)).div(await pair01.totalSupply()); + + const dxdaoBalanceBeforeTake = await provider.getBalance(dxdao.address) + const protocolfeeSplitterBalanceBeforeTake = await provider.getBalance(protocolfeeSplitter.address) + const externalfeeSplitterBalanceBeforeTake = await provider.getBalance(externalfeeSplitter.address) + + // estimate weth from tokens + const wethFromToken0 = await getAmountOut(wethToken0Pair, token0.address, token0FromProtocolFee); + const wethFromToken1 = await getAmountOut(wethToken1Pair, token0.address, token1FromProtocolFee); + + // set external fee receiver + await feeSplitter.setFeePercentageToExternalReceiver(pair01.address, 2000) + const [newExternalReceiver, newPercentFeeToExternalReceiver] = await feeSplitter.externalFeeReceivers(pair01.address) + expect(newExternalReceiver).to.eq(externalfeeSplitter.address) + expect(newPercentFeeToExternalReceiver).to.eq(2000) + + // split weth to avatar and external Receiver with OLD fee percentage + const wethTkn0ToExternalReceiver = wethFromToken0.mul(percentFeeToExternalReceiver).div(10000) + const wethTkn1ToExternalReceiver = wethFromToken1.mul(percentFeeToExternalReceiver).div(10000) + const tkn0ToProtocolfeeSplitter = wethFromToken0.sub(wethTkn0ToExternalReceiver) + const tkn1ToProtocolfeeSplitter = wethFromToken1.sub(wethTkn1ToExternalReceiver) + + // weth to external Receiver after token-weth swap + const wethExternal = wethTkn0ToExternalReceiver.add(wethTkn1ToExternalReceiver) + + // weth to dao after token-weth swap + const wethfeeSplitter = tkn0ToProtocolfeeSplitter.add(tkn1ToProtocolfeeSplitter) + + expect(await token0.balanceOf(feeSplitter.address)).to.eq(0) + expect(await token1.balanceOf(feeSplitter.address)).to.eq(0) + expect(await WETH.balanceOf(feeSplitter.address)).to.eq(0) + expect(await pair01.balanceOf(feeSplitter.address)).to.eq(0) + expect(await provider.getBalance(feeSplitter.address)).to.eq(0) + + // dont send token0 and token1 to fallbackreceiver and external fee receiver + expect((await token0.balanceOf(fallbackReceiver.address))) + .to.eq(0) + expect((await token1.balanceOf(fallbackReceiver.address))) + .to.eq(0) + expect((await token0.balanceOf(externalfeeSplitter.address))) + .to.eq(0) + expect((await token1.balanceOf(externalfeeSplitter.address))) + .to.eq(0) + + // should change eth balance for avatar and external Receiver + expect((await provider.getBalance(protocolfeeSplitter.address))) + .to.be.eq(protocolfeeSplitterBalanceBeforeTake.add(wethfeeSplitter)) + expect((await provider.getBalance(externalfeeSplitter.address))) + .to.be.eq(externalfeeSplitterBalanceBeforeTake.add(wethExternal)) + + // should not send eth to avatar (gas used for updating split %) + expect((await provider.getBalance(dxdao.address))) + .to.be.lte(dxdaoBalanceBeforeTake) + }) + + // Where token0-token1 and token1-WETH pairs exist + it( + 'should emit TakeProtocolFee event', + async () => { + const tokenAmount = expandTo18Decimals(40); + const wethAmount = expandTo18Decimals(40); + const amountIn = expandTo18Decimals(4); + + await token0.transfer(pair01.address, tokenAmount) + await token1.transfer(pair01.address, tokenAmount) + await pair01.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, tokenAmount) + await WETH.transfer(wethToken1Pair.address, wethAmount) + await wethToken1Pair.mint(dxdao.address, overrides) + + let amountOut = await getAmountOut(pair01, token0.address, amountIn); + await token0.transfer(pair01.address, amountIn) + await pair01.swap(0, amountOut, dxdao.address, '0x', overrides) + + amountOut = await getAmountOut(wethToken1Pair, token1.address, amountIn); + await token1.transfer(wethToken1Pair.address, amountIn) + await wethToken1Pair.swap(0, amountOut, dxdao.address, '0x', overrides) + + await token0.transfer(pair01.address, expandTo18Decimals(10)) + await token1.transfer(pair01.address, expandTo18Decimals(10)) + await pair01.mint(dxdao.address, overrides) + + await token1.transfer(wethToken1Pair.address, expandTo18Decimals(10)) + await WETH.transfer(wethToken1Pair.address, expandTo18Decimals(10)) + await wethToken1Pair.mint(dxdao.address, overrides) + + await expect(feeSplitter.connect(dxdao).takeProtocolFee([pair01.address, wethToken1Pair.address], overrides) + ).to.emit(feeSplitter, 'TakeProtocolFee').withArgs(dxdao.address, protocolfeeSplitter.address, 2) + }) + }) +}) diff --git a/test/DXswapPair.spec.ts b/test/DXswapPair.spec.ts index 897456c7e..2eff2a21f 100644 --- a/test/DXswapPair.spec.ts +++ b/test/DXswapPair.spec.ts @@ -5,7 +5,7 @@ import { ethers } from "hardhat"; import { BigNumber, constants } from 'ethers' import { pairFixture } from './shared/fixtures' import { DXswapFactory, DXswapFeeReceiver, DXswapFeeSetter, DXswapPair, ERC20 } from './../typechain' -import { encodePrice, expandTo18Decimals } from './shared/utilities'; +import { encodePrice, expandTo18Decimals, mineBlock } from './shared/utilities'; import { time } from '@nomicfoundation/hardhat-network-helpers' const MINIMUM_LIQUIDITY = BigNumber.from(10).pow(3) @@ -15,7 +15,7 @@ const FEE_DENOMINATOR = BigNumber.from(10).pow(4) const { AddressZero } = constants const overrides = { - gasLimit: 9999999 + gasLimit: 15000000 } describe('DXswapPair', () => { @@ -217,9 +217,7 @@ describe('DXswapPair', () => { const swapAmount = expandTo18Decimals(1) const expectedOutputAmount = BigNumber.from('453305446940074565') await token1.transfer(pair.address, swapAmount) - - await time.increase(1) - + await mineBlock(provider, (await provider.getBlock('latest')).timestamp + 1) const tx = await pair.swap(expectedOutputAmount, 0, dxdao.address, '0x', overrides) const receipt = await tx.wait() expect(receipt.gasUsed).to.eq(75947) @@ -272,7 +270,7 @@ describe('DXswapPair', () => { const swapAmount = expandTo18Decimals(3) await token0.transfer(pair.address, swapAmount) - await time.increaseTo(blockTimestamp + 9) + await mineBlock(provider, blockTimestamp + 9) // swap to a new price eagerly instead of syncing await pair.swap(0, expandTo18Decimals(1), dxdao.address, '0x', overrides) // make the price nice @@ -281,7 +279,7 @@ describe('DXswapPair', () => { expect(await pair.price1CumulativeLast()).to.eq(initialPrice[1].mul(10)) expect((await pair.getReserves())[2]).to.eq(blockTimestamp + 10) - await time.increaseTo(blockTimestamp + 19) + await mineBlock(provider, blockTimestamp + 19) await pair.sync(overrides) const newPrice = encodePrice(expandTo18Decimals(6), expandTo18Decimals(2)) expect(await pair.price0CumulativeLast()).to.eq(initialPrice[0].mul(10).add(newPrice[0].mul(10))) diff --git a/test/DynamicFees.spec.ts b/test/DynamicFees.spec.ts index 1fe8aa5d6..02b25288b 100644 --- a/test/DynamicFees.spec.ts +++ b/test/DynamicFees.spec.ts @@ -13,7 +13,7 @@ const SWAP_DEN = BigNumber.from(10000); const ROUND_EXCEPTION = BigNumber.from(10).pow(4) const overrides = { - gasLimit: 9999999 + gasLimit: 15000000 } describe('DXswapFeeReceiver', () => { diff --git a/test/shared/fixtures.ts b/test/shared/fixtures.ts index 49f203b1a..11908702b 100644 --- a/test/shared/fixtures.ts +++ b/test/shared/fixtures.ts @@ -3,6 +3,8 @@ import { DXswapFactory, DXswapFactory__factory, DXswapPair, DXswapPair__factory, import { defaultAbiCoder } from 'ethers/lib/utils'; import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { JsonRpcProvider } from '@ethersproject/providers'; +import { DXswapFeeSplitter } from '../../typechain/DXswapFeeSplitter'; +import { DXswapFeeSplitter__factory } from '../../typechain/factories/DXswapFeeSplitter__factory'; const overrides = { @@ -14,6 +16,7 @@ interface FactoryFixture { dxswapFactory: DXswapFactory feeSetter: DXswapFeeSetter feeReceiver: DXswapFeeReceiver + feeSplitter: DXswapFeeSplitter WETH: WETH9 } @@ -41,7 +44,10 @@ export async function factoryFixture(provider: JsonRpcProvider, [dxdao, protocol const feeReceiverAddress = await dxswapFactory.feeTo() const feeReceiver = (await new DXswapFeeReceiver__factory(dxdao).deploy(dxdao.address, dxswapFactory.address, WETH.address, protocolFeeReceiver.address, fallbackReceiver.address)).attach(feeReceiverAddress) - return { dxswapFactory, feeSetter, feeReceiver, WETH } + // deplot FeeSplitter + const feeSplitter = await new DXswapFeeSplitter__factory(dxdao).deploy(dxdao.address, dxswapFactory.address, WETH.address, protocolFeeReceiver.address, fallbackReceiver.address) + + return { dxswapFactory, feeSetter, feeReceiver, feeSplitter, WETH } } interface PairFixture extends FactoryFixture { @@ -49,11 +55,9 @@ interface PairFixture extends FactoryFixture { token1: ERC20 token2: ERC20 token3: ERC20 - token4: ERC20 dxswapPair01: DXswapPair dxswapPair23: DXswapPair dxswapPair03: DXswapPair - dxswapPair24: DXswapPair wethToken1Pair: DXswapPair wethToken0Pair: DXswapPair } @@ -64,7 +68,6 @@ export async function pairFixture(provider: JsonRpcProvider, [dxdao, protocolFee const tokenB = await new ERC20__factory(dxdao).deploy(TOTAL_SUPPLY) const tokenC = await new ERC20__factory(dxdao).deploy(TOTAL_SUPPLY) const tokenD = await new ERC20__factory(dxdao).deploy(TOTAL_SUPPLY) - const tokenE = await new ERC20__factory(dxdao).deploy(TOTAL_SUPPLY) // deploy weth const WETH = await new WETH9__factory(dxdao).deploy() @@ -76,12 +79,11 @@ export async function pairFixture(provider: JsonRpcProvider, [dxdao, protocolFee const token2 = tokenC.address < tokenD.address ? tokenC : tokenD const token3 = token2.address === tokenC.address ? tokenD : tokenC - const token4 = tokenE const dxSwapDeployer = await new DXswapDeployer__factory(dxdao).deploy(protocolFeeReceiver.address, dxdao.address, WETH.address, - [token0.address, token1.address, token2.address, token0.address, token0.address, token2.address], - [token1.address, WETH.address, token3.address, token3.address, WETH.address, token4.address], - [15, 15, 15, 15, 15, 15], + [token0.address, token1.address, token2.address, token0.address, token0.address], + [token1.address, WETH.address, token3.address, token3.address, WETH.address], + [15, 15, 15, 15, 15], overrides) await dxdao.sendTransaction({ to: dxSwapDeployer.address, gasPrice: 20000000000, value: expandTo18Decimals(1) }) @@ -105,6 +107,11 @@ export async function pairFixture(provider: JsonRpcProvider, [dxdao, protocolFee // set receivers feeReceiver.connect(dxdao).changeReceivers(protocolFeeReceiver.address, fallbackReceiver.address) + //deploy FeeSplitter + const feeSplitter = await new DXswapFeeSplitter__factory(dxdao).deploy(dxdao.address, dxswapFactory.address, WETH.address, protocolFeeReceiver.address, fallbackReceiver.address) + // set receivers + feeSplitter.connect(dxdao).changeReceivers(protocolFeeReceiver.address, fallbackReceiver.address) + // initialize DXswapPair factory const dxSwapPair_factory = await new DXswapPair__factory(dxdao).deploy() @@ -112,15 +119,14 @@ export async function pairFixture(provider: JsonRpcProvider, [dxdao, protocolFee const pairAddress1 = await dxswapFactory.getPair(token0.address, token1.address) const dxswapPair01 = dxSwapPair_factory.attach(pairAddress1) + // await dxswapFactory.createPair(token2.address, token3.address, overrides) const pairAddress2 = await dxswapFactory.getPair(token2.address, token3.address) const dxswapPair23 = dxSwapPair_factory.attach(pairAddress2) + // await dxswapFactory.createPair(token0.address, token3.address, overrides) const pairAddress3 = await dxswapFactory.getPair(token0.address, token3.address) const dxswapPair03 = dxSwapPair_factory.attach(pairAddress3) - const pairAddress4 = await dxswapFactory.getPair(token2.address, token4.address) - const dxswapPair24 = dxSwapPair_factory.attach(pairAddress4) - // create weth/erc20 pair const WETHPairAddress = await dxswapFactory.getPair(token1.address, WETH.address) const wethToken1Pair = dxSwapPair_factory.attach(WETHPairAddress) @@ -129,5 +135,5 @@ export async function pairFixture(provider: JsonRpcProvider, [dxdao, protocolFee const WETH0PairAddress = await dxswapFactory.getPair(token0.address, WETH.address) const wethToken0Pair = dxSwapPair_factory.attach(WETH0PairAddress) - return { dxswapFactory, feeSetter, feeReceiver, WETH, token0, token1, token2, token3, token4, dxswapPair01, dxswapPair23, dxswapPair03, dxswapPair24, wethToken1Pair, wethToken0Pair } + return { dxswapFactory, feeSetter, feeReceiver, feeSplitter, WETH, token0, token1, token2, token3, dxswapPair01, dxswapPair23, dxswapPair03, wethToken1Pair, wethToken0Pair } } diff --git a/test/shared/utilities.ts b/test/shared/utilities.ts index 1d946a87b..cc0164bc5 100644 --- a/test/shared/utilities.ts +++ b/test/shared/utilities.ts @@ -1,4 +1,4 @@ -import { BigNumber, Contract, constants } from 'ethers' +import { BigNumber, Contract, providers, constants } from 'ethers' import { getAddress, keccak256, @@ -80,6 +80,14 @@ export async function getApprovalDigest( ) } +export async function mineBlock(provider: providers.JsonRpcProvider, timestamp: number, force = false): Promise { + if (force) { + await provider.send("evm_setNextBlockTimestamp", [timestamp]) + return provider.send("evm_mine", []) + } + return provider.send('evm_mine', [timestamp]) +} + export function encodePrice(reserve0: BigNumber, reserve1: BigNumber) { return [reserve1.mul(BigNumber.from(2).pow(112)).div(reserve0), reserve0.mul(BigNumber.from(2).pow(112)).div(reserve1)] } diff --git a/yarn.lock b/yarn.lock index 396a1b9d0..8b926b4b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1558,13 +1558,6 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bip66@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22" - integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI= - dependencies: - safe-buffer "^5.0.1" - blakejs@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.1.0.tgz#69df92ef953aa88ca51a32df6ab1c54a155fc7a5" @@ -1595,7 +1588,7 @@ bn.js@^5.1.2: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== -bn.js@^5.1.3, bn.js@^5.2.1: +bn.js@^5.1.3, bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -1648,7 +1641,7 @@ browser-stdout@1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6, browserify-aes@^1.2.0: +browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== @@ -2466,15 +2459,6 @@ dotenv@^10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== -drbg.js@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b" - integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs= - dependencies: - browserify-aes "^1.0.6" - create-hash "^1.1.2" - create-hmac "^1.1.4" - duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -3136,7 +3120,7 @@ ethjs-unit@0.1.6: bn.js "4.11.6" number-to-bn "1.7.0" -ethjs-util@0.1.6, ethjs-util@^0.1.6: +ethjs-util@0.1.6, ethjs-util@^0.1.3, ethjs-util@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/ethjs-util/-/ethjs-util-0.1.6.tgz#f308b62f185f9fe6237132fb2a9818866a5cd536" integrity sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w== @@ -6003,6 +5987,13 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +rlp@^2.0.0: + version "2.2.7" + resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.7.tgz#33f31c4afac81124ac4b283e2bd4d9720b30beaf" + integrity sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ== + dependencies: + bn.js "^5.2.0" + rlp@^2.2.3: version "2.2.4" resolved "https://registry.yarnpkg.com/rlp/-/rlp-2.2.4.tgz#d6b0e1659e9285fc509a5d169a9bd06f704951c1" @@ -6091,20 +6082,6 @@ scrypt-js@3.0.1, scrypt-js@^3.0.0, scrypt-js@^3.0.1: resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-3.0.1.tgz#d314a57c2aef69d1ad98a138a21fe9eafa9ee312" integrity sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA== -secp256k1@^3.0.1: - version "3.8.0" - resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.8.0.tgz#28f59f4b01dbee9575f56a47034b7d2e3b3b352d" - integrity sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw== - dependencies: - bindings "^1.5.0" - bip66 "^1.1.5" - bn.js "^4.11.8" - create-hash "^1.2.0" - drbg.js "^1.0.1" - elliptic "^6.5.2" - nan "^2.14.0" - safe-buffer "^5.1.2" - secp256k1@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.2.tgz#15dd57d0f0b9fdb54ac1fa1694f40e5e9a54f4a1" @@ -7563,14 +7540,6 @@ yargs-parser@20.2.4: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== -yargs-parser@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"