Skip to content
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

Make FeeHandler able to distribute tokens #10247

Merged
merged 22 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
489 changes: 224 additions & 265 deletions packages/protocol/contracts/common/FeeHandler.sol

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions packages/protocol/contracts/common/MentoFeeHandlerSeller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
pragma solidity ^0.5.13;

import "../common/interfaces/IFeeHandlerSeller.sol";

import "../stability/interfaces/IExchange.sol";
import "../stability/interfaces/ISortedOracles.sol";
import "./UsingRegistry.sol";
import "../stability/StableToken.sol";
import "../common/FixidityLib.sol";
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "../common/Initializable.sol";

contract MentoFeeHandlerSeller is IFeeHandlerSeller, UsingRegistry, Initializable {
using FixidityLib for FixidityLib.Fraction;

/**
* @notice Sets initialized == true on implementation contracts.
* @param test Set to true to skip implementation initialisation.
*/
constructor(bool test) public Initializable(test) {}

function initialize(address _registryAddress) external initializer {
_transferOwnership(msg.sender);
setRegistry(_registryAddress);
}

/**
* @notice Returns the storage, major, minor, and patch version of the contract.
* @return Storage version of the contract.
* @return Major version of the contract.
* @return Minor version of the contract.
* @return Patch version of the contract.
*/
function getVersionNumber() external pure returns (uint256, uint256, uint256, uint256) {
return (1, 1, 0, 0);
}

function sell(
address sellTokenAddress,
address buyToken,
uint256 amount,
uint256 maxSlippage // as fraction,
) external {
require(
buyToken == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID),
"Buy token can only be gold token"
);

martinvol marked this conversation as resolved.
Show resolved Hide resolved
StableToken stableToken = StableToken(sellTokenAddress);

address exchangeAddress = registry.getAddressForOrDie(stableToken.getExchangeRegistryId());

IExchange exchange = IExchange(exchangeAddress);

uint256 minAmount = 0;
if (maxSlippage != 0) {
// max slippage is set
// use sorted oracles as reference

// TODO check amount of reports or that the bucket hasn't been updated in 5 minutes
// safetyCheck() or modifier
ISortedOracles sortedOracles = getSortedOracles();
(uint256 rateNumerator, uint256 rateDenominator) = sortedOracles.medianRate(sellTokenAddress);
minAmount = calculateMinAmount(rateNumerator, rateDenominator, amount, maxSlippage);
}

// TODO an upgrade would be to compare using routers as well
stableToken.approve(exchangeAddress, amount);
exchange.sell(amount, minAmount, false);

IERC20 goldToken = getGoldToken();
// IERC20 stableAsERC20 = IERC20(sellTokenAddress);
goldToken.transfer(msg.sender, goldToken.balanceOf(address(this)));

}

// TODO move this to abstract class
function calculateMinAmount(
uint256 midPriceNumerator,
uint256 midPriceDenominator,
uint256 amount,
uint256 maxSlippage // as fraction
)
public
pure
returns (
// FixidityLib.Fraction memory maxSlippage
uint256
)
{
FixidityLib.Fraction memory maxSlippageFraction = FixidityLib.wrap(maxSlippage);

FixidityLib.Fraction memory price = FixidityLib.newFixedFraction(
midPriceNumerator,
midPriceDenominator
);
FixidityLib.Fraction memory amountFraction = FixidityLib.newFixed(amount);
FixidityLib.Fraction memory totalAmount = price.multiply(amountFraction);

return
totalAmount
.subtract((price.multiply(maxSlippageFraction)).multiply(amountFraction))
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: we can remove the inner brackets

.subtract(price.multiply(maxSlippageFraction).multiply(amountFraction))

.fromFixed();
}

function bestQuote(address token, uint256 balance) external {}

// in case some funds need to be returned or moved to another contract
function transfer(address token, uint256 amount, address to) external {}
}
158 changes: 158 additions & 0 deletions packages/protocol/contracts/common/UniswapFeeHandlerSeller.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
pragma solidity ^0.5.13;

import "../common/interfaces/IFeeHandlerSeller.sol";

import "../stability/interfaces/IExchange.sol";
import "../stability/interfaces/ISortedOracles.sol";
import "./UsingRegistry.sol";
import "../stability/StableToken.sol";
import "../common/FixidityLib.sol";
import "openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "../common/Initializable.sol";

import "../uniswap/interfaces/IUniswapV2RouterMin.sol";
import "../uniswap/interfaces/IUniswapV2FactoryMin.sol";

contract UniswapFeeHandlerSeller is IFeeHandlerSeller, UsingRegistry, Initializable {
using FixidityLib for FixidityLib.Fraction;
mapping(address => address[]) public routerAddresses;

uint256 constant MAX_TIMESTAMP_BLOCK_EXCHANGE = 20;

event ReceivedQuote(address router, uint256 quote);
event RouterUsed(address router);

/**
* @notice Sets initialized == true on implementation contracts.
* @param test Set to true to skip implementation initialisation.
*/
constructor(bool test) public Initializable(test) {}

function initialize(address _registryAddress) external initializer {
_transferOwnership(msg.sender);
setRegistry(_registryAddress);
}

// This function explicitly defines few variables because it was getting error "stack too deep"
function sell(
address sellTokenAddress,
address buyToken,
uint256 amount,
uint256 maxSlippage // as fraction,
) external {
require(
buyToken == registry.getAddressForOrDie(GOLD_TOKEN_REGISTRY_ID),
"Buy token can only be gold token"
);

// An improvement to this function would be to allow the user to pass a path as argument
// and if it generates a better outcome that the ones enabled that gets used
// and the user gets a reward

IERC20 goldToken = getGoldToken();
// address celoAddress = address(getGoldToken());

uint256 bestRouterIndex = 0;
uint256 bestRouterQuote = 0;

address[] memory path = new address[](2);
// address[] memory thisTokenRouterAddresses = routerAddresses[sellTokenAddress];

require(routerAddresses[sellTokenAddress].length > 0, "routerAddresses should be non empty");
martinvol marked this conversation as resolved.
Show resolved Hide resolved

// IERC20 token = IERC20(sellTokenAddress);
uint256 balanceToBurn = IERC20(sellTokenAddress).balanceOf(address(this));
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we check whether the balanceToBurn > 0 ?

Copy link
Contributor Author

@martinvol martinvol Apr 11, 2023

Choose a reason for hiding this comment

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

This contract assumes a bunch of checks were done by FeeHandler.sol, so those checks would be redundant.


for (uint256 i = 0; i < routerAddresses[sellTokenAddress].length; i++) {
address poolAddress = routerAddresses[sellTokenAddress][i];
IUniswapV2RouterMin router = IUniswapV2RouterMin(poolAddress);

path[0] = sellTokenAddress;
path[1] = address(goldToken);

// using the second return value becuase it's the last argument
// the previous values show how many tokens are exchanged in each path
// so the first value would be equivalent to balanceToBurn
uint256 wouldGet = router.getAmountsOut(balanceToBurn, path)[1];
emit ReceivedQuote(poolAddress, wouldGet);
if (wouldGet > bestRouterQuote) {
bestRouterQuote = wouldGet;
bestRouterIndex = i;
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of saving bestRouterIndex we could save

bestRouterAddress
bestRouter

In such case we wouldn't have to retrieve it from storage on line 89 again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

great point, this piece of code is not currently in use, so will fix after merging

}
}

// don't try to exchange on zero quotes
if (bestRouterQuote == 0) {
return;
}

address bestRouterAddress = routerAddresses[sellTokenAddress][bestRouterIndex];
IUniswapV2RouterMin bestRouter = IUniswapV2RouterMin(bestRouterAddress);

uint256 minAmount = 0;
if (maxSlippage != 0) {
address pair = IUniswapV2FactoryMin(bestRouter.factory()).getPair(
sellTokenAddress,
address(goldToken)
);
minAmount = calculateMinAmount(
IERC20(sellTokenAddress).balanceOf(pair),
goldToken.balanceOf(pair),
amount,
maxSlippage
);
}

IERC20(sellTokenAddress).approve(bestRouterAddress, balanceToBurn);
bestRouter.swapExactTokensForTokens(
balanceToBurn,
minAmount,
path,
address(this),
block.timestamp + MAX_TIMESTAMP_BLOCK_EXCHANGE
);

goldToken.transfer(msg.sender, goldToken.balanceOf(address(this)));
emit RouterUsed(bestRouterAddress);
// _sendBackCeloBalance();
}

// function _sendBackCeloBalance() private {
// IERC20 goldToken = getGoldToken();
// // IERC20 stableAsERC20 = IERC20(sellTokenAddress);
// }

// TODO move this to abstract class
function calculateMinAmount(
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this method is identical in MentoFeeHandlerSeller and UniswapFeeHandlerSeller we could extract it to library

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will fix after merging

uint256 midPriceNumerator,
uint256 midPriceDenominator,
uint256 amount,
uint256 maxSlippage // as fraction
)
public
pure
returns (
// FixidityLib.Fraction memory maxSlippage
uint256
)
{
FixidityLib.Fraction memory maxSlippageFraction = FixidityLib.wrap(maxSlippage);

FixidityLib.Fraction memory price = FixidityLib.newFixedFraction(
midPriceNumerator,
midPriceDenominator
);
FixidityLib.Fraction memory amountFraction = FixidityLib.newFixed(amount);
FixidityLib.Fraction memory totalAmount = price.multiply(amountFraction);

return
totalAmount
.subtract((price.multiply(maxSlippageFraction)).multiply(amountFraction))
.fromFixed();
}

function bestQuote(address token, uint256 balance) external {}

// in case some funds need to be returned or moved to another contract
function transfer(address token, uint256 amount, address to) external {}
}
47 changes: 47 additions & 0 deletions packages/protocol/contracts/common/interfaces/IFeeHandler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pragma solidity ^0.5.13;

import "../FixidityLib.sol";

interface IFeeHandler {
// FixidityLib.Fraction external burnFraction; // TODO move this to implementation

// // address that will get a fraction of the fees, currently the GreenFund.
// address external feesBeneficiary;

// tracks how much should be transfered and how much burn
// mapping(address => uint256) external tokensToDistribute;

// sets the portion of the fee that should be burned.
function setBurnFraction(uint256 fraction) external;

function addToken(address tokenAddress, address handlerAddress) external;
// function removeToken(address tokenAddress) external;

// function changeHandler(address tokenAddress, address handlerAddress) external;

// // marks token to be handled in "handleAll())
// function activateToken(address tokenAddress) external;
// function deactivateToken(address tokenAddress) external;

function sell(address tokenAddress) external;

// // calls exchange(tokenAddress), and distribute(tokenAddress)
function handle(address tokenAddress) external;

// // main entrypoint for a burn, iterates over token and calles handle
function handleAll() external;

// Sends the balance of token at tokenAddress to feesBeneficiary,
// according to the entry tokensToDistribute[tokenAddress]
function distribute(address tokenAddress) external;

// // burns the balance of Celo in the contract minus the entry of tokensToDistribute[CeloAddress]
function burnCelo() external;

// // calls distribute for all the nonCeloTokens
// function distributeAll() external;

// // in case some funds need to be returned or moved to another contract
// function transfer(address token, uint256 amount, address to) external;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
pragma solidity ^0.5.13;

import "../FixidityLib.sol";

interface IFeeHandlerSeller {
function sell(
address sellTokenAddress,
address buyTokenAddress,
uint256 amount,
uint256 minAmount
) external;
function bestQuote(address token, uint256 balance) external;

// in case some funds need to be returned or moved to another contract
function transfer(address token, uint256 amount, address to) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pragma solidity ^0.5.13;

import "../Proxy.sol";

/* solhint-disable-next-line no-empty-blocks */
contract MentoFeeHandlerSellerProxy is Proxy {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pragma solidity ^0.5.13;

import "../Proxy.sol";

/* solhint-disable-next-line no-empty-blocks */
contract UniswapSellerProxy is Proxy {}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ library UniswapV2Library {
view
returns (uint256 reserveA, uint256 reserveB)
{
(address token0, address _) = sortTokens(tokenA, tokenB);
(address token0, ) = sortTokens(tokenA, tokenB);

// require(false, "revert here");
(uint256 reserve0, uint256 reserve1, ) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB))
Expand Down
1 change: 1 addition & 0 deletions packages/protocol/lib/registry-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum CeloContractName {
ExchangeBRL = 'ExchangeBRL',
FederatedAttestations = 'FederatedAttestations',
FeeHandler = 'FeeHandler',
MentoFeeHandlerSeller = 'MentoFeeHandlerSeller',
FeeCurrencyWhitelist = 'FeeCurrencyWhitelist',
Freezer = 'Freezer',
GasPriceMinimum = 'GasPriceMinimum',
Expand Down
15 changes: 15 additions & 0 deletions packages/protocol/migrations/26_00_mento_fee_handler_seller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CeloContractName } from '@celo/protocol/lib/registry-utils'
import { deploymentForCoreContract } from '@celo/protocol/lib/web3-utils'
import { config } from '@celo/protocol/migrationsConfig'
import { MentoFeeHandlerSellerInstance } from 'types'

const initializeArgs = async () => {
return [config.registry.predeployedProxyAddress]
}

module.exports = deploymentForCoreContract<MentoFeeHandlerSellerInstance>(
web3,
artifacts,
CeloContractName.MentoFeeHandlerSeller,
initializeArgs
)
Loading