From 2ce482a6ed833a50a61c06188b8f03f515eee7dd Mon Sep 17 00:00:00 2001 From: OxMarco <> Date: Fri, 23 Jun 2023 00:35:04 +0200 Subject: [PATCH 1/5] separate responsibilities --- src/interfaces/IBalancerPoolManager.sol | 26 +++ src/libraries/BalancerHelper.sol | 102 ++++++++++- src/services/debit/BalancerService.sol | 163 ++++-------------- .../debit/utils/BalancerPoolManager.sol | 78 +++++++++ 4 files changed, 234 insertions(+), 135 deletions(-) create mode 100644 src/interfaces/IBalancerPoolManager.sol create mode 100644 src/services/debit/utils/BalancerPoolManager.sol diff --git a/src/interfaces/IBalancerPoolManager.sol b/src/interfaces/IBalancerPoolManager.sol new file mode 100644 index 00000000..d7a5d15e --- /dev/null +++ b/src/interfaces/IBalancerPoolManager.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.18; + +interface IBalancerPoolManager { + struct PoolData { + bytes32 balancerPoolID; + address[] tokens; + uint256[] weights; + uint256[] scalingFactors; + uint256 maximumWeightIndex; + uint8 length; + uint256 swapFee; + address gauge; + address protocolFeeCollector; + } + + event PoolWasAdded(address indexed balancerPool); + event PoolWasRemoved(address indexed balancerPool); + error InexistentPool(); + + function getPool(address pool) external view returns (PoolData memory); + + function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external; + + function removePool(address poolAddress) external; +} diff --git a/src/libraries/BalancerHelper.sol b/src/libraries/BalancerHelper.sol index 02fd1bf3..675333f4 100644 --- a/src/libraries/BalancerHelper.sol +++ b/src/libraries/BalancerHelper.sol @@ -1,6 +1,10 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.18; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IBalancerVault } from "../interfaces/external/balancer/IBalancerVault.sol"; +import { IBalancerPool } from "../interfaces/external/balancer/IBalancerPool.sol"; +import { IProtocolFeesCollector } from "../interfaces/external/balancer/IProtocolFeesCollector.sol"; import { FloatingPointMath } from "./FloatingPointMath.sol"; import { WeightedMath } from "./external/Balancer/WeightedMath.sol"; @@ -12,7 +16,7 @@ library BalancerHelper { uint256[] memory balances, uint256 bptAmountIn, uint256 totalSupply - ) public pure returns (uint256[] memory) { + ) internal pure returns (uint256[] memory) { uint256[] memory amountsOut = WeightedMath._calcTokensOutGivenExactBptIn(balances, bptAmountIn, totalSupply); return amountsOut; } @@ -23,7 +27,7 @@ library BalancerHelper { uint256[] memory amountsOut, uint256 totalSupply, uint256 swapFee - ) public pure returns (uint256) { + ) internal pure returns (uint256) { // _upscaleArray(amountsOut); uint256 bptAmountIn = WeightedMath._calcBptInGivenExactTokensOut( @@ -37,11 +41,103 @@ library BalancerHelper { return bptAmountIn; } - function getTokenIndex(address[] memory tokens, address token) public pure returns (uint8) { + function getTokenIndex(address[] memory tokens, address token) internal pure returns (uint8) { for (uint8 i = 0; i < tokens.length; i++) { if (tokens[i] == token) return i; } return type(uint8).max; } + + function _modifyBalancesWithFees( + address poolAddress, + address protocolFeeCollector, + uint256[] memory scalingFactors, + uint256[] memory balances, + uint256[] memory normalizedWeights, + uint256 maximumWeightIndex + ) internal view { + uint256 length = scalingFactors.length; + for (uint256 i = 0; i < length; i++) balances[i] *= scalingFactors[i]; + + uint256[] memory dueProtocolFeeAmounts = new uint256[](length); + dueProtocolFeeAmounts[maximumWeightIndex] = WeightedMath._calcDueTokenProtocolSwapFeeAmount( + balances[maximumWeightIndex], + normalizedWeights[maximumWeightIndex], + IBalancerPool(poolAddress).getLastInvariant(), + WeightedMath._calculateInvariant(normalizedWeights, balances), + IProtocolFeesCollector(protocolFeeCollector).getSwapFeePercentage() + ); + + balances[maximumWeightIndex] -= dueProtocolFeeAmounts[maximumWeightIndex]; + } + + // Assumes balances are already upscaled and downscales them back together with balances + function calculateExpectedBPTToExit( + address poolAddress, + uint256[] memory balances, + uint256[] memory amountsOut, + uint256[] memory scalingFactors, + address protocolFeeCollector, + uint256 maximumWeightIndex + ) internal view returns (uint256) { + uint256 length = scalingFactors.length; + uint256[] memory normalizedWeights = IBalancerPool(poolAddress).getNormalizedWeights(); + _modifyBalancesWithFees( + poolAddress, + protocolFeeCollector, + scalingFactors, + balances, + normalizedWeights, + maximumWeightIndex + ); + + for (uint256 i = 0; i < length; i++) amountsOut[i] *= scalingFactors[i]; + + uint256 expectedBpt = WeightedMath._calcBptInGivenExactTokensOut( + balances, + normalizedWeights, + amountsOut, + IERC20(poolAddress).totalSupply(), + IBalancerPool(poolAddress).getSwapFeePercentage() + ); + + for (uint256 i = 0; i < length; i++) { + amountsOut[i] /= scalingFactors[i]; + balances[i] /= scalingFactors[i]; + } + + return expectedBpt; + } + + // Assumes balances are already upscaled and downscales them back together with balances + function calculateExpectedTokensFromBPT( + address poolAddress, + address protocolFeeCollector, + uint256[] memory balances, + uint256[] memory scalingFactors, + uint256[] memory normalizedWeights, + uint256 maximumWeightIndex, + uint256 amount, + uint256 totalSupply + ) internal view returns (uint256[] memory) { + uint256[] memory normalizedWeights = IBalancerPool(poolAddress).getNormalizedWeights(); + + _modifyBalancesWithFees( + poolAddress, + protocolFeeCollector, + scalingFactors, + balances, + normalizedWeights, + maximumWeightIndex + ); + uint256[] memory expectedTokens = WeightedMath._calcTokensOutGivenExactBptIn(balances, amount, totalSupply); + + for (uint256 i = 0; i < scalingFactors.length; i++) { + expectedTokens[i] /= scalingFactors[i]; + balances[i] /= scalingFactors[i]; + } + + return expectedTokens; + } } diff --git a/src/services/debit/BalancerService.sol b/src/services/debit/BalancerService.sol index ef57f80f..649c2d21 100644 --- a/src/services/debit/BalancerService.sol +++ b/src/services/debit/BalancerService.sol @@ -4,6 +4,7 @@ pragma solidity =0.8.18; import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IOracle } from "../../interfaces/IOracle.sol"; +import { IBalancerPoolManager } from "../../interfaces/IBalancerPoolManager.sol"; import { IFactory } from "../../interfaces/external/wizardex/IFactory.sol"; import { IPool } from "../../interfaces/external/wizardex/IPool.sol"; import { IBalancerVault } from "../../interfaces/external/balancer/IBalancerVault.sol"; @@ -14,6 +15,7 @@ import { BalancerHelper } from "../../libraries/BalancerHelper.sol"; import { VaultHelper } from "../../libraries/VaultHelper.sol"; import { WeightedMath } from "../../libraries/external/Balancer/WeightedMath.sol"; import { AuctionRateModel } from "../../irmodels/AuctionRateModel.sol"; +import { BalancerPoolManager } from "./utils/BalancerPoolManager.sol"; import { Service } from "../Service.sol"; import { DebitService } from "../DebitService.sol"; import { Whitelisted } from "../Whitelisted.sol"; @@ -25,31 +27,17 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { using SafeERC20 for IERC20; using SafeERC20 for IBalancerPool; - struct PoolData { - bytes32 balancerPoolID; - address[] tokens; - uint256[] weights; - uint256[] scalingFactors; - uint256 maximumWeightIndex; - uint8 length; - uint256 swapFee; - address gauge; - address protocolFeeCollector; - } - - event PoolWasAdded(address indexed balancerPool); - event PoolWasRemoved(address indexed balancerPool); - error InexistentPool(); error TokenIndexMismatch(); error SlippageError(); - mapping(address => PoolData) public pools; + IBalancerPoolManager internal immutable poolManager; IBalancerVault internal immutable balancerVault; uint256 public rewardRate; address public immutable bal; IOracle public immutable oracle; IFactory public immutable dex; + mapping(address => IBalancerPoolManager.PoolData) public pools; constructor( address _manager, @@ -63,11 +51,12 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { dex = IFactory(_factory); balancerVault = IBalancerVault(_balancerVault); bal = _bal; + + poolManager = new BalancerPoolManager(_balancerVault); } function _open(Agreement memory agreement, bytes memory /*data*/) internal override { - PoolData memory pool = pools[agreement.collaterals[0].token]; - if (pool.length == 0) revert InexistentPool(); + IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); uint256[] memory amountsIn = new uint256[](agreement.loans.length); address[] memory tokens = new address[](agreement.loans.length); @@ -97,7 +86,7 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function _close(uint256 /*tokenID*/, Agreement memory agreement, bytes memory data) internal override { - PoolData memory pool = pools[agreement.collaterals[0].token]; + IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); // TODO: add check on fees to be sure amountOut is not too little if (pool.gauge != address(0)) IGauge(pool.gauge).withdraw(agreement.collaterals[0].amount, true); @@ -144,8 +133,7 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function quote(Agreement memory agreement) public view override returns (uint256[] memory) { - PoolData memory pool = pools[agreement.collaterals[0].token]; - if (pool.length == 0) revert InexistentPool(); + IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); (, uint256[] memory totalBalances, ) = balancerVault.getPoolTokens(pool.balancerPoolID); uint256[] memory amountsOut = new uint256[](agreement.loans.length); @@ -154,7 +142,15 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { amountsOut[index] = agreement.loans[index].amount; } // Calculate needed BPT to repay loan + fees - uint256 bptAmountOut = _calculateExpectedBPTToExit(agreement.collaterals[0].token, totalBalances, amountsOut); + uint256 bptAmountOut = BalancerHelper.calculateExpectedBPTToExit( + agreement.collaterals[0].token, + totalBalances, + amountsOut, + pool.scalingFactors, + pool.protocolFeeCollector, + pool.maximumWeightIndex + ); + // The remaining BPT is swapped to obtain profit // We need to update the balances since we virtually took tokens out of the pool // We also need to update the total supply since the bptOut were burned @@ -162,9 +158,13 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { totalBalances[index] -= amountsOut[index]; } if (bptAmountOut < agreement.collaterals[0].amount) { - profits = _calculateExpectedTokensFromBPT( + profits = BalancerHelper.calculateExpectedTokensFromBPT( agreement.collaterals[0].token, + pool.protocolFeeCollector, totalBalances, + pool.scalingFactors, + pool.weights, + pool.maximumWeightIndex, agreement.collaterals[0].amount - bptAmountOut, IERC20(agreement.collaterals[0].token).totalSupply() - bptAmountOut ); @@ -177,55 +177,8 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { return profits; } - function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external onlyOwner { - assert(poolAddress != address(0)); - - (address[] memory poolTokens, , ) = balancerVault.getPoolTokens(balancerPoolID); - uint256 length = poolTokens.length; - assert(length > 0); - - IBalancerPool bpool = IBalancerPool(poolAddress); - bpool.safeApprove(gauge, type(uint256).max); - - uint256 fee = bpool.getSwapFeePercentage(); - uint256[] memory weights = bpool.getNormalizedWeights(); - uint256 maxWeightTokenIndex = 0; - uint256[] memory scalingFactors = new uint256[](length); - - for (uint8 i = 0; i < length; i++) { - if (IERC20(poolTokens[i]).allowance(address(this), address(balancerVault)) == 0) - IERC20(poolTokens[i]).safeApprove(address(balancerVault), type(uint256).max); - if (weights[i] > weights[maxWeightTokenIndex]) maxWeightTokenIndex = i; - scalingFactors[i] = 10 ** (18 - IERC20Metadata(poolTokens[i]).decimals()); - } - - pools[poolAddress] = PoolData( - balancerPoolID, - poolTokens, - weights, - scalingFactors, - maxWeightTokenIndex, - uint8(length), - fee, - gauge, - balancerVault.getProtocolFeesCollector() - ); - - emit PoolWasAdded(poolAddress); - } - - function removePool(address poolAddress) external onlyOwner { - PoolData memory pool = pools[poolAddress]; - assert(pools[poolAddress].length != 0); - - IERC20(poolAddress).approve(pool.gauge, 0); - delete pools[poolAddress]; - - emit PoolWasRemoved(poolAddress); - } - function harvest(address poolAddress) external { - PoolData memory pool = pools[poolAddress]; + IBalancerPoolManager.PoolData memory pool = poolManager.getPool(poolAddress); if (pool.length == 0) revert InexistentPool(); IGauge(pool.gauge).claim_rewards(address(this)); @@ -240,70 +193,16 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { // TODO add premium to the caller } - function _modifyBalancesWithFees( - address poolAddress, - uint256[] memory balances, - uint256[] memory normalizedWeights - ) internal view { - PoolData memory pool = pools[poolAddress]; - - for (uint256 i = 0; i < pool.length; i++) balances[i] *= pool.scalingFactors[i]; - - uint256[] memory dueProtocolFeeAmounts = new uint256[](pool.length); - dueProtocolFeeAmounts[pool.maximumWeightIndex] = WeightedMath._calcDueTokenProtocolSwapFeeAmount( - balances[pool.maximumWeightIndex], - normalizedWeights[pool.maximumWeightIndex], - IBalancerPool(poolAddress).getLastInvariant(), - WeightedMath._calculateInvariant(normalizedWeights, balances), - IProtocolFeesCollector(pool.protocolFeeCollector).getSwapFeePercentage() + function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external onlyOwner { + // We need a delegate call as we are giving tokens approvals + address(poolManager).delegatecall( + abi.encodeWithSelector(IBalancerPoolManager.addPool.selector, poolAddress, balancerPoolID, gauge) ); - - balances[pool.maximumWeightIndex] -= dueProtocolFeeAmounts[pool.maximumWeightIndex]; } - // Assumes balances are already upscaled and downscales them back together with balances - function _calculateExpectedBPTToExit( - address poolAddress, - uint256[] memory balances, - uint256[] memory amountsOut - ) internal view returns (uint256) { - PoolData memory pool = pools[poolAddress]; - uint256[] memory normalizedWeights = IBalancerPool(poolAddress).getNormalizedWeights(); - _modifyBalancesWithFees(poolAddress, balances, normalizedWeights); - - for (uint256 i = 0; i < pool.length; i++) amountsOut[i] *= pool.scalingFactors[i]; - - uint256 expectedBpt = WeightedMath._calcBptInGivenExactTokensOut( - balances, - normalizedWeights, - amountsOut, - IERC20(poolAddress).totalSupply(), - IBalancerPool(poolAddress).getSwapFeePercentage() + function removePool(address poolAddress) external onlyOwner { + address(poolManager).delegatecall( + abi.encodeWithSelector(IBalancerPoolManager.removePool.selector, poolAddress) ); - for (uint256 i = 0; i < pool.length; i++) { - amountsOut[i] /= pool.scalingFactors[i]; - balances[i] /= pool.scalingFactors[i]; - } - return expectedBpt; - } - - // Assumes balances are already upscaled and downscales them back together with balances - function _calculateExpectedTokensFromBPT( - address poolAddress, - uint256[] memory balances, - uint256 amount, - uint256 totalSupply - ) internal view returns (uint256[] memory) { - PoolData memory pool = pools[poolAddress]; - uint256[] memory normalizedWeights = IBalancerPool(poolAddress).getNormalizedWeights(); - _modifyBalancesWithFees(poolAddress, balances, normalizedWeights); - uint256[] memory expectedTokens = WeightedMath._calcTokensOutGivenExactBptIn(balances, amount, totalSupply); - - for (uint256 i = 0; i < pool.length; i++) { - expectedTokens[i] /= pool.scalingFactors[i]; - balances[i] /= pool.scalingFactors[i]; - } - - return expectedTokens; } } diff --git a/src/services/debit/utils/BalancerPoolManager.sol b/src/services/debit/utils/BalancerPoolManager.sol new file mode 100644 index 00000000..68dfafc8 --- /dev/null +++ b/src/services/debit/utils/BalancerPoolManager.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.18; + +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IBalancerPool } from "../../../interfaces/external/balancer/IBalancerPool.sol"; +import { IBalancerVault } from "../../../interfaces/external/balancer/IBalancerVault.sol"; +import { IBalancerPoolManager } from "../../../interfaces/IBalancerPoolManager.sol"; + +contract BalancerPoolManager is IBalancerPoolManager { + using SafeERC20 for IERC20; + + address public immutable owner; + IBalancerVault internal immutable balancerVault; + mapping(address => PoolData) public pools; + + constructor(address _balancerVault) { + balancerVault = IBalancerVault(_balancerVault); + owner = msg.sender; + } + + modifier onlyOwner() { + assert(msg.sender == owner); + _; + } + + function getPool(address poolAddress) external view override returns (PoolData memory) { + if (pools[poolAddress].length == 0) revert InexistentPool(); + return pools[poolAddress]; + } + + function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external override onlyOwner { + assert(poolAddress != address(0)); + + (address[] memory poolTokens, , ) = balancerVault.getPoolTokens(balancerPoolID); + uint256 length = poolTokens.length; + assert(length > 0); + + IBalancerPool bpool = IBalancerPool(poolAddress); + bpool.approve(gauge, type(uint256).max); + + uint256 fee = bpool.getSwapFeePercentage(); + uint256[] memory weights = bpool.getNormalizedWeights(); + uint256 maxWeightTokenIndex = 0; + uint256[] memory scalingFactors = new uint256[](length); + + for (uint8 i = 0; i < length; i++) { + if (IERC20(poolTokens[i]).allowance(address(this), address(balancerVault)) == 0) + IERC20(poolTokens[i]).safeApprove(address(balancerVault), type(uint256).max); + if (weights[i] > weights[maxWeightTokenIndex]) maxWeightTokenIndex = i; + scalingFactors[i] = 10 ** (18 - IERC20Metadata(poolTokens[i]).decimals()); + } + + pools[poolAddress] = PoolData( + balancerPoolID, + poolTokens, + weights, + scalingFactors, + maxWeightTokenIndex, + uint8(length), + fee, + gauge, + balancerVault.getProtocolFeesCollector() + ); + + emit PoolWasAdded(poolAddress); + } + + function removePool(address poolAddress) external override onlyOwner { + PoolData memory pool = pools[poolAddress]; + assert(pools[poolAddress].length != 0); + + IERC20(poolAddress).approve(pool.gauge, 0); + delete pools[poolAddress]; + + emit PoolWasRemoved(poolAddress); + } +} From a14b4dc1aa8766d2b2cb74bacb13fbfdb7ffb521 Mon Sep 17 00:00:00 2001 From: OxMarco <> Date: Fri, 23 Jun 2023 10:53:15 +0200 Subject: [PATCH 2/5] split contracts --- src/interfaces/IBalancerPoolManager.sol | 3 --- src/services/debit/BalancerService.sol | 18 ++++++++++-------- .../debit/utils/BalancerPoolManager.sol | 5 ----- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/interfaces/IBalancerPoolManager.sol b/src/interfaces/IBalancerPoolManager.sol index d7a5d15e..6b49b77f 100644 --- a/src/interfaces/IBalancerPoolManager.sol +++ b/src/interfaces/IBalancerPoolManager.sol @@ -16,9 +16,6 @@ interface IBalancerPoolManager { event PoolWasAdded(address indexed balancerPool); event PoolWasRemoved(address indexed balancerPool); - error InexistentPool(); - - function getPool(address pool) external view returns (PoolData memory); function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external; diff --git a/src/services/debit/BalancerService.sol b/src/services/debit/BalancerService.sol index 649c2d21..ba11f670 100644 --- a/src/services/debit/BalancerService.sol +++ b/src/services/debit/BalancerService.sol @@ -31,7 +31,7 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { error TokenIndexMismatch(); error SlippageError(); - IBalancerPoolManager internal immutable poolManager; + address internal immutable poolManager; IBalancerVault internal immutable balancerVault; uint256 public rewardRate; address public immutable bal; @@ -52,11 +52,12 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { balancerVault = IBalancerVault(_balancerVault); bal = _bal; - poolManager = new BalancerPoolManager(_balancerVault); + poolManager = address(new BalancerPoolManager(_balancerVault)); } function _open(Agreement memory agreement, bytes memory /*data*/) internal override { - IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); + IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; + if (pool.length == 0) revert InexistentPool(); uint256[] memory amountsIn = new uint256[](agreement.loans.length); address[] memory tokens = new address[](agreement.loans.length); @@ -86,7 +87,7 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function _close(uint256 /*tokenID*/, Agreement memory agreement, bytes memory data) internal override { - IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); + IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; // TODO: add check on fees to be sure amountOut is not too little if (pool.gauge != address(0)) IGauge(pool.gauge).withdraw(agreement.collaterals[0].amount, true); @@ -133,7 +134,8 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function quote(Agreement memory agreement) public view override returns (uint256[] memory) { - IBalancerPoolManager.PoolData memory pool = poolManager.getPool(agreement.collaterals[0].token); + IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; + if (pool.length == 0) revert InexistentPool(); (, uint256[] memory totalBalances, ) = balancerVault.getPoolTokens(pool.balancerPoolID); uint256[] memory amountsOut = new uint256[](agreement.loans.length); @@ -178,7 +180,7 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function harvest(address poolAddress) external { - IBalancerPoolManager.PoolData memory pool = poolManager.getPool(poolAddress); + IBalancerPoolManager.PoolData memory pool = pools[poolAddress]; if (pool.length == 0) revert InexistentPool(); IGauge(pool.gauge).claim_rewards(address(this)); @@ -195,13 +197,13 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external onlyOwner { // We need a delegate call as we are giving tokens approvals - address(poolManager).delegatecall( + poolManager.delegatecall( abi.encodeWithSelector(IBalancerPoolManager.addPool.selector, poolAddress, balancerPoolID, gauge) ); } function removePool(address poolAddress) external onlyOwner { - address(poolManager).delegatecall( + poolManager.delegatecall( abi.encodeWithSelector(IBalancerPoolManager.removePool.selector, poolAddress) ); } diff --git a/src/services/debit/utils/BalancerPoolManager.sol b/src/services/debit/utils/BalancerPoolManager.sol index 68dfafc8..df3019b6 100644 --- a/src/services/debit/utils/BalancerPoolManager.sol +++ b/src/services/debit/utils/BalancerPoolManager.sol @@ -24,11 +24,6 @@ contract BalancerPoolManager is IBalancerPoolManager { _; } - function getPool(address poolAddress) external view override returns (PoolData memory) { - if (pools[poolAddress].length == 0) revert InexistentPool(); - return pools[poolAddress]; - } - function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external override onlyOwner { assert(poolAddress != address(0)); From 9ead73924a131ba6890add574f6e5ed69259d444 Mon Sep 17 00:00:00 2001 From: OxMarco <> Date: Fri, 23 Jun 2023 11:08:37 +0200 Subject: [PATCH 3/5] harvester --- src/interfaces/IBalancerHarvester.sol | 6 +++ src/services/debit/BalancerService.sol | 38 ++++++++----------- .../debit/utils/BalancerHarvester.sol | 38 +++++++++++++++++++ 3 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 src/interfaces/IBalancerHarvester.sol create mode 100644 src/services/debit/utils/BalancerHarvester.sol diff --git a/src/interfaces/IBalancerHarvester.sol b/src/interfaces/IBalancerHarvester.sol new file mode 100644 index 00000000..ceea3c67 --- /dev/null +++ b/src/interfaces/IBalancerHarvester.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.18; + +interface IBalancerHarvester { + function harvest(address gauge, address[] memory tokens) external; +} diff --git a/src/services/debit/BalancerService.sol b/src/services/debit/BalancerService.sol index ba11f670..dc80c8bc 100644 --- a/src/services/debit/BalancerService.sol +++ b/src/services/debit/BalancerService.sol @@ -4,7 +4,6 @@ pragma solidity =0.8.18; import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { IOracle } from "../../interfaces/IOracle.sol"; -import { IBalancerPoolManager } from "../../interfaces/IBalancerPoolManager.sol"; import { IFactory } from "../../interfaces/external/wizardex/IFactory.sol"; import { IPool } from "../../interfaces/external/wizardex/IPool.sol"; import { IBalancerVault } from "../../interfaces/external/balancer/IBalancerVault.sol"; @@ -12,10 +11,10 @@ import { IBalancerPool } from "../../interfaces/external/balancer/IBalancerPool. import { IProtocolFeesCollector } from "../../interfaces/external/balancer/IProtocolFeesCollector.sol"; import { IGauge } from "../../interfaces/external/balancer/IGauge.sol"; import { BalancerHelper } from "../../libraries/BalancerHelper.sol"; -import { VaultHelper } from "../../libraries/VaultHelper.sol"; import { WeightedMath } from "../../libraries/external/Balancer/WeightedMath.sol"; import { AuctionRateModel } from "../../irmodels/AuctionRateModel.sol"; -import { BalancerPoolManager } from "./utils/BalancerPoolManager.sol"; +import { IBalancerPoolManager, BalancerPoolManager } from "./utils/BalancerPoolManager.sol"; +import { IBalancerHarvester, BalancerHarvester } from "./utils/BalancerHarvester.sol"; import { Service } from "../Service.sol"; import { DebitService } from "../DebitService.sol"; import { Whitelisted } from "../Whitelisted.sol"; @@ -32,11 +31,12 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { error SlippageError(); address internal immutable poolManager; + address internal immutable harvester; IBalancerVault internal immutable balancerVault; uint256 public rewardRate; address public immutable bal; - IOracle public immutable oracle; - IFactory public immutable dex; + address public immutable oracle; + address public immutable dex; mapping(address => IBalancerPoolManager.PoolData) public pools; constructor( @@ -47,12 +47,13 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { address _bal, uint256 _deadline ) Service("BalancerService", "BALANCER-SERVICE", _manager, _deadline) { - oracle = IOracle(_oracle); - dex = IFactory(_factory); + oracle = _oracle; + dex = _factory; balancerVault = IBalancerVault(_balancerVault); bal = _bal; poolManager = address(new BalancerPoolManager(_balancerVault)); + harvester = address(new BalancerHarvester(_oracle, _factory, manager, bal)); } function _open(Agreement memory agreement, bytes memory /*data*/) internal override { @@ -180,31 +181,24 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function harvest(address poolAddress) external { - IBalancerPoolManager.PoolData memory pool = pools[poolAddress]; - if (pool.length == 0) revert InexistentPool(); - - IGauge(pool.gauge).claim_rewards(address(this)); - - (address token, address vault) = VaultHelper.getBestVault(pool.tokens, manager); - // TODO check oracle - uint256 price = oracle.getPrice(bal, token, 1); - address dexPool = dex.pools(bal, token, 10); // TODO hardcoded tick - // TODO add discount - IPool(dexPool).createOrder(IERC20(bal).balanceOf(address(this)), price, vault, block.timestamp + 1 weeks); - - // TODO add premium to the caller + (bool success, bytes memory data) = harvester.delegatecall( + abi.encodeWithSelector(IBalancerHarvester.harvest.selector, poolAddress) + ); + require(success, string(data)); } function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external onlyOwner { // We need a delegate call as we are giving tokens approvals - poolManager.delegatecall( + (bool success, bytes memory data) = poolManager.delegatecall( abi.encodeWithSelector(IBalancerPoolManager.addPool.selector, poolAddress, balancerPoolID, gauge) ); + require(success, string(data)); } function removePool(address poolAddress) external onlyOwner { - poolManager.delegatecall( + (bool success, bytes memory data) = poolManager.delegatecall( abi.encodeWithSelector(IBalancerPoolManager.removePool.selector, poolAddress) ); + require(success, string(data)); } } diff --git a/src/services/debit/utils/BalancerHarvester.sol b/src/services/debit/utils/BalancerHarvester.sol new file mode 100644 index 00000000..6289c1da --- /dev/null +++ b/src/services/debit/utils/BalancerHarvester.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.18; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IManager } from "../../../interfaces/IManager.sol"; +import { IOracle } from "../../../interfaces/IOracle.sol"; +import { IFactory } from "../../../interfaces/external/wizardex/IFactory.sol"; +import { IPool } from "../../../interfaces/external/wizardex/IPool.sol"; +import { IGauge } from "../../../interfaces/external/balancer/IGauge.sol"; +import { VaultHelper } from "../../../libraries/VaultHelper.sol"; +import { IBalancerHarvester } from "../../../interfaces/IBalancerHarvester.sol"; + +contract BalancerHarvester is IBalancerHarvester { + IOracle public immutable oracle; + IFactory public immutable dex; + IManager public immutable manager; + address public immutable bal; + + constructor(address _oracle, address _dex, IManager _manager, address _bal) { + oracle = IOracle(_oracle); + dex = IFactory(_dex); + manager = _manager; + bal = _bal; + } + + function harvest(address gauge, address[] memory tokens) external override { + IGauge(gauge).claim_rewards(address(this)); + + (address token, address vault) = VaultHelper.getBestVault(tokens, manager); + // TODO check oracle + uint256 price = oracle.getPrice(bal, token, 1); + address dexPool = dex.pools(bal, token, 10); // TODO hardcoded tick + // TODO add discount + IPool(dexPool).createOrder(IERC20(bal).balanceOf(address(this)), price, vault, block.timestamp + 1 weeks); + + // TODO add premium to the caller + } +} From a2a52ed0c34fdb1c0436f4a000ef14ed6cba8ebb Mon Sep 17 00:00:00 2001 From: OxMarco <> Date: Fri, 23 Jun 2023 11:09:28 +0200 Subject: [PATCH 4/5] add owner --- src/services/debit/utils/BalancerHarvester.sol | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/debit/utils/BalancerHarvester.sol b/src/services/debit/utils/BalancerHarvester.sol index 6289c1da..9062d3b8 100644 --- a/src/services/debit/utils/BalancerHarvester.sol +++ b/src/services/debit/utils/BalancerHarvester.sol @@ -11,6 +11,7 @@ import { VaultHelper } from "../../../libraries/VaultHelper.sol"; import { IBalancerHarvester } from "../../../interfaces/IBalancerHarvester.sol"; contract BalancerHarvester is IBalancerHarvester { + address public immutable owner; IOracle public immutable oracle; IFactory public immutable dex; IManager public immutable manager; @@ -21,9 +22,15 @@ contract BalancerHarvester is IBalancerHarvester { dex = IFactory(_dex); manager = _manager; bal = _bal; + owner = msg.sender; } - function harvest(address gauge, address[] memory tokens) external override { + modifier onlyOwner() { + assert(msg.sender == owner); + _; + } + + function harvest(address gauge, address[] memory tokens) external override onlyOwner { IGauge(gauge).claim_rewards(address(this)); (address token, address vault) = VaultHelper.getBestVault(tokens, manager); From 3e306a9231cbb2f9f9c83918fd49cedb78da3121 Mon Sep 17 00:00:00 2001 From: OxMarco <> Date: Fri, 23 Jun 2023 13:50:35 +0200 Subject: [PATCH 5/5] quoter is broken, rest is fixed --- .prettierignore | 1 + src/interfaces/IBalancerHarvester.sol | 6 - ...erPoolManager.sol => IBalancerManager.sol} | 10 +- src/services/debit/BalancerService.sol | 77 +++------ .../debit/utils/BalancerHarvester.sol | 45 ------ src/services/debit/utils/BalancerManager.sol | 147 ++++++++++++++++++ .../debit/utils/BalancerPoolManager.sol | 73 --------- test/services/BalancerService.test.sol | 1 + 8 files changed, 180 insertions(+), 180 deletions(-) delete mode 100644 src/interfaces/IBalancerHarvester.sol rename src/interfaces/{IBalancerPoolManager.sol => IBalancerManager.sol} (64%) delete mode 100644 src/services/debit/utils/BalancerHarvester.sol create mode 100644 src/services/debit/utils/BalancerManager.sol delete mode 100644 src/services/debit/utils/BalancerPoolManager.sol diff --git a/.prettierignore b/.prettierignore index 00e4910a..612c0343 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,6 +6,7 @@ **/out **/node_modules **/venv +**/artifacts # files *.env diff --git a/src/interfaces/IBalancerHarvester.sol b/src/interfaces/IBalancerHarvester.sol deleted file mode 100644 index ceea3c67..00000000 --- a/src/interfaces/IBalancerHarvester.sol +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.18; - -interface IBalancerHarvester { - function harvest(address gauge, address[] memory tokens) external; -} diff --git a/src/interfaces/IBalancerPoolManager.sol b/src/interfaces/IBalancerManager.sol similarity index 64% rename from src/interfaces/IBalancerPoolManager.sol rename to src/interfaces/IBalancerManager.sol index 6b49b77f..ffdb7870 100644 --- a/src/interfaces/IBalancerPoolManager.sol +++ b/src/interfaces/IBalancerManager.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity =0.8.18; -interface IBalancerPoolManager { +import { IService } from "./IService.sol"; + +interface IBalancerManager { struct PoolData { bytes32 balancerPoolID; address[] tokens; @@ -20,4 +22,10 @@ interface IBalancerPoolManager { function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external; function removePool(address poolAddress) external; + + function harvest(address gauge, address[] memory tokens) external; + + function quote(IService.Agreement memory agreement, PoolData memory pool) external view returns (uint256[] memory); + + function getPool(address) external view returns (PoolData memory); } diff --git a/src/services/debit/BalancerService.sol b/src/services/debit/BalancerService.sol index dc80c8bc..00c5d463 100644 --- a/src/services/debit/BalancerService.sol +++ b/src/services/debit/BalancerService.sol @@ -13,8 +13,7 @@ import { IGauge } from "../../interfaces/external/balancer/IGauge.sol"; import { BalancerHelper } from "../../libraries/BalancerHelper.sol"; import { WeightedMath } from "../../libraries/external/Balancer/WeightedMath.sol"; import { AuctionRateModel } from "../../irmodels/AuctionRateModel.sol"; -import { IBalancerPoolManager, BalancerPoolManager } from "./utils/BalancerPoolManager.sol"; -import { IBalancerHarvester, BalancerHarvester } from "./utils/BalancerHarvester.sol"; +import { IBalancerManager, BalancerManager } from "./utils/BalancerManager.sol"; import { Service } from "../Service.sol"; import { DebitService } from "../DebitService.sol"; import { Whitelisted } from "../Whitelisted.sol"; @@ -31,13 +30,12 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { error SlippageError(); address internal immutable poolManager; - address internal immutable harvester; IBalancerVault internal immutable balancerVault; uint256 public rewardRate; address public immutable bal; address public immutable oracle; address public immutable dex; - mapping(address => IBalancerPoolManager.PoolData) public pools; + mapping(address => IBalancerManager.PoolData) public pools; constructor( address _manager, @@ -52,13 +50,15 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { balancerVault = IBalancerVault(_balancerVault); bal = _bal; - poolManager = address(new BalancerPoolManager(_balancerVault)); - harvester = address(new BalancerHarvester(_oracle, _factory, manager, bal)); + poolManager = address(new BalancerManager(_balancerVault, _oracle, _factory, manager, bal)); } function _open(Agreement memory agreement, bytes memory /*data*/) internal override { - IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; - if (pool.length == 0) revert InexistentPool(); + (bool success, bytes memory data) = poolManager.delegatecall( + abi.encodeWithSelector(IBalancerManager.getPool.selector, agreement.collaterals[0].token) + ); + IBalancerManager.PoolData memory pool = abi.decode(data, (IBalancerManager.PoolData)); + if (success == false || pool.length == 0) revert InexistentPool(); uint256[] memory amountsIn = new uint256[](agreement.loans.length); address[] memory tokens = new address[](agreement.loans.length); @@ -88,7 +88,11 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function _close(uint256 /*tokenID*/, Agreement memory agreement, bytes memory data) internal override { - IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; + (bool success, bytes memory data) = poolManager.delegatecall( + abi.encodeWithSelector(IBalancerManager.getPool.selector, agreement.collaterals[0].token) + ); + assert(success); + IBalancerManager.PoolData memory pool = abi.decode(data, (IBalancerManager.PoolData)); // TODO: add check on fees to be sure amountOut is not too little if (pool.gauge != address(0)) IGauge(pool.gauge).withdraw(agreement.collaterals[0].amount, true); @@ -135,70 +139,33 @@ contract BalancerService is Whitelisted, AuctionRateModel, DebitService { } function quote(Agreement memory agreement) public view override returns (uint256[] memory) { - IBalancerPoolManager.PoolData memory pool = pools[agreement.collaterals[0].token]; + IBalancerManager.PoolData memory pool = pools[agreement.collaterals[0].token]; if (pool.length == 0) revert InexistentPool(); - (, uint256[] memory totalBalances, ) = balancerVault.getPoolTokens(pool.balancerPoolID); - uint256[] memory amountsOut = new uint256[](agreement.loans.length); - uint256[] memory profits; - for (uint256 index = 0; index < agreement.loans.length; index++) { - amountsOut[index] = agreement.loans[index].amount; - } - // Calculate needed BPT to repay loan + fees - uint256 bptAmountOut = BalancerHelper.calculateExpectedBPTToExit( - agreement.collaterals[0].token, - totalBalances, - amountsOut, - pool.scalingFactors, - pool.protocolFeeCollector, - pool.maximumWeightIndex - ); - - // The remaining BPT is swapped to obtain profit - // We need to update the balances since we virtually took tokens out of the pool - // We also need to update the total supply since the bptOut were burned - for (uint256 index = 0; index < agreement.loans.length; index++) { - totalBalances[index] -= amountsOut[index]; - } - if (bptAmountOut < agreement.collaterals[0].amount) { - profits = BalancerHelper.calculateExpectedTokensFromBPT( - agreement.collaterals[0].token, - pool.protocolFeeCollector, - totalBalances, - pool.scalingFactors, - pool.weights, - pool.maximumWeightIndex, - agreement.collaterals[0].amount - bptAmountOut, - IERC20(agreement.collaterals[0].token).totalSupply() - bptAmountOut - ); - // Besides spurious tokens, also amountsOut had been obtained at the first exit - for (uint256 index = 0; index < agreement.loans.length; index++) { - profits[index] += amountsOut[index]; - } - } + uint256[] memory profits = IBalancerManager(poolManager).quote(agreement, pool); return profits; } function harvest(address poolAddress) external { - (bool success, bytes memory data) = harvester.delegatecall( - abi.encodeWithSelector(IBalancerHarvester.harvest.selector, poolAddress) + (bool success, bytes memory data) = poolManager.delegatecall( + abi.encodeWithSelector(IBalancerManager.harvest.selector, poolAddress) ); - require(success, string(data)); + assert(success); } function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external onlyOwner { // We need a delegate call as we are giving tokens approvals (bool success, bytes memory data) = poolManager.delegatecall( - abi.encodeWithSelector(IBalancerPoolManager.addPool.selector, poolAddress, balancerPoolID, gauge) + abi.encodeWithSelector(IBalancerManager.addPool.selector, poolAddress, balancerPoolID, gauge) ); - require(success, string(data)); + assert(success); } function removePool(address poolAddress) external onlyOwner { (bool success, bytes memory data) = poolManager.delegatecall( - abi.encodeWithSelector(IBalancerPoolManager.removePool.selector, poolAddress) + abi.encodeWithSelector(IBalancerManager.removePool.selector, poolAddress) ); - require(success, string(data)); + assert(success); } } diff --git a/src/services/debit/utils/BalancerHarvester.sol b/src/services/debit/utils/BalancerHarvester.sol deleted file mode 100644 index 9062d3b8..00000000 --- a/src/services/debit/utils/BalancerHarvester.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.18; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IManager } from "../../../interfaces/IManager.sol"; -import { IOracle } from "../../../interfaces/IOracle.sol"; -import { IFactory } from "../../../interfaces/external/wizardex/IFactory.sol"; -import { IPool } from "../../../interfaces/external/wizardex/IPool.sol"; -import { IGauge } from "../../../interfaces/external/balancer/IGauge.sol"; -import { VaultHelper } from "../../../libraries/VaultHelper.sol"; -import { IBalancerHarvester } from "../../../interfaces/IBalancerHarvester.sol"; - -contract BalancerHarvester is IBalancerHarvester { - address public immutable owner; - IOracle public immutable oracle; - IFactory public immutable dex; - IManager public immutable manager; - address public immutable bal; - - constructor(address _oracle, address _dex, IManager _manager, address _bal) { - oracle = IOracle(_oracle); - dex = IFactory(_dex); - manager = _manager; - bal = _bal; - owner = msg.sender; - } - - modifier onlyOwner() { - assert(msg.sender == owner); - _; - } - - function harvest(address gauge, address[] memory tokens) external override onlyOwner { - IGauge(gauge).claim_rewards(address(this)); - - (address token, address vault) = VaultHelper.getBestVault(tokens, manager); - // TODO check oracle - uint256 price = oracle.getPrice(bal, token, 1); - address dexPool = dex.pools(bal, token, 10); // TODO hardcoded tick - // TODO add discount - IPool(dexPool).createOrder(IERC20(bal).balanceOf(address(this)), price, vault, block.timestamp + 1 weeks); - - // TODO add premium to the caller - } -} diff --git a/src/services/debit/utils/BalancerManager.sol b/src/services/debit/utils/BalancerManager.sol new file mode 100644 index 00000000..01b56fba --- /dev/null +++ b/src/services/debit/utils/BalancerManager.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity =0.8.18; + +import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; +import { IBalancerPool } from "../../../interfaces/external/balancer/IBalancerPool.sol"; +import { IBalancerVault } from "../../../interfaces/external/balancer/IBalancerVault.sol"; +import { IBalancerManager } from "../../../interfaces/IBalancerManager.sol"; +import { IOracle } from "../../../interfaces/IOracle.sol"; +import { IFactory } from "../../../interfaces/external/wizardex/IFactory.sol"; +import { IPool } from "../../../interfaces/external/wizardex/IPool.sol"; +import { IManager } from "../../../interfaces/IManager.sol"; +import { IGauge } from "../../../interfaces/external/balancer/IGauge.sol"; +import { IService } from "../../../interfaces/IService.sol"; +import { VaultHelper } from "../../../libraries/VaultHelper.sol"; +import { BalancerHelper } from "../../../libraries/BalancerHelper.sol"; +import { console2 } from "forge-std/console2.sol"; + +contract BalancerManager is IBalancerManager { + using SafeERC20 for IERC20; + + IBalancerVault internal immutable balancerVault; + IOracle public immutable oracle; + IFactory public immutable dex; + IManager public immutable manager; + address public immutable bal; + + mapping(address => PoolData) public pools; + + constructor(address _balancerVault, address _oracle, address _dex, IManager _manager, address _bal) { + balancerVault = IBalancerVault(_balancerVault); + oracle = IOracle(_oracle); + dex = IFactory(_dex); + manager = _manager; + bal = _bal; + } + + function getPool(address poolAddress) external view override returns (PoolData memory) { + return pools[poolAddress]; + } + + function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external override { + assert(poolAddress != address(0)); + + (address[] memory poolTokens, , ) = balancerVault.getPoolTokens(balancerPoolID); + uint256 length = poolTokens.length; + assert(length > 0); + + IBalancerPool bpool = IBalancerPool(poolAddress); + bpool.approve(gauge, type(uint256).max); + + uint256 fee = bpool.getSwapFeePercentage(); + uint256[] memory weights = bpool.getNormalizedWeights(); + uint256 maxWeightTokenIndex = 0; + uint256[] memory scalingFactors = new uint256[](length); + + for (uint8 i = 0; i < length; i++) { + if (IERC20(poolTokens[i]).allowance(address(this), address(balancerVault)) == 0) + IERC20(poolTokens[i]).safeApprove(address(balancerVault), type(uint256).max); + if (weights[i] > weights[maxWeightTokenIndex]) maxWeightTokenIndex = i; + scalingFactors[i] = 10 ** (18 - IERC20Metadata(poolTokens[i]).decimals()); + } + + pools[poolAddress] = PoolData( + balancerPoolID, + poolTokens, + weights, + scalingFactors, + maxWeightTokenIndex, + uint8(length), + fee, + gauge, + balancerVault.getProtocolFeesCollector() + ); + + emit PoolWasAdded(poolAddress); + } + + function removePool(address poolAddress) external override { + PoolData memory pool = pools[poolAddress]; + assert(pools[poolAddress].length != 0); + + IERC20(poolAddress).approve(pool.gauge, 0); + delete pools[poolAddress]; + + emit PoolWasRemoved(poolAddress); + } + + function harvest(address gauge, address[] memory tokens) external override { + IGauge(gauge).claim_rewards(address(this)); + + (address token, address vault) = VaultHelper.getBestVault(tokens, manager); + // TODO check oracle + uint256 price = oracle.getPrice(bal, token, 1); + address dexPool = dex.pools(bal, token, 10); // TODO hardcoded tick + // TODO add discount + IPool(dexPool).createOrder(IERC20(bal).balanceOf(address(this)), price, vault, block.timestamp + 1 weeks); + + // TODO add premium to the caller + } + + function quote( + IService.Agreement memory agreement, + PoolData memory pool + ) external view override returns (uint256[] memory) { + (, uint256[] memory totalBalances, ) = balancerVault.getPoolTokens(pool.balancerPoolID); + uint256[] memory amountsOut = new uint256[](agreement.loans.length); + uint256[] memory profits; + for (uint256 index = 0; index < agreement.loans.length; index++) { + amountsOut[index] = agreement.loans[index].amount; + } + // Calculate needed BPT to repay loan + fees + uint256 bptAmountOut = BalancerHelper.calculateExpectedBPTToExit( + agreement.collaterals[0].token, + totalBalances, + amountsOut, + pool.scalingFactors, + pool.protocolFeeCollector, + pool.maximumWeightIndex + ); + + // The remaining BPT is swapped to obtain profit + // We need to update the balances since we virtually took tokens out of the pool + // We also need to update the total supply since the bptOut were burned + for (uint256 index = 0; index < agreement.loans.length; index++) { + totalBalances[index] -= amountsOut[index]; + } + if (bptAmountOut < agreement.collaterals[0].amount) { + profits = BalancerHelper.calculateExpectedTokensFromBPT( + agreement.collaterals[0].token, + pool.protocolFeeCollector, + totalBalances, + pool.scalingFactors, + pool.weights, + pool.maximumWeightIndex, + agreement.collaterals[0].amount - bptAmountOut, + IERC20(agreement.collaterals[0].token).totalSupply() - bptAmountOut + ); + // Besides spurious tokens, also amountsOut had been obtained at the first exit + for (uint256 index = 0; index < agreement.loans.length; index++) { + profits[index] += amountsOut[index]; + } + } + + return profits; + } +} diff --git a/src/services/debit/utils/BalancerPoolManager.sol b/src/services/debit/utils/BalancerPoolManager.sol deleted file mode 100644 index df3019b6..00000000 --- a/src/services/debit/utils/BalancerPoolManager.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: BUSL-1.1 -pragma solidity =0.8.18; - -import { IERC20, SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; -import { IBalancerPool } from "../../../interfaces/external/balancer/IBalancerPool.sol"; -import { IBalancerVault } from "../../../interfaces/external/balancer/IBalancerVault.sol"; -import { IBalancerPoolManager } from "../../../interfaces/IBalancerPoolManager.sol"; - -contract BalancerPoolManager is IBalancerPoolManager { - using SafeERC20 for IERC20; - - address public immutable owner; - IBalancerVault internal immutable balancerVault; - mapping(address => PoolData) public pools; - - constructor(address _balancerVault) { - balancerVault = IBalancerVault(_balancerVault); - owner = msg.sender; - } - - modifier onlyOwner() { - assert(msg.sender == owner); - _; - } - - function addPool(address poolAddress, bytes32 balancerPoolID, address gauge) external override onlyOwner { - assert(poolAddress != address(0)); - - (address[] memory poolTokens, , ) = balancerVault.getPoolTokens(balancerPoolID); - uint256 length = poolTokens.length; - assert(length > 0); - - IBalancerPool bpool = IBalancerPool(poolAddress); - bpool.approve(gauge, type(uint256).max); - - uint256 fee = bpool.getSwapFeePercentage(); - uint256[] memory weights = bpool.getNormalizedWeights(); - uint256 maxWeightTokenIndex = 0; - uint256[] memory scalingFactors = new uint256[](length); - - for (uint8 i = 0; i < length; i++) { - if (IERC20(poolTokens[i]).allowance(address(this), address(balancerVault)) == 0) - IERC20(poolTokens[i]).safeApprove(address(balancerVault), type(uint256).max); - if (weights[i] > weights[maxWeightTokenIndex]) maxWeightTokenIndex = i; - scalingFactors[i] = 10 ** (18 - IERC20Metadata(poolTokens[i]).decimals()); - } - - pools[poolAddress] = PoolData( - balancerPoolID, - poolTokens, - weights, - scalingFactors, - maxWeightTokenIndex, - uint8(length), - fee, - gauge, - balancerVault.getProtocolFeesCollector() - ); - - emit PoolWasAdded(poolAddress); - } - - function removePool(address poolAddress) external override onlyOwner { - PoolData memory pool = pools[poolAddress]; - assert(pools[poolAddress].length != 0); - - IERC20(poolAddress).approve(pool.gauge, 0); - delete pools[poolAddress]; - - emit PoolWasRemoved(poolAddress); - } -} diff --git a/test/services/BalancerService.test.sol b/test/services/BalancerService.test.sol index 87565aa7..4a98003d 100644 --- a/test/services/BalancerService.test.sol +++ b/test/services/BalancerService.test.sol @@ -213,6 +213,7 @@ contract BalancerServiceWeightedTriPool is BaseIntegrationServiceTest { for (uint256 i = 0; i < loanLength; i++) amountsIn[i] = order.agreement.loans[i].amount + order.agreement.loans[i].margin; (, uint256[] memory balances, ) = IBalancerVault(balancerVault).getPoolTokens(balancerPoolID); + uint256 expectedTokens = _calculateExpectedBPTFromJoin(balances, amountsIn); service.open(order); (, IService.Collateral[] memory collaterals, , ) = service.getAgreement(0);