-
Notifications
You must be signed in to change notification settings - Fork 4
Feat: protocol fee split #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
34f449d
76a95eb
48a0c85
08e3a5c
275ac5b
1d05cbb
ad1ca78
8dff165
677acb5
3ae81fb
721aa22
5572f39
dadbb47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,122 +6,226 @@ import './interfaces/IWETH.sol'; | |
| import './libraries/TransferHelper.sol'; | ||
| import './libraries/SafeMath.sol'; | ||
|
|
||
|
|
||
| contract DXswapFeeReceiver { | ||
| using SafeMath for uint; | ||
| using SafeMath for uint256; | ||
|
|
||
| address public owner; | ||
| IDXswapFactory public factory; | ||
| address public WETH; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't remember if this is actually intended to be WETH or just the deployment chain's native currency wrapper (WXDAI on GC for example). If the latter applies, I'd suggest a rename to P.S.: deriving from the below logic, it looks like WETH should indeed be called in a more network-neutral way.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the framework has been to convert the fees, which start in the form of LP tokens, into the chain's native currency if possible. This has made sense thus far with Ethereum Mainnet, Arbitrum (using ETH as native currency), and Gnosis Chain (using xDai). However, I think generally speaking it's not obvious that the protocol fee should always be converted to native currency. It could depend a lot on which chain. Maybe we should consider making the target to convert the fee to more flexible? But in the context of how DXdao has been using the feeReceiver, then the above comment is correct that the intent is to be the chain's native currency and not always WETH. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thing is, this contract only really works with the target chain's native asset and its wrapper as is (as you recognize). If we want to make this more flexible I think we have two options:
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the approach of the second option makes sense. The first implementation deployment of the feeReceiver was on mainnet and the recipient was the bonding curve, which needs to be sent ETH in order for it to work. That fact probably influenced our initial implementation. However, I don't think the new implementation needs to send to the bonding curve, as the bonding curve is now paused and in all likelihood will be sunset.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that the second option makes sense! |
||
| address public ethReceiver; | ||
| address public fallbackReceiver; | ||
| uint32 public maxSwapPriceImpact = 100; // uses default 1% as max allowed price impact for takeProtocolFee swap | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is expressed in basis points it can probably be a |
||
|
|
||
| // 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; | ||
| uint32 feePercentage; | ||
| } | ||
|
|
||
| mapping(address => ExternalFeeReceiver) public externalFeeReceivers; | ||
|
|
||
| event TakeProtocolFee(address indexed sender, address indexed to, uint256 NumberOfPairs); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Change |
||
|
|
||
| constructor( | ||
| address _owner, address _factory, address _WETH, address _ethReceiver, address _fallbackReceiver | ||
| address _owner, | ||
| address _factory, | ||
| address _WETH, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If above applies, this should be changed too. |
||
| address _ethReceiver, | ||
| address _fallbackReceiver | ||
| ) public { | ||
| owner = _owner; | ||
| factory = IDXswapFactory(_factory); | ||
| WETH = _WETH; | ||
| ethReceiver = _ethReceiver; | ||
| fallbackReceiver = _fallbackReceiver; | ||
| } | ||
|
|
||
| function() external payable {} | ||
|
|
||
| // called by the owner to set the new owner | ||
| function transferOwnership(address newOwner) external { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm actually becoming a fan of the |
||
| require(msg.sender == owner, 'DXswapFeeReceiver: FORBIDDEN'); | ||
| owner = newOwner; | ||
| } | ||
|
|
||
|
|
||
| // called by the owner to change receivers addresses | ||
| function changeReceivers(address _ethReceiver, address _fallbackReceiver) external { | ||
| require(msg.sender == owner, 'DXswapFeeReceiver: 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, 'DXswapFeeReceiver: IDENTICAL_ADDRESSES'); | ||
| (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); | ||
| require(token0 != address(0), 'DXswapFeeReceiver: ZERO_ADDRESS'); | ||
| } | ||
|
|
||
| // Helper function to know if an address is a contract, extcodesize returns the size of the code of a smart | ||
| // contract in a specific address | ||
| function isContract(address addr) internal returns (bool) { | ||
| uint size; | ||
| assembly { size := extcodesize(addr) } | ||
| function isContract(address addr) internal view returns (bool) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This must be replaced by a call to the factory. Increases gas consumption a little bit, but since it's only used to check if a pair exists, in the process we also have guarantees about the validity of a pair (and we avoid surprises like the hack we've had). |
||
| uint256 size; | ||
| assembly { | ||
| size := extcodesize(addr) | ||
| } | ||
| return size > 0; | ||
| } | ||
|
|
||
| // Calculates the CREATE2 address for a pair without making any external calls | ||
| // Taken from DXswapLibrary, removed the factory parameter | ||
| function pairFor(address tokenA, address tokenB) internal view returns (address pair) { | ||
| (address token0, address token1) = sortTokens(tokenA, tokenB); | ||
| pair = address(uint(keccak256(abi.encodePacked( | ||
| hex'ff', | ||
| factory, | ||
| keccak256(abi.encodePacked(token0, token1)), | ||
| hex'd306a548755b9295ee49cc729e13ca4a45e00199bbd890fa146da43a50571776' // init code hash | ||
| )))); | ||
| pair = address( | ||
| uint256( | ||
| keccak256( | ||
| abi.encodePacked( | ||
| hex'ff', | ||
| factory, | ||
| keccak256(abi.encodePacked(token0, token1)), | ||
| hex'd306a548755b9295ee49cc729e13ca4a45e00199bbd890fa146da43a50571776' // init code hash | ||
| ) | ||
| ) | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| // Done with code form DXswapRouter and DXswapLibrary, removed the deadline argument | ||
| function _swapTokensForETH(uint amountIn, address fromToken) | ||
| internal | ||
| { | ||
| function _swapTokensForETH(uint256 amountIn, address fromToken) internal returns (uint256 amountOut) { | ||
| IDXswapPair pairToUse = IDXswapPair(pairFor(fromToken, WETH)); | ||
|
|
||
| (uint reserve0, uint reserve1,) = pairToUse.getReserves(); | ||
| (uint reserveIn, uint reserveOut) = fromToken < WETH ? (reserve0, reserve1) : (reserve1, reserve0); | ||
|
|
||
| require(reserveIn > 0 && reserveOut > 0, 'DXswapFeeReceiver: INSUFFICIENT_LIQUIDITY'); | ||
| uint amountInWithFee = amountIn.mul(uint(10000).sub(pairToUse.swapFee())); | ||
| uint numerator = amountInWithFee.mul(reserveOut); | ||
| uint denominator = reserveIn.mul(10000).add(amountInWithFee); | ||
| uint amountOut = numerator / denominator; | ||
|
|
||
| TransferHelper.safeTransfer( | ||
| fromToken, address(pairToUse), amountIn | ||
| ); | ||
|
|
||
| (uint amount0Out, uint amount1Out) = fromToken < WETH ? (uint(0), amountOut) : (amountOut, uint(0)); | ||
|
|
||
| pairToUse.swap( | ||
| amount0Out, amount1Out, address(this), new bytes(0) | ||
| ); | ||
|
|
||
| IWETH(WETH).withdraw(amountOut); | ||
| TransferHelper.safeTransferETH(ethReceiver, amountOut); | ||
|
|
||
| (uint256 reserve0, uint256 reserve1, ) = pairToUse.getReserves(); | ||
| (uint256 reserveIn, uint256 reserveOut) = fromToken < WETH ? (reserve0, reserve1) : (reserve1, reserve0); | ||
|
|
||
| require(reserveIn > 0 && reserveOut > 0, 'DXswapFeeReceiver: INSUFFICIENT_LIQUIDITY'); // should never happen since pool was checked before | ||
| uint256 amountInWithFee = amountIn.mul(uint256(10000).sub(pairToUse.swapFee())); | ||
| uint256 numerator = amountInWithFee.mul(reserveOut); | ||
| uint256 denominator = reserveIn.mul(10000).add(amountInWithFee); | ||
| amountOut = numerator / denominator; | ||
|
|
||
| TransferHelper.safeTransfer(fromToken, address(pairToUse), amountIn); | ||
|
|
||
| (uint256 amount0Out, uint256 amount1Out) = fromToken < WETH ? (uint256(0), amountOut) : (amountOut, uint256(0)); | ||
|
|
||
| pairToUse.swap(amount0Out, amount1Out, address(this), new bytes(0)); | ||
| } | ||
|
|
||
| // Transfer to the owner address the token converted into ETH if possible, if not just transfer the token. | ||
| function _takeETHorToken(address token, uint amount) internal { | ||
| if (token == WETH) { | ||
| // If it is WETH, transfer directly to ETH receiver | ||
| IWETH(WETH).withdraw(amount); | ||
| TransferHelper.safeTransferETH(ethReceiver, amount); | ||
| } else if (isContract(pairFor(token, WETH))) { | ||
| // If it is not WETH and there is a direct path to WETH, swap and transfer WETH to ETH receiver | ||
| _swapTokensForETH(amount, token); | ||
| } else { | ||
| // If it is not WETH and there is not a direct path to WETH, transfer tokens directly to fallback receiver | ||
| TransferHelper.safeTransfer(token, fallbackReceiver, amount); | ||
| } | ||
| // Helper function to know if token-WETH pool exists and has enough liquidity | ||
| function _isSwapPossible( | ||
| address token0, | ||
| address token1, | ||
| uint256 amount | ||
| ) internal view returns (bool) { | ||
| address pair = pairFor(token0, token1); | ||
| if (!isContract(pair)) 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.mul(10000) / reserveIn; // simplified formula | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess either use SafeMath or not. Let's not mix:) so either: or
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see this is intentionally a simplified formula. Do we think it's good enough?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've done quite a lot of calculations (if you're interested in details I can share the sheet) and imo it's good enough just as an extra protection step. Ofc it is possible to implement more complex formula, but that could cost more. I'll check it out and compare gas usage. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like to see more if possible. |
||
| if (priceImpact > maxSwapPriceImpact) return false; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| // 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; | ||
| uint32 _percentFeeToExternalReceiver = externalFeeReceivers[pair].feePercentage; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case and see if using this helps lowering gas consumption. |
||
|
|
||
| if (_percentFeeToExternalReceiver > 0 && _externalFeeReceiver != address(0)) { | ||
| uint256 feeToExternalReceiver = amount.mul(_percentFeeToExternalReceiver) / 10000; | ||
| uint256 feeToAvatarDAO = amount.sub(feeToExternalReceiver); | ||
| if (token == WETH) { | ||
| IWETH(WETH).withdraw(amount); | ||
| TransferHelper.safeTransferETH(_externalFeeReceiver, feeToExternalReceiver); | ||
| TransferHelper.safeTransferETH(ethReceiver, feeToAvatarDAO); | ||
| } else { | ||
| TransferHelper.safeTransfer(token, _externalFeeReceiver, feeToExternalReceiver); | ||
| TransferHelper.safeTransfer(token, fallbackReceiver, feeToAvatarDAO); | ||
| } | ||
| } else { | ||
| if (token == WETH) { | ||
| IWETH(WETH).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 != WETH && _isSwapPossible(token, WETH, amount)) { | ||
| // If it is not WETH and there is a direct path to WETH, swap tokens | ||
| uint256 amountOut = _swapTokensForETH(amount, token); | ||
| _splitAndTransferFee(pair, WETH, amountOut); | ||
| } else { | ||
| // If it is WETH or there is not a direct path from token to WETH, transfer tokens | ||
| _splitAndTransferFee(pair, token, amount); | ||
| } | ||
| } | ||
|
|
||
| // Take what was charged as protocol fee from the DXswap pair liquidity | ||
| function takeProtocolFee(IDXswapPair[] calldata pairs) external { | ||
| for (uint i = 0; i < pairs.length; i++) { | ||
| 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))); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO we need to perform some validation on the |
||
| (uint amount0, uint amount1) = pairs[i].burn(address(this)); | ||
| if (amount0 > 0) | ||
| _takeETHorToken(token0, amount0); | ||
| if (amount1 > 0) | ||
| _takeETHorToken(token1, amount1); | ||
| (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 maximum swap price impact allowed for single token-weth swap (within 0-100% range) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should maybe clarify what number correspond to what percentages |
||
| function setMaxSwapPriceImpact(uint32 _maxSwapPriceImpact) external { | ||
| require(msg.sender == owner, 'DXswapFeeReceiver: CALLER_NOT_OWNER'); | ||
| require(_maxSwapPriceImpact > 0 && _maxSwapPriceImpact < 10000, 'DXswapFeeReceiver: FORBIDDEN_PRICE_IMPACT'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first check
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed it's not needed for unsigned integers, but this check is done to not to set |
||
| maxSwapPriceImpact = _maxSwapPriceImpact; | ||
| } | ||
|
|
||
| // called by the owner to set external fee receiver address | ||
| function setExternalFeeReceiver(address _pair, address _externalReceiver) external { | ||
| require(msg.sender == owner, 'DXswapFeeReceiver: CALLER_NOT_OWNER'); | ||
| externalFeeReceivers[_pair].externalReceiver = _externalReceiver; | ||
| } | ||
|
|
||
| // called by the owner to set fee percentage to external receiver | ||
| function setFeePercentageToExternalReceiver(address _pair, uint32 _feePercentageToExternalReceiver) external { | ||
| require(msg.sender == owner, 'DXswapFeeReceiver: CALLER_NOT_OWNER'); | ||
| IDXswapPair swapPair = IDXswapPair(_pair); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please let's not assume the input is a Swapr pair, not even if only the owner can perform the call here. |
||
| uint256 feeReceiverBalance = swapPair.balanceOf(address(this)); | ||
| if (feeReceiverBalance > 0) { | ||
| // withdraw accumulated fees before updating the split percentage | ||
| address token0 = swapPair.token0(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This common logic could probably be extracted in a function to avoid duplicated code. |
||
| 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, 'DXswapFeeReceiver: TOKENS_NOT_BURNED'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might fail. When performing |
||
|
|
||
| // fee percentage check | ||
| require( | ||
| _feePercentageToExternalReceiver >= 0 && _feePercentageToExternalReceiver <= 5000, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 'DXswapFeeReceiver: FORBIDDEN_FEE_PERCENTAGE_SPLIT' | ||
| ); | ||
| // update the split percentage for specific pair | ||
| externalFeeReceivers[_pair].feePercentage = _feePercentageToExternalReceiver; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd honestly upgrade to Solidity v0.8.13 or later if available and leverage both: