Skip to content

Conversation

@karczuRF
Copy link
Collaborator

Summary

Closes #39
The goal is to upgrade FeeReceiver contract to solve two issues:

  • trying to liquidate in pair with small liquidity
  • add protocol fee split feature

New features

  • change DXswapFeeReceiver contract to protect from situation where takeProtocolFee function tries to swap token in pair with too small liquidity
  • allow to control fee split and external receiver for each pair
  • supports max one external receiver address
  • no deadline to claim protocol fee

(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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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:

amount.mul(10000).div(reserveIn); 

or

amount * 10000 / reserveIn

address public WETH;
address public ethReceiver;
address public fallbackReceiver;
uint32 public maxSwapPriceImpact = 100; // uses default 1% as max allowed price impact for takeProtocolFee swap
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is expressed in basis points it can probably be a uint16.


address public owner;
IDXswapFactory public factory;
address public WETH;
Copy link

Choose a reason for hiding this comment

The 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 nativeCurrencyWrapper or something like that.

P.S.: deriving from the below logic, it looks like WETH should indeed be called in a more network-neutral way.

Copy link
Member

@jpkcambridge jpkcambridge Aug 18, 2022

Choose a reason for hiding this comment

The 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.

Copy link

@luzzif luzzif Aug 29, 2022

Choose a reason for hiding this comment

The 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:

  1. If we want to keep the current logic unchanged, another contract would be needed, dealing with a standard ERC20 token for example.
  2. Even though this option would alter the logic a little bit (in my opinion in very acceptable or even desirable ways) I'd avoid calling withdraw on the wrapper when executing _takeETHorToken, have the contract send wrapped native currency to the recipient and convert the above variable to a standard ERC20 named for example targetToken or something. This is desirable to simplify the logic, reducing gas consumption and attack surface. As is, for each harvested LP token that is convertible to the native currency (or, actually, its wrapper) we also perform the withdraw. It might be more sensible to offload the withdraw to the recipient for it to be performed only when actually needed.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the second option makes sense!
Ok, so if we decide not to swap to ETH at all then the _swapTokensForETH function can be removed as well as withdraw + safeTransferETH. Instead the new argument will be added like targetToken. By default feeReceiver will try to swap to targetToken which is ERC20 and if it's not possible send LP to the fallbackReceiver.
Am I right or misunderstood sth?

address _owner, address _factory, address _WETH, address _ethReceiver, address _fallbackReceiver
address _owner,
address _factory,
address _WETH,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If above applies, this should be changed too.

import './interfaces/IDXswapFactory.sol';
import './interfaces/IDXswapPair.sol';
import './interfaces/IWETH.sol';
import './libraries/TransferHelper.sol';
Copy link

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:

  1. The fact that with Sol >=v0.8.0, safe math is not really required anymore since under/overflow checks are automatically performed.
  2. The Yul optimizer is production ready and I'd use it to shave off some gas consumption.

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should maybe clarify what number correspond to what percentages

(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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see more if possible.


address public owner;
IDXswapFactory public factory;
address public WETH;
Copy link

@luzzif luzzif Aug 29, 2022

Choose a reason for hiding this comment

The 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:

  1. If we want to keep the current logic unchanged, another contract would be needed, dealing with a standard ERC20 token for example.
  2. Even though this option would alter the logic a little bit (in my opinion in very acceptable or even desirable ways) I'd avoid calling withdraw on the wrapper when executing _takeETHorToken, have the contract send wrapped native currency to the recipient and convert the above variable to a standard ERC20 named for example targetToken or something. This is desirable to simplify the logic, reducing gas consumption and attack surface. As is, for each harvested LP token that is convertible to the native currency (or, actually, its wrapper) we also perform the withdraw. It might be more sensible to offload the withdraw to the recipient for it to be performed only when actually needed.


mapping(address => ExternalFeeReceiver) public externalFeeReceivers;

event TakeProtocolFee(address indexed sender, address indexed to, uint256 NumberOfPairs);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change NumberOfPairs to camel case.

function() external payable {}

// called by the owner to set the new owner
function transferOwnership(address newOwner) external {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually becoming a fan of the acceptOwnership method of doing ownership transfers. In case ownership of a contract is delicate, better to implement this pattern with increased security.

function isContract(address addr) internal returns (bool) {
uint size;
assembly { size := extcodesize(addr) }
function isContract(address addr) internal view returns (bool) {
Copy link

Choose a reason for hiding this comment

The 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 reserveIn, uint256 reserveOut) = token0 < token1 ? (reserve0, reserve1) : (reserve1, reserve0);
if (reserveIn == 0 || reserveOut == 0) return false;

uint256 priceImpact = amount.mul(10000) / reserveIn; // simplified formula
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see more if possible.

// called by the owner to set maximum swap price impact allowed for single token-weth swap (within 0-100% range)
function setMaxSwapPriceImpact(uint32 _maxSwapPriceImpact) external {
require(msg.sender == owner, 'DXswapFeeReceiver: CALLER_NOT_OWNER');
require(_maxSwapPriceImpact > 0 && _maxSwapPriceImpact < 10000, 'DXswapFeeReceiver: FORBIDDEN_PRICE_IMPACT');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first check _maxSwapPriceImpact > 0 is not needed since we're dealing with unsigned integers.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 priceImpact to zero.

// 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);
Copy link

Choose a reason for hiding this comment

The 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();
Copy link

Choose a reason for hiding this comment

The 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.

if (amount1 > 0) _takeTokenOrETH(address(swapPair), token1, amount1);
emit TakeProtocolFee(msg.sender, ethReceiver, 1);
}
require(swapPair.balanceOf(address(this)) == 0, 'DXswapFeeReceiver: TOKENS_NOT_BURNED');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might fail. When performing burn some fee tokens might be minted depending on the pair's activity.


// fee percentage check
require(
_feePercentageToExternalReceiver >= 0 && _feePercentageToExternalReceiver <= 5000,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_feePercentageToExternalReceiver >= 0 can be removed, dealing with unsigned integers.

@karczuRF karczuRF marked this pull request as ready for review December 16, 2022 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade Fee Receiver

5 participants