diff --git a/contracts/exchange_handlers/BalancerV2Handler.sol b/contracts/exchange_handlers/BalancerV2Handler.sol new file mode 100644 index 0000000..45e7979 --- /dev/null +++ b/contracts/exchange_handlers/BalancerV2Handler.sol @@ -0,0 +1,171 @@ +pragma solidity 0.5.7; +pragma experimental ABIEncoderV2; + +import "../lib/SafeMath.sol"; +import "../lib/Math.sol"; +import "../lib/Utils.sol"; +import "../lib/AllowanceSetter.sol"; +import "./ExchangeHandler.sol"; +import "../lib/TotleControl.sol"; + +import "../lib/BalancerV2SwapLib.sol"; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + +interface WETH { + function deposit() external payable; + function withdraw(uint256 amount) external; +} + +interface IVault{ + function swap( + BalancerV2SwapLib.SingleSwap calldata singleSwap, + BalancerV2SwapLib.FundManagement calldata funds, + uint256 limit, + uint256 deadline + ) external payable returns (uint256); +} + +interface BV2Pool { + function getPoolId() external returns (bytes32 _poolId); + function getVault() external returns (IVault _vault); +} + + +/// @title BalancerV2Handler +/// @notice Handles the all BalancerHandler trades for the primary contract +contract BalancerV2Handler is ExchangeHandler, AllowanceSetter { + + /* + * State Variables + */ + WETH weth; + /* + * Types + */ + + /// @notice Constructor + constructor(address _weth) public { + weth = WETH(_weth); + } + + struct OrderData { + address pool; + address tokenIn; + address tokenOut; + uint256 maxOrderSpend; + } + + event OrderPerformed(address caller, address pool, address tokenIn, address tokenOut, uint256 amountSpentOnOrder, uint256 amountReceivedFromOrder, uint256 timestamp); + + /* + * Public functions + */ + + /* + * Internal functions + */ + + function getMaxToSpend( + uint256 targetAmount, + uint256 availableToSpend, + uint256 maxOrderSpend + ) internal returns (uint256 max) { + max = Math.min(Math.min(availableToSpend, targetAmount), maxOrderSpend); + return max; + } + + function performOrder( + bytes memory genericPayload, + uint256 availableToSpend, + uint256 targetAmount, + + bool targetAmountIsSource + ) + public + payable + returns (uint256 amountSpentOnOrder, uint256 amountReceivedFromOrder) + { + OrderData memory data = abi.decode(genericPayload, (OrderData)); + IERC20(data.tokenIn).transferFrom(msg.sender, address(this), availableToSpend); + + amountSpentOnOrder = getMaxToSpend( + targetAmount, + availableToSpend, + data.maxOrderSpend + ); + + if (data.tokenIn == address(weth)) { + weth.deposit.value(amountSpentOnOrder)(); + } + + uint256 prevContractAmount; + if (amountSpentOnOrder > 0) { + BV2Pool pool = BV2Pool(data.pool); + IVault vault = pool.getVault(); + + approveAddress(address(vault), data.tokenIn); + approveAddress(msg.sender, data.tokenOut); + + bytes32 poolId = pool.getPoolId(); + + BalancerV2SwapLib.SingleSwap memory singleSwap = BalancerV2SwapLib.SingleSwap( + poolId, + BalancerV2SwapLib.SwapKind.GIVEN_IN, + IAsset(data.tokenIn), + IAsset(data.tokenOut), + amountSpentOnOrder, + "" + ); + + BalancerV2SwapLib.FundManagement memory fundManagement = BalancerV2SwapLib.FundManagement( + address(this), + false, + address(this), + false + ); + + amountReceivedFromOrder = vault.swap( // Getting amountCalculated + singleSwap, + fundManagement, + 0, + block.timestamp + ); + + + } + + if (amountSpentOnOrder < availableToSpend) { + if (data.tokenIn == address(weth)) { + msg.sender.transfer(availableToSpend - amountSpentOnOrder); + } else { + ERC20SafeTransfer.safeTransfer( + data.tokenIn, + msg.sender, + availableToSpend - amountSpentOnOrder + ); + } + } + + if (data.tokenOut == address(weth)) { + weth.withdraw(amountReceivedFromOrder); + msg.sender.transfer(amountReceivedFromOrder); + } else { + ERC20SafeTransfer.safeTransfer( + data.tokenOut, + msg.sender, + amountReceivedFromOrder + ); + } + + emit OrderPerformed(msg.sender, data.pool, data.tokenIn, data.tokenOut, amountSpentOnOrder, amountReceivedFromOrder, block.timestamp); + + } + + /* + * Payable fallback function + */ + + function() external payable {} +} \ No newline at end of file diff --git a/contracts/lib/BalancerV2SwapLib.sol b/contracts/lib/BalancerV2SwapLib.sol new file mode 100644 index 0000000..ac5881d --- /dev/null +++ b/contracts/lib/BalancerV2SwapLib.sol @@ -0,0 +1,70 @@ +pragma solidity 0.5.7; +pragma experimental ABIEncoderV2; + + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + + /** + * @dev This is an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero + * address sentinel value). We're just relying on the fact that `interface` can be used to declare new address-like + * types. + * + * This concept is unrelated to a Pool's Asset Managers. + */ + interface IAsset { + // solhint-disable-previous-line no-empty-blocks + } + +library BalancerV2SwapLib { + + enum SwapKind { GIVEN_IN, GIVEN_OUT } + + + /** + * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on + * the `kind` value. + * + * `assetIn` and `assetOut` are either token addresses, or the IAsset sentinel value for ETH (the zero address). + * Note that Pools never interact with ETH directly: it will be wrapped to or unwrapped from WETH by the Vault. + * + * The `userData` field is ignored by the Vault, but forwarded to the Pool in the `onSwap` hook, and may be + * used to extend swap behavior. + */ + struct SingleSwap { + bytes32 poolId; + SwapKind kind; + IAsset assetIn; + IAsset assetOut; + uint256 amount; + bytes userData; + } + + /** + * @dev All tokens in a swap are either sent from the `sender` account to the Vault, or from the Vault to the + * `recipient` account. + * + * If the caller is not `sender`, it must be an authorized relayer for them. + * + * If `fromInternalBalance` is true, the `sender`'s Internal Balance will be preferred, performing an ERC20 + * transfer for the difference between the requested amount and the User's Internal Balance (if any). The `sender` + * must have allowed the Vault to use their tokens via `IERC20.approve()`. This matches the behavior of + * `joinPool`. + * + * If `toInternalBalance` is true, tokens will be deposited to `recipient`'s internal balance instead of + * transferred. This matches the behavior of `exitPool`. + * + * Note that ETH cannot be deposited to or withdrawn from Internal Balance: attempting to do so will trigger a + * revert. + */ + struct FundManagement { + address sender; + bool fromInternalBalance; + address payable recipient; + bool toInternalBalance; + } + + + +} + + diff --git a/test/balancerV2Test.js b/test/balancerV2Test.js new file mode 100644 index 0000000..c0abc70 --- /dev/null +++ b/test/balancerV2Test.js @@ -0,0 +1,306 @@ +/* + For run tests it needs to be started in different terminals: + + 1) ganache-cli --gasLimit 8000000 -f https://mainnet.infura.io/v3/c5c51292972442b780350d13af3ddeb7 -i 1 -e 10000 -p 8545 --chainId 1 --unlock 0x28C6c06298d514Db089934071355E5743bf21d60 0xf7cd385cb9a442358b892b14301f6310e57cc5c9 0x4f868c1aa37fcf307ab38d215382e88fca6275e2 0x7f4cbc1ff3763a8e2a147e8a93e8bcfcbdd45885 + 2) npx truffle test --network mainnetFork + + With network in truffle-config.js: + mainnetFork: { + host: "127.0.0.1", + port: "8545", + gasPrice: 10, + gas: 8e6, + network_id: 1, + skipDryRun: true, + unlocked_accounts:["0x28C6c06298d514Db089934071355E5743bf21d60", "0xf7cd385cb9a442358b892b14301f6310e57cc5c9", "0xba12222222228d8ba445958a75a0704d566bf2c8", "0x7f4cbc1ff3763a8e2a147e8a93e8bcfcbdd45885"] + } +*/ + + +const Web3 = require('web3'); +const web3 = new Web3(Web3.givenProvider || 'ws://localhost:8545'); +const { + balance, + time} = require('@openzeppelin/test-helpers'); +const BN = web3.utils.BN; +const { default: BigNumber } = require('bignumber.js'); +/* Chai */ +const { expect } = require('chai'); +const truffleAssert = require("truffle-assertions"); + +const timeMachine = require('ganache-time-traveler'); +const BalancerHandler = artifacts.require('BalancerV2Handler'); +const TOKEN = artifacts.require('ERC20'); +// MAINNET ADDRESSES +const balancerHandlerAddr = '0x8345454d4B70275B8806a84AF3bb810DD01DcD82'; + + + +const sleep = s => new Promise(resolve => setTimeout(resolve, 1000 * s)); + +describe('Fork test BalancerHandler', () => { + let balancerHandler; + let fromBlock; + let dai, usdc, graph, enjin, weth; + let holderDai, hoderUsdc, holderGraph, holderEnjin, holderWeth; + + let res; + + before(async() => { + fromBlock = await web3.eth.getBlockNumber(); + [ + governance, strategist, user1, user2, rewards + ] = await web3.eth.getAccounts(); + + dai = await TOKEN.at('0x6B175474E89094C44Da98b954EedeAC495271d0F'); + usdc = await TOKEN.at('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'); + graph = await TOKEN.at('0xc944E90C64B2c07662A292be6244BDf05Cda44a7'); + enjin = await TOKEN.at('0xf629cbd94d3791c9250152bd8dfbdf380e2a3b9c'); + weth = await TOKEN.at('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + balancerHandler = await BalancerHandler.new('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'); + balancerHandler.sendTransaction({ + from:user2, + value:10000000000000000000 + }) + console.log("Contract: ", balancerHandler.address); + console.log("User: ", governance); + + holderDai = '0x28C6c06298d514Db089934071355E5743bf21d60'; + hoderUsdc = '0x28C6c06298d514Db089934071355E5743bf21d60'; + holderGraph = '0xf7cd385cb9a442358b892b14301f6310e57cc5c9'; + holderEnjin = '0x7f4cbc1ff3763a8e2a147e8a93e8bcfcbdd45885' + holderWeth = '0x4f868c1aa37fcf307ab38d215382e88fca6275e2'; + + + }); + + + + it('Perform order should working properly', async() => { + let pool = '0x14462305d211c12a736986f4e8216e28c5ea7ab4'; //Weighted USDC--Graph + let tokenOut = graph.address; + let tokenIn = usdc.address; + + let res; + let amountSpentOnOrder; + let amountReceivedFromOrder; + + //await graph.transfer(balancerHandler.address, await graph.balanceOf(holderGraph), {from: holderGraph}) + + await usdc.transfer(governance, Math.round((await usdc.balanceOf(hoderUsdc))/3), {from: hoderUsdc}) + //await usdc.transfer(balancerHandler.address, await usdc.balanceOf(hoderUsdc), {from: hoderUsdc}) + await usdc.approve(balancerHandler.address, await usdc.balanceOf(governance), {from: governance}); + + let availableToSpend = await usdc.balanceOf(governance); + let targetAmount = 765113153; + let targetAmountIsSource = true; + + console.log("STEP1 CONTRACT graph: ", (await graph.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT usdc: ", (await usdc.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP1 USER graph: ", (await graph.balanceOf(governance)).toString()); + console.log("STEP1 USER usdc: ", (await usdc.balanceOf(governance)).toString()); + console.log("STEP1 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + try { + res = await balancerHandler.performOrder( + web3.eth.abi.encodeParameter( + { + "OrderData":{ + "pool": 'address', + "tokenIn": 'address', + "tokenOut": 'address', + "maxOrderSpend": 'uint256' + } + }, + { + 'pool': pool, + 'tokenIn': tokenIn, + 'tokenOut': tokenOut, + 'maxOrderSpend': '765113153' + } + ), + availableToSpend,targetAmount,targetAmountIsSource , + {from: governance} ); + }catch(e){ + console.log(e, res); + } + + expect(res != null); + + console.log('\n'); + console.log("STEP2 CONTRACT graph: ", (await graph.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT usdc: ", (await usdc.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP2 USER graph: ", (await graph.balanceOf(governance)).toString()); + console.log("STEP2 USER usdc: ", (await usdc.balanceOf(governance)).toString()); + console.log("STEP2 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + + await timeMachine.advanceBlock(); // New Block + + console.log('\n'); + console.log("STEP3 CONTRACT graph: ", (await graph.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT usdc: ", (await usdc.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP3 USER graph: ", (await graph.balanceOf(governance)).toString()); + console.log("STEP3 USER usdc: ", (await usdc.balanceOf(governance)).toString()); + console.log("STEP3 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + + let events = await balancerHandler.getPastEvents("allEvents", {fromBlock: fromBlock, toBlock: "latest"}); + expect(events != null); + console.log('\n'); + console.log(events[events.length-1]); + + }); + + it('Perform order should working properly with WETH as out', async() => { + let pool = '0x1050f901a307e7e71471ca3d12dfcea01d0a0a1c'; + let tokenIn = enjin.address; + let tokenOut = weth.address; + + await enjin.transfer(governance, await enjin.balanceOf(holderEnjin), {from: holderEnjin}) + await enjin.approve(balancerHandler.address, await enjin.balanceOf(governance), {from: governance}); + + let availableToSpend = await enjin.balanceOf(governance); + let targetAmount = 76511315300000; + let targetAmountIsSource = true; + + console.log("\n****\n"); + + console.log("STEP1 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP1 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP1 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP1 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + try { + res = await balancerHandler.performOrder( + web3.eth.abi.encodeParameter( + { + "OrderData":{ + "pool": 'address', + "tokenIn": 'address', + "tokenOut": 'address', + "maxOrderSpend": 'uint256' + } + }, + { + 'pool': pool, + 'tokenIn': tokenIn, + 'tokenOut': tokenOut, + 'maxOrderSpend': '76511315300000' + } + ), + availableToSpend,targetAmount,targetAmountIsSource , + {from: governance} ); + }catch(e){ + console.log(e, res); + } + + expect(res != null); + + console.log("\nSTEP2 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP2 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP2 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP2 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + await timeMachine.advanceBlock(); + + console.log("\nSTEP3 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP3 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP3 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP3 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + let events = await balancerHandler.getPastEvents("allEvents", {fromBlock: fromBlock, toBlock: "latest"}); + expect(events != null); + console.log('\n'); + console.log(events[events.length-1]); + + }); + + it('Perform order should working properly with WETH as in', async() => { + let pool = '0x1050f901a307e7e71471ca3d12dfcea01d0a0a1c'; + let tokenOut = enjin.address; + let tokenIn = weth.address; + + await weth.transfer(governance, await weth.balanceOf(holderWeth), {from: holderWeth}) + await weth.approve(balancerHandler.address, await weth.balanceOf(governance), {from: governance}); + + let availableToSpend = 0.15*10**15; + let targetAmount = 0.15*10**14; + let targetAmountIsSource = true; + + console.log("\n****\n"); + + console.log("STEP1 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP1 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP1 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP1 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP1 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + try { + res = await balancerHandler.performOrder( + web3.eth.abi.encodeParameter( + { + "OrderData":{ + "pool": 'address', + "tokenIn": 'address', + "tokenOut": 'address', + "maxOrderSpend": 'uint256' + } + }, + { + 'pool': pool, + 'tokenIn': tokenIn, + 'tokenOut': tokenOut, + 'maxOrderSpend': 0.15*10**14 + } + ), + availableToSpend,targetAmount,targetAmountIsSource , + {from: governance} ); + }catch(e){ + console.log(e, res); + } + + expect(res != null); + + console.log("\nSTEP2 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP2 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP2 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP2 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP2 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + await timeMachine.advanceBlock(); + + console.log("\nSTEP3 CONTRACT weth: ", (await weth.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT enjin: ", (await enjin.balanceOf(balancerHandler.address)).toString()); + console.log("STEP3 CONTRACT eth: ", (await web3.eth.getBalance(balancerHandler.address)).toString()); + + console.log("STEP3 USER weth: ", (await weth.balanceOf(governance)).toString()); + console.log("STEP3 USER enjin: ", (await enjin.balanceOf(governance)).toString()); + console.log("STEP3 USER eth: ", (await web3.eth.getBalance(governance)).toString()); + + let events = await balancerHandler.getPastEvents("allEvents", {fromBlock: fromBlock, toBlock: "latest"}); + expect(events != null); + console.log('\n'); + console.log(events[events.length-1]); + + }); +});