diff --git a/contracts/interfaces/bancor/IBancor.sol b/contracts/interfaces/bancor/IBancor.sol new file mode 100644 index 0000000..490fc9f --- /dev/null +++ b/contracts/interfaces/bancor/IBancor.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* + Liquidity Protection Store interface +*/ +interface ILiquidityProtectionStore { + + /** + * @dev returns an existing locked network token (BNT) balance details + * + * @param _provider locked balances provider + * @param _index start index + * @return amount of network tokens + * @return lock expiration time + */ + function lockedBalance(address _provider, uint256 _index) external view returns (uint256, uint256); + + /** + * @dev returns an existing protected liquidity details + * + * @param _id protected liquidity id + * + * @return liquidity provider + * @return pool token address + * @return reserve token address + * @return pool token amount (total) + * @return reserve token amount (staked) + * @return rate of 1 protected reserve token in units of the other reserve token (numerator) + * @return rate of 1 protected reserve token in units of the other reserve token (denominator) + * @return timestamp + */ + function protectedLiquidity(uint256 _id) + external + view + returns ( + address, + address, + address, + uint256, + uint256, + uint256, + uint256, + uint256 + ); +} + +/* + Liquidity Protection interface +*/ +interface ILiquidityProtection { + + function addLiquidity( + IERC20 poolAnchor, + IERC20 reserveToken, + uint256 amount + ) external payable returns (uint256); + + function removeLiquidity(uint256 id, uint32 portion) external; + + function removeLiquidityReturn( + uint256 id, + uint32 portion, + uint256 removeTimestamp + ) external view returns ( + uint256, + uint256, + uint256 + ); +} + +/* + Liquidity Protection Stats interface +*/ +interface ILiquidityProtectionStats { + + /** + * @dev returns the total amount of protected pool tokens + * + * @param poolToken pool token address + * @return total amount of protected pool tokens + */ + function totalPoolAmount(IERC20 poolToken) external view returns (uint256); + + /** + * @dev returns the total amount of protected reserve tokens + * + * @param poolToken pool token address + * @param reserveToken reserve token address + * @return total amount of protected reserve tokens + */ + function totalReserveAmount(IERC20 poolToken, IERC20 reserveToken) external view returns (uint256); + +} diff --git a/contracts/strategies/BancorStrategy.sol b/contracts/strategies/BancorStrategy.sol new file mode 100644 index 0000000..2f7b533 --- /dev/null +++ b/contracts/strategies/BancorStrategy.sol @@ -0,0 +1,522 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "contracts/interfaces/bancor/IBancor.sol"; +import "@openzeppelin/contracts/math/SafeMath.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; +import "../Pausable.sol"; +import "../interfaces/vesper/IController.sol"; +import "../interfaces/vesper/IStrategy.sol"; +import "../interfaces/vesper/IVesperPool.sol"; +import "../interfaces/uniswap/IUniswapV2Router02.sol"; + +/** + * ContractRegistry - https://etherscan.io/address/0x52Ae12ABe5D8BD778BD5397F99cA900624CfADD4 + * Use addressOf to find various addresses if these need to be updated + * https://github.com/bancorprotocol/contracts-solidity/blob/master/solidity/contracts/utility/ContractRegistryClient.sol + */ +/// @title This strategy will deposit collateral token in Bancor and earn interest. +abstract contract BancorStrategy is IStrategy, Pausable { + using SafeERC20 for IERC20; + using SafeMath for uint; + + address internal constant BNT = 0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C; + address internal constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address internal constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IController public immutable controller; + address public immutable override pool; + IERC20 public immutable poolToken; // tokens in Vesper pool + IERC20 public immutable collateralToken; // collateral held in Bancor + IERC20 public immutable anchorToken; // anchor token in Bancor (e.g. BNT_ETH) + ILiquidityProtection internal immutable liquidityProtection; + ILiquidityProtectionStore internal immutable liquidityProtectionStore; + uint internal immutable liquidityTimelock; + + uint public pendingFee; + + struct Deposit { + uint id; + uint depositTime; + uint principle; + uint expected; + uint compensation; + } + + struct Withdrawal { + uint principle; + uint portion; + uint expected; + uint actual; + uint compensation; + } + + mapping(uint => Deposit) public deposits; + uint[] public depositIndex; + uint public depositCount = 0; + +// TODO: DELETE ME + uint public count = 0; + + constructor( + address _controller, + address _pool, + address _collateralToken, + address _anchorToken, + address _liquidityProtection, + address _liquidityProtectionStore, + uint _liquidityTimelockDays + ) public { + require(_controller != address(0), "Controller is zero"); + require(IController(_controller).isPool(_pool), "Not a valid pool"); + require(_anchorToken != address(0), "AnchorToken is zero"); + require(_liquidityProtection != address(0), "LiquidityProtection is zero"); + require(_liquidityProtectionStore != address(0), "LiquidityProtectionStore is zero"); + controller = IController(_controller); + pool = _pool; + poolToken = IERC20(IVesperPool(_pool).token()); + collateralToken = IERC20(_collateralToken); + anchorToken = IERC20(_anchorToken); + liquidityProtection = ILiquidityProtection(_liquidityProtection); + liquidityProtectionStore = ILiquidityProtectionStore(_liquidityProtectionStore); + liquidityTimelock = _liquidityTimelockDays * 60 * 60 * 24; + } + + function peek() external view returns (string memory) { + // uint _principle; + // uint _actual; + bool hasWithdrawable; + uint[] memory withdrawables; + (hasWithdrawable, withdrawables) = _getWithdrawableDeposits(); + if (hasWithdrawable) { + return uint2str(withdrawables.length); + } + return "0"; + // (_principle, _actual) = _updateDeposits(); + // return string(abi.encodePacked(uint2str(_principle), " ", uint2str(_actual))); + } + + modifier live() { + require(!paused || _msgSender() == address(controller), "Contract has paused"); + _; + } + + modifier onlyAuthorized() { + require( + _msgSender() == address(controller) || _msgSender() == pool, + "Caller is not authorized" + ); + _; + } + + modifier onlyController() { + require(_msgSender() == address(controller), "Caller is not the controller"); + _; + } + + modifier onlyPool() { + require(_msgSender() == pool, "Caller is not pool"); + _; + } + + function pause() external override onlyController { + _pause(); + } + + function unpause() external override onlyController { + _unpause(); + } + + /** + * @notice Deposit all collateral token from pool into Bancor. + * Anyone can call it except when paused. + */ + function depositAll() external live { + deposit(poolToken.balanceOf(pool)); + count = count + 1; + } + + /// @notice Vesper pools are using this function so it should exist in all strategies. + //solhint-disable-next-line no-empty-blocks + function beforeWithdraw() external override onlyPool {} + + /** + * @dev Withdraw collateral token from Bancor. + * @param _amount Amount of collateral token + */ + function withdraw(uint _amount) external override onlyAuthorized { + require(false, addressToString(msg.sender)); + if (msg.sender == address(controller)) { + // solhint-disable-next-line avoid-low-level-calls + (bool success, ) = pool.delegatecall(abi.encodeWithSignature("withdraw(uint256)", _amount)); + require(success, "delegated withdraw failed"); + } + _withdraw(_amount, false); + } + + /** + * @dev Withdraw all collateral from Bancor and deposit into pool. + * Controller only function, called when migrating strategy. + */ + function withdrawAll() external override onlyController { + _withdrawAll(); + } + + /** + * @dev Calculate interest fee on earning from Bancor and transfer fee to fee collector. + * Deposit available collateral from pool into Bancor. + * Anyone can call it except when paused. + */ + function rebalance() external override live { + _rebalanceEarned(); + uint balance = poolToken.balanceOf(pool); + if (balance != 0) { + _deposit(balance); + } + } + + /** + * @dev Calculate earning from Bancor and also calculate interest fee. + * Deposit fee into Vesper pool to get Vesper pool shares. + * Transfer fee, Vesper pool shares, to fee collector + */ + function _rebalanceEarned() internal { + _updatePendingFee(); + + if (pendingFee != 0) { + _withdraw(pendingFee, true); + pendingFee = 0; + uint256 feeInShare = IERC20(pool).balanceOf(address(this)); + IERC20(pool).safeTransfer(controller.feeCollector(pool), feeInShare); + } + } + + function _updatePendingFee() internal { + bool tHasWithdrawals; + uint[] memory tWithdrawableDeposits; + + (tHasWithdrawals, tWithdrawableDeposits) = _getWithdrawableDeposits(); + if (tHasWithdrawals) { + uint tPrinciple; + uint tActual; + (tPrinciple, tActual) = _updateDeposits(tWithdrawableDeposits); + pendingFee = _calculatePendingFee(tPrinciple, tActual); + } + } + + function _calculatePendingFee(uint _principle, uint _actual) internal view returns (uint) { + uint fee = 0; + // only charge if there is interest to charge on + if (_actual > _principle) { + uint interest = _actual.sub(_principle); + fee = interest.mul(controller.interestFee(pool)).div(1e18); + } + return fee; + } + + /** + * @notice Sweep given token to vesper pool + * @param _fromToken token address to sweep + */ + function sweepErc20(address _fromToken) external { + require((_fromToken != BNT && _fromToken != address(collateralToken) && _fromToken != address(poolToken)), "Not allowed to sweep that"); + // if (_fromToken == ETH) { + // payable(pool).transfer(address(this).balance); + // } else { + uint amount = IERC20(_fromToken).balanceOf(address(this)); + IERC20(_fromToken).safeTransfer(pool, amount); + // } + } + + /// @notice Returns true if strategy can be upgraded. + /// @dev If there are no idsTokens in strategy then it is upgradable + function isUpgradable() external view override returns (bool) { + return depositIndex.length == 0; + } + + function isReservedToken(address _token) external view override returns (bool) { + return (_token != BNT && _token != address(collateralToken) && _token != address(poolToken)); + } + + /// @dev Address of BNT token + function token() external view override returns (address) { + return BNT; + } + + /** + * @notice Total collateral locked in Bancor. + * @dev This value will be used in pool share calculation, so true totalLocked + * will be balance in Bancor minus any pending fee to collect. + * @return Return value will be in poolToken defined decimal. + */ + function totalLocked() external view override returns (uint) { + uint total = 0; + for (uint i = 0; i < depositIndex.length; i++) { + uint lockedBalance; + (,,,,lockedBalance,,,) = liquidityProtectionStore.protectedLiquidity(deposits[depositIndex[i]].id); + total = total.add(lockedBalance); + } + return total; + } + + /** + * @notice Deposit collateral token from pool into Bancor. + * @param _amount Amount of collateral token to deposit + */ + function deposit(uint _amount) public override live { + _deposit(_amount); + } + + //solhint-disable-next-line no-empty-blocks + function _beforeDeposit(uint _amount) internal virtual {} + + function _deposit(uint _amount) internal virtual { + _beforeDeposit(_amount); + uint depositId; + if (address(collateralToken) == ETH) { + depositId = liquidityProtection.addLiquidity{value: _amount}(anchorToken, collateralToken, _amount); + } else { + depositId = liquidityProtection.addLiquidity(anchorToken, collateralToken, _amount); + } + Deposit memory aDeposit = Deposit(depositId, block.timestamp, _amount, _amount, 0); + uint tNewIndex = depositCount; + depositCount = depositCount + 1; + deposits[tNewIndex] = aDeposit; + depositIndex.push(tNewIndex); + _updatePendingFee(); + } + +// TODO: DELETE ME + function addressToString(address _addr) public pure returns(string memory) + { + bytes32 value = bytes32(uint256(_addr)); + bytes memory alphabet = "0123456789abcdef"; + + bytes memory str = new bytes(51); + str[0] = "0"; + str[1] = "x"; + for (uint256 i = 0; i < 20; i++) { + str[2+i*2] = alphabet[uint8(value[i + 12] >> 4)]; + str[3+i*2] = alphabet[uint8(value[i + 12] & 0x0f)]; + } + return string(str); + } + +// TODO: DELETE ME + function uint2str(uint _i) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return "0"; + } + uint j = _i; + uint len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint k = len - 1; + while (_i != 0) { + bstr[k--] = byte(uint8(48 + _i % 10)); + _i /= 10; + } + return string(bstr); + } + + /** + * @dev withdraw from withdrawable deposits + */ + function _withdraw(uint _amount, bool _feeWithdrawal) internal { + bool tHasWithdrawals; + uint[] memory tWithdrawals; + (tHasWithdrawals, tWithdrawals) = _getWithdrawableDeposits(); + // stop processing if nothing is withdrawable + require(tHasWithdrawals, "no deposits are withdrawable yet"); + + uint tPrinciples; + uint tActuals; + (tPrinciples, tActuals) = _updateDeposits(tWithdrawals); + require(_amount <= tPrinciples, "not enough principle to withdraw"); + _withdrawDeposits(tWithdrawals, _amount, tPrinciples, tActuals, _feeWithdrawal); + } + + /** + * @dev withdraw everything regardless of IL protection + */ + function _withdrawAll() internal { + uint tPrinciples; + uint tActuals; + + (tPrinciples, tActuals) = _updateDeposits(depositIndex); + _withdrawDeposits(depositIndex, tPrinciples, tPrinciples, tActuals, false); + } + + function _processFees(uint _actuals, uint _principles) internal virtual { + uint fees = _actuals.sub(_principles).mul(controller.interestFee(pool).div(1e18)); + IERC20(pool).safeTransfer(controller.feeCollector(pool), fees); + } + + function _withdrawDeposits( + uint[] memory _depositIndexes, + uint _amount, + uint _principles, + uint _actuals, + bool _feeWithdrawal + ) internal returns (uint, uint) { + uint remainingAmount = _amount; + uint totalInterest = 0; + uint totalCompensation = 0; + for (uint i = _depositIndexes.length - 1; i > 0; i--) { + Withdrawal memory withdrawal = Withdrawal(0,0,0,0,0); + + Deposit memory iD = deposits[_depositIndexes[i]]; + withdrawal.principle = iD.principle; + withdrawal.portion = remainingAmount.div(withdrawal.principle).mul(1e6); + + // nothing else to withdraw, use less than or eq in case there are small inaccuracies + if (remainingAmount <= 0) { + return (totalInterest, totalCompensation); + } else { + // remove liquidity from Bancor + liquidityProtection.removeLiquidity(iD.id, uint32(withdrawal.portion)); + + if (remainingAmount >= withdrawal.principle) { + // remove our deposit after removing liquidity + _removeDeposit(iD.id); + } else if (remainingAmount < withdrawal.principle) { + // change existing deposit to reflect partial withdrawal + iD.principle = iD.principle.sub(remainingAmount); + + // refetch the amounts to update our deposit + uint pExpected; + uint pCompensation; + (pExpected , , pCompensation) = liquidityProtection.removeLiquidityReturn(iD.id, 1e6, block.timestamp); + iD.expected = pExpected; + iD.compensation = pCompensation; + } + } + + if (withdrawal.compensation == 0) { + // sum the interest if we're in profit + // this takes into account partial withdrawals by dividing by the calculated portion remaining from the withdraw request + totalInterest = totalInterest.add(withdrawal.actual.sub(withdrawal.principle).mul(withdrawal.portion.div(1e6))); + } else { + // or scrape together the compensation + totalCompensation = totalCompensation.add(withdrawal.compensation); + } + remainingAmount = remainingAmount.sub(withdrawal.expected); + } + _convertBNT(); + + // if it's a fee withdrawal don't process fees or send to pool + if (_feeWithdrawal == false) { + _processWithdrawal(_actuals, _principles); + } + return (totalInterest, totalCompensation); + } + + function _processWithdrawal(uint _actuals, uint _principles) internal virtual { + _processFees(_actuals, _principles); + poolToken.safeTransfer(pool, poolToken.balanceOf(address(this))); + } + + /** + * @dev remove the deposit from deposits and depositIndex + **/ + function _removeDeposit(uint _id) internal { + int tIndex = -1; + + // find the deposit by id + for (uint i = 0; i < depositIndex.length; i++) { + Deposit memory iD = deposits[depositIndex[i]]; + if (iD.id == _id) { + tIndex = int(depositIndex[i]); + } + } + + if (tIndex == -1 || uint(tIndex) >= depositIndex.length) return; + delete deposits[uint(tIndex)]; + + for (uint i = uint(tIndex); i < depositIndex.length - 1; i++){ + depositIndex[i] = depositIndex[i+1]; + } + depositIndex.pop(); + } + + function _convertBNT() internal { + uint _balance = IERC20(BNT).balanceOf(address(this)); + if (_balance > 0) { + IUniswapV2Router02 uniswapRouter = IUniswapV2Router02(controller.uniswapRouter()); + address[] memory path = _getPath(BNT, address(poolToken)); + uint amountsOut = uniswapRouter.getAmountsOut(_balance, path)[path.length - 1]; + if (amountsOut != 0) { + IERC20(BNT).safeApprove(address(uniswapRouter), 0); + IERC20(BNT).safeApprove(address(uniswapRouter), _balance); + uniswapRouter.swapExactTokensForTokens(_balance, 1, path, address(this), now + 30); + } + } + } + + function _getPath(address _from, address _to) internal pure returns (address[] memory) { + address[] memory path; + if (_from == WETH || _to == WETH) { + path = new address[](2); + path[0] = _from; + path[1] = _to; + } else { + path = new address[](3); + path[0] = _from; + path[1] = WETH; + path[2] = _to; + } + return path; + } + + function _getWithdrawableDeposits() internal view returns (bool, uint[] memory) { + uint[] memory tWithdrawable = new uint[](depositIndex.length); + uint j = 0; + // get deposit indexes of deposits made >= liquidityTimelockDays ago + for (uint i = 0; i < depositIndex.length; i++) { + Deposit memory iD = deposits[i]; + if (block.timestamp.sub(iD.depositTime) >= liquidityTimelock) { + tWithdrawable[j] = i; + j++; + } + } + // remove empties + if (tWithdrawable.length > 0) { + for (uint i = tWithdrawable.length - 1; i > j; i--) { + delete tWithdrawable[i]; + } + } + return (j > 0, tWithdrawable); + } + + function _updateDeposits(uint[] memory _depositIndexes) internal returns (uint, uint) { + uint tPrinciples = 0; + uint tActuals = 0; + for (uint i; i < _depositIndexes.length; i++) { + Deposit memory iD = deposits[_depositIndexes[i]]; + Withdrawal memory iW; + (iW.expected, iW.actual, iW.compensation) = liquidityProtection.removeLiquidityReturn(iD.id, 1e6, block.timestamp); + + uint tOriginalPrinciple = iD.principle; // TODO: DELETE ME + + iW.principle = iD.principle; + tPrinciples = tPrinciples.add(iW.principle); + tActuals = tActuals.add(iW.actual); + + iD.expected = iW.expected; + iD.compensation = iW.compensation; + deposits[_depositIndexes[i]] = iD; + iD.principle = iW.actual; // set the principle to actual value so that fees aren't charged on the same interest + if (count == 1) { // TODO: DELETE ME + require(iW.actual > tOriginalPrinciple, string(abi.encodePacked(uint2str(iW.actual), " ", uint2str(tOriginalPrinciple)))); + } + } + pendingFee = _calculatePendingFee(tPrinciples, tActuals); + + return (tPrinciples, tActuals); + } +} diff --git a/contracts/strategies/BancorStrategyETH.sol b/contracts/strategies/BancorStrategyETH.sol new file mode 100644 index 0000000..8fe68cb --- /dev/null +++ b/contracts/strategies/BancorStrategyETH.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "./BancorStrategy.sol"; +import "../interfaces/token/IToken.sol"; + +//solhint-disable no-empty-blocks +contract BancorStrategyETH is BancorStrategy { + string public constant NAME = "Strategy-Bancor-ETH"; + string public constant VERSION = "0.1.0"; + + constructor(address _controller, address _pool, uint _liquidityTimelockDays) + public + BancorStrategy( + _controller, + _pool, + 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, //ETH + 0xb1CD6e4153B2a390Cf00A6556b0fC1458C4A5533, //ETH_BNT_ANCHOR, + 0xeead394A017b8428E2D5a976a054F303F78f3c0C, //LIQUIDITY_PROTECTION + 0xf5FAB5DBD2f3bf675dE4cB76517d4767013cfB55, //LIQUIDITY_PROTECTION_STORE + _liquidityTimelockDays + ) + {} + + // function _beforeWithdrawal() internal override { + // TokenLike(WETH).deposit(); + // } + + // function _processWithdrawal(uint _actuals, uint _principles) internal override { + // _processFees(_actuals, _principles); + // collateralToken.safeTransfer(pool, collateralToken.balanceOf(address(this))); + // } + + // function _processFees(uint _actuals, uint _principles) internal override { + // uint fees = _actuals.sub(_principles).mul(controller.interestFee(pool).div(1e18)); + // controller.feeCollector(pool).send(fees); + // } + + receive() external payable {} + + function _beforeDeposit(uint _amount) internal override { + poolToken.safeTransferFrom(pool, address(this), _amount); + TokenLike(WETH).withdraw(_amount); + } + +} diff --git a/contracts/test/IBancorNetworkTest.sol b/contracts/test/IBancorNetworkTest.sol new file mode 100644 index 0000000..cf874dd --- /dev/null +++ b/contracts/test/IBancorNetworkTest.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +interface IBancorNetworkTest { + function convertByPath( + address[] memory _path, + uint256 _amount, + uint256 _minReturn, + address _beneficiary, + address _affiliateAccount, + uint256 _affiliateFee + ) external payable returns (uint256); + + function rateByPath( + address[] memory _path, + uint256 _amount + ) external view returns (uint256); + + function conversionPath( + IERC20 _sourceToken, + IERC20 _targetToken + ) external view returns (address[] memory); +} \ No newline at end of file diff --git a/contracts/test/ILiquidityProtectionStatsTest.sol b/contracts/test/ILiquidityProtectionStatsTest.sol new file mode 100644 index 0000000..5287271 --- /dev/null +++ b/contracts/test/ILiquidityProtectionStatsTest.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* + Liquidity Protection interface +*/ +interface ILiquidityProtectionStatsTest { + + /** + * @dev returns the total amount of protected pool tokens + * + * @param poolToken pool token address + * @return total amount of protected pool tokens + */ + function totalPoolAmount(IERC20 poolToken) external view returns (uint256); + + /** + * @dev returns the total amount of protected reserve tokens + * + * @param poolToken pool token address + * @param reserveToken reserve token address + * @return total amount of protected reserve tokens + */ + function totalReserveAmount(IERC20 poolToken, IERC20 reserveToken) external view returns (uint256); + +} diff --git a/contracts/test/ILiquidityProtectionStoreTest.sol b/contracts/test/ILiquidityProtectionStoreTest.sol new file mode 100644 index 0000000..1edd4f3 --- /dev/null +++ b/contracts/test/ILiquidityProtectionStoreTest.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* + Liquidity Protection interface +*/ +interface ILiquidityProtectionStoreTest { + + function protectedLiquidityIds(address _provider) external view returns (uint256[] memory); + +} diff --git a/contracts/test/ILiquidityProtectionSystemStoreTest.sol b/contracts/test/ILiquidityProtectionSystemStoreTest.sol new file mode 100644 index 0000000..fdbd26c --- /dev/null +++ b/contracts/test/ILiquidityProtectionSystemStoreTest.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* + Liquidity Protection interface +*/ +interface ILiquidityProtectionSystemStoreTest { + + /** + * @dev increases the amount of network tokens minted into a specific pool + * can be executed only by an owner + * + * @param poolAnchor pool anchor + * @param amount amount to increase the minted tokens by + */ + function incNetworkTokensMinted(IERC20 poolAnchor, uint256 amount) external; + + /** + * @dev decreases the amount of network tokens minted into a specific pool + * can be executed only by an owner + * + * @param poolAnchor pool anchor + * @param amount amount to decrease the minted tokens by + */ + function decNetworkTokensMinted(IERC20 poolAnchor, uint256 amount) external; + +} \ No newline at end of file diff --git a/contracts/test/ILiquidityProtectionTest.sol b/contracts/test/ILiquidityProtectionTest.sol new file mode 100644 index 0000000..7b3d17c --- /dev/null +++ b/contracts/test/ILiquidityProtectionTest.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.6.12; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/* + Liquidity Protection interface +*/ +interface ILiquidityProtectionTest { + + function addLiquidity( + IERC20 poolAnchor, + IERC20 reserveToken, + uint256 amount + ) external payable returns (uint256); + + function removeLiquidity(uint256 id, uint32 portion) external; + + function poolAvailableSpace(IERC20 poolAnchor) external returns (uint256, uint256); + +} diff --git a/test/behavior/bancor-strategy.js b/test/behavior/bancor-strategy.js new file mode 100644 index 0000000..dbeb068 --- /dev/null +++ b/test/behavior/bancor-strategy.js @@ -0,0 +1,388 @@ +'use strict' + +const swapper = require('../utils/tokenSwapper') +const bancorHelper = require('../utils/bancorHelper') +const {deposit} = require('../utils/poolOps') +const {expect, assert} = require('chai') +const {BN, time} = require('@openzeppelin/test-helpers') +const DECIMAL = new BN('1000000000000000000') +const ERC20 = artifacts.require('ERC20') +const DAY = 86400; + +const BNT_ADDRESS = '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C' +const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' + +// Bancor strategy behavior test suite +function shouldBehaveLikeStrategy(poolName, collateralName, accounts) { + let pool, strategy, controller, bnt, feeCollector + let collateralToken, providerToken, collateralDecimal + const [owner, user1, user2, user3, user4] = accounts + + function convertTo18Str(amount) { + const multiplier = DECIMAL.div(new BN('10').pow(collateralDecimal)) + return new BN(amount).mul(multiplier).toString() + } + + function convertTo18(amount) { + const multiplier = DECIMAL.div(new BN('10').pow(collateralDecimal)) + return new BN(amount).mul(multiplier) + } + + function convertFrom18Str(amount) { + const divisor = DECIMAL.div(new BN('10').pow(collateralDecimal)) + return new BN(amount).div(divisor).toString() + } + + function convertFrom18(amount) { + const divisor = DECIMAL.div(new BN('10').pow(collateralDecimal)) + return new BN(amount).div(divisor) + } + + async function mineBlocks(numberOfBlocks) { + await time.advanceBlockTo((await time.latestBlock()).add(new BN(numberOfBlocks))) + } + + async function timeIncrease(duration) { + await time.increase(duration) + } + + async function getPoolRatio() { + const poolTokenSpace = await bancorHelper.totalPoolAmount() + const poolReserveSpace = await bancorHelper.totalReserveAmount() + const poolRatio = poolTokenSpace.div(poolReserveSpace) + return poolRatio + } + + describe(`${poolName}:: BancorStrategy basic tests`, function () { + beforeEach(async function () { + pool = this.pool + controller = this.controller + strategy = this.strategy + collateralToken = this.collateralToken + providerToken = this.providerToken + feeCollector = this.feeCollector + // Decimal will be used for amount conversion + collateralDecimal = await this.collateralToken.decimals.call() + + bnt = await ERC20.at(BNT_ADDRESS) + }) + + it('Should sweep erc20 from strategy', async function () { + const metAddress = '0xa3d58c4e56fedcae3a7c43a725aee9a71f0ece4e' + const token = await ERC20.at(metAddress) + const tokenBalance = await swapper.swapEthForToken(1, metAddress, user4, strategy.address) + await strategy.sweepErc20(metAddress) + + const totalSupply = await pool.totalSupply() + const metBalanceInPool = await token.balanceOf(pool.address) + expect(totalSupply).to.be.bignumber.equal('0', `Total supply of ${poolName} is wrong`) + expect(metBalanceInPool).to.be.bignumber.equal(tokenBalance, 'ERC20 token balance is wrong') + }) + + // describe(`${poolName}:: Claim COMP in BancorStrategy`, function () { + // it('Should liquidate COMP when claimed by external source', async function () { + // await deposit(pool, collateralToken, 2, user2) + // await pool.rebalance() + // await mineBlocks(100) + // await providerToken.exchangeRateCurrent() + // await deposit(pool, collateralToken, 2, user3) + // await pool.rebalance() + // await comptroller.claimComp(strategy.address, [providerToken.address], {from: user4}) + // let compBalance = await comp.balanceOf(strategy.address) + // expect(compBalance).to.be.bignumber.gt('0', 'Should earn COMP') + // await swapper.swapEthForToken(10, COMP_ADDRESS, user4, strategy.address) + // await pool.rebalance() + // const tokensHere = await pool.tokensHere() + // compBalance = await comp.balanceOf(strategy.address) + // expect(compBalance).to.be.bignumber.equal('0', 'COMP balance should be zero') + // expect(tokensHere).to.be.bignumber.equal('0', `Tokens here of ${poolName} should be zero`) + // }) + // it('Should claim COMP when rebalance() is called', async function () { + // await deposit(pool, collateralToken, 2, user3) + // await pool.rebalance() + // await mineBlocks(50) + // await providerToken.exchangeRateCurrent() + // await deposit(pool, collateralToken, 2, user4) + // await pool.rebalance() + // const tokensHere = await pool.tokensHere() + // const compBalance = await comp.balanceOf(strategy.address) + // expect(compBalance).to.be.bignumber.equal('0', 'COMP balance should be zero') + // expect(tokensHere).to.be.bignumber.equal('0', `Tokens here of ${poolName} should be zero`) + // }) + // }) + + describe(`${poolName}:: DepositAll in BancorStrategy`, function () { + it(`Should deposit ${collateralName} and call depositAll() in Strategy`, async function () { + const depositAmount = await deposit(pool, collateralToken, 2, user3) + const tokensHere = await pool.tokensHere() + // await providerToken.exchangeRateCurrent() + await strategy.depositAll() + const vPoolBalance = await pool.balanceOf(user3) + expect(convertFrom18(vPoolBalance)).to.be.bignumber.equal( + depositAmount, + `${poolName} balance of user is wrong` + ) + // await providerToken.exchangeRateCurrent() + expect(await pool.tokenLocked()).to.be.bignumber.gte(tokensHere, 'Token locked is not correct') + }) + + it('Should increase pending fee and share price after withdraw', async function () { + // inflate the ETH/BNT market to deflate later, add 0.1% of the pool so that we can still play with ratio + const poolAmount = (await bancorHelper.totalPoolAmount()) + .div(new BN('10000')) + .div(new BN('10').pow(collateralDecimal)); + // const user2DepositId = await bancorHelper.addLiquidity(user2, poolAmount); + // await mineBlocks(10) + + // deposit collateralToken into pool + await deposit(pool, collateralToken, 100, user1) + let fee = await strategy.pendingFee() + expect(fee).to.be.bignumber.equal('0', 'fee should be zero') + + // deposit to bancor + await strategy.depositAll() + fee = await strategy.pendingFee() + let peek = await strategy.peek.call(); + expect(peek).to.equal('0') + const sharePrice1 = await pool.getPricePerShare() + expect(fee).to.be.bignumber.equal('0', 'fee should be zero') + await timeIncrease(2 * DAY) + peek = await strategy.peek.call(); + expect(peek).to.equal('1') + + // trade ETH with Bancor and Time travel to trigger some earning + // await bancorHelper.removeLiquidity(user2, user2DepositId) + + // TODO: UNABLE TO GENERATE FEES / INTEREST ON BANCOR?! + const uniBntBalance = await swapper.swapEthForToken(poolAmount, BNT_ADDRESS, user2, user2) + await bancorHelper.swapBNTForETH(uniBntBalance, user2) + await mineBlocks(1) + await bancorHelper.swapETHForBNT(poolAmount, user2) + await mineBlocks(1) + await bancorHelper.swapBNTForETH(uniBntBalance, user2) + await mineBlocks(1) + await bancorHelper.swapETHForBNT(poolAmount, user2) + await mineBlocks(1) + await bancorHelper.swapBNTForETH(uniBntBalance, user2) + await mineBlocks(1) + await bancorHelper.swapETHForBNT(poolAmount, user2) + await mineBlocks(1) + await bancorHelper.swapBNTForETH(uniBntBalance, user2) + await mineBlocks(1) + await bancorHelper.swapETHForBNT(poolAmount, user2) + await mineBlocks(1) + await bancorHelper.swapBNTForETH(uniBntBalance, user2) + // await bancorHelper.addLiquidityBnt(user2, uniBntBalance); + await mineBlocks(10) + await timeIncrease(2 * DAY) + + // deposit again + await deposit(pool, collateralToken, 100, user1) + peek = await strategy.peek.call(); + expect(peek).to.equal('1') + await strategy.depositAll() + + await timeIncrease(2 * DAY) + peek = await strategy.peek.call(); + expect(peek).to.equal('2') + + fee = await strategy.pendingFee() + expect(fee).to.be.bignumber.gt('0', `fee should be > 0, was ${fee}`) + + let sharePrice2 = await pool.getPricePerShare() + expect(sharePrice2).to.be.bignumber.gt(sharePrice1, 'share price should increase') + + // Time travel to trigger some earning + await mineBlocks(200) + const vPoolBalance = await pool.balanceOf(user1) + await pool.withdraw(vPoolBalance, {from: user1}) + + const updatedFee = await strategy.pendingFee() + expect(updatedFee).to.be.bignumber.gt(fee, 'updated fee should be greater than previous fee') + // When all tokens are burnt, price will be back to 1.0 + sharePrice2 = await pool.getPricePerShare() + expect(sharePrice2).to.be.bignumber.equal(convertFrom18Str(DECIMAL), 'share price should 1.0') + + // We still have some pending fee to be converted into collateral, which will increase totalValue + await pool.rebalance() + expect(await pool.totalValue()).to.be.bignumber.gt('0', `Total value of ${poolName} should be > 0`) + }) + }) + + describe(`${poolName}:: Interest fee via BancorStrategy`, function () { + it('Should handle interest fee correctly after withdraw', async function () { + await deposit(pool, collateralToken, 200, user2) + await pool.rebalance() + + const pricePerShare = await pool.getPricePerShare() + const vPoolBalanceBefore = await pool.balanceOf(feeCollector) + + // Mine some blocks + await mineBlocks(30) +// await providerToken.exchangeRateCurrent() + const pricePerShare2 = await pool.getPricePerShare() + expect(pricePerShare2).to.be.bignumber.gt(pricePerShare, 'PricePerShare should be higher after time travel') + + await pool.withdraw(await pool.balanceOf(user2), {from: user2}) +// await providerToken.exchangeRateCurrent() + + let tokenLocked = await pool.tokenLocked() + // Ideally this should be zero but we have 1 block earning of cToken + const dust = DECIMAL.div(new BN('100')) // Dust is less than 1e16 + expect(tokenLocked).to.be.bignumber.lte(dust, 'Token locked should be zero') + let totalSupply = await pool.totalSupply() + expect(totalSupply).to.be.bignumber.equal('0', 'Total supply should be zero') + + await pool.rebalance() +// await providerToken.exchangeRateCurrent() + + tokenLocked = await pool.tokenLocked() + expect(tokenLocked).to.be.bignumber.gt('0', 'Token locked should be greater than zero') + totalSupply = await pool.totalSupply() + expect(totalSupply).to.be.bignumber.gt('0', 'Total supply should be greater than zero') + + const vPoolBalanceAfter = await pool.balanceOf(feeCollector) + expect(vPoolBalanceAfter).to.be.bignumber.gt(vPoolBalanceBefore, 'Fee collected is not correct') + const tokensHere = await pool.tokensHere() + expect(tokensHere).to.be.bignumber.equal('0', 'Tokens here is not correct') + }) + }) + + describe(`${poolName}:: Updates via Controller`, function () { + it('Should call withdraw() in strategy', async function () { + await deposit(pool, collateralToken, 2, user4) + await pool.rebalance() + timeIncrease(2 * DAY) + + const vPoolBalanceBefore = await pool.balanceOf(user4) + + const totalSupply = await pool.totalSupply() + const price = await pool.getPricePerShare() + const withdrawAmount = totalSupply.mul(price).div(DECIMAL).toString() + + const target = strategy.address + const methodSignature = 'withdraw(uint256)' + const data = web3.eth.abi.encodeParameter('uint256', withdrawAmount) + await controller.executeTransaction(target, 0, methodSignature, data, {from: accounts[0]}) + + const vPoolBalance = await pool.balanceOf(user4) + const collateralBalance = await collateralToken.balanceOf(pool.address) + + expect(collateralBalance).to.be.bignumber.eq(withdrawAmount, `${collateralName} balance of pool is wrong`) + expect(vPoolBalance).to.be.bignumber.eq(vPoolBalanceBefore, `${poolName} balance of user is wrong`) + }) + + it('Should call withdrawAll() in strategy', async function () { + await deposit(pool, collateralToken, 2, user3) + await pool.rebalance() + + const totalLocked = await strategy.totalLocked() + + const target = strategy.address + const methodSignature = 'withdrawAll()' + const data = '0x' + await controller.executeTransaction(target, 0, methodSignature, data, {from: owner}) + + const tokensInPool = await pool.tokensHere() + expect(tokensInPool).to.be.bignumber.gte(totalLocked, 'TokensHere in pool is not correct') + }) + + it('Should rebalance after withdrawAll() and adding new strtegy', async function () { + await deposit(pool, collateralToken, 200, user3) + await pool.rebalance() + await mineBlocks(25) +// await providerToken.exchangeRateCurrent() + const totalLockedBefore = await strategy.totalLocked() + + const target = strategy.address + const methodSignature = 'withdrawAll()' + const data = '0x' + await controller.executeTransaction(target, 0, methodSignature, data, {from: owner}) + + strategy = await this.newStrategy.new(controller.address, pool.address, 100) + await controller.updateStrategy(pool.address, strategy.address) + await pool.rebalance() + + const totalLockedAfter = await strategy.totalLocked() + expect(totalLockedAfter).to.be.bignumber.gte(totalLockedBefore, 'Total locked with new strategy is wrong') + + const withdrawAmount = await pool.balanceOf(user3) + await pool.withdraw(withdrawAmount, {from: user3}) + + const collateralBalance = convertTo18(await collateralToken.balanceOf(user3)) + expect(collateralBalance).to.be.bignumber.gt(withdrawAmount, `${collateralName} balance of user is wrong`) + }) + }) + + describe(`${poolName}:: Strategy migration without withdrawAll()`, function () { + it('Should migrate strategy', async function () { + await controller.updateInterestFee(pool.address, '0') + await deposit(pool, collateralToken, 20, user1) + await pool.rebalance() + + let pricePerShare = await pool.getPricePerShare() + let vPoolBalance = await pool.balanceOf(user1) +// await providerToken.exchangeRateCurrent() + await pool.withdraw(vPoolBalance.div(new BN(2)), {from: user1}) + + // Migrate out + let target = strategy.address + let methodSignature = 'migrateOut()' + let data = '0x' + await controller.executeTransaction(target, 0, methodSignature, data) + +// await providerToken.exchangeRateCurrent() + let pricePerShare2 = await pool.getPricePerShare() + expect(pricePerShare2).to.be.bignumber.gt(pricePerShare, 'Share price should increase') + + strategy = await this.newStrategy.new(controller.address, pool.address) + await controller.updateStrategy(pool.address, strategy.address) + await mineBlocks(25) +// await providerToken.exchangeRateCurrent() + + // Deposit and rebalance with new strategy but before migrateIn + await deposit(pool, collateralToken, 20, user2) + await pool.rebalance() + + pricePerShare = pricePerShare2 +// await providerToken.exchangeRateCurrent() + pricePerShare2 = await pool.getPricePerShare() + expect(pricePerShare2).to.be.bignumber.gt(pricePerShare, 'Share price should increase') + + // Migrate in + target = strategy.address + methodSignature = 'migrateIn()' + data = '0x' + await controller.executeTransaction(target, 0, methodSignature, data) + await mineBlocks(25) +// await providerToken.exchangeRateCurrent() + + // Deposit and rebalance after migrateIn + const depositAmount = await deposit(pool, collateralToken, 20, user2) + await pool.rebalance() + + pricePerShare = pricePerShare2 + pricePerShare2 = await pool.getPricePerShare() + expect(pricePerShare2).to.be.bignumber.gt(pricePerShare, 'Share price should increase') + + vPoolBalance = await pool.balanceOf(user1) + await pool.withdraw(vPoolBalance, {from: user1}) + vPoolBalance = await pool.balanceOf(user2) + await pool.withdraw(vPoolBalance, {from: user2}) + + const cTokenBalance = await providerToken.balanceOf(strategy.address) + vPoolBalance = await pool.balanceOf(user1) + const collateralBalance = await collateralToken.balanceOf(user1) + + expect(cTokenBalance).to.be.bignumber.gt('0', 'cToken balance should be > 0') + expect(vPoolBalance).to.be.bignumber.eq('0', `${poolName} balance of user should be zero`) + expect(collateralBalance).to.be.bignumber.gt( + depositAmount, + `${collateralName} balance should be > deposit amount` + ) + }) + }) + }) +} + +module.exports = {shouldBehaveLikeStrategy} diff --git a/test/utils/bancorHelper.js b/test/utils/bancorHelper.js new file mode 100644 index 0000000..ad27b8b --- /dev/null +++ b/test/utils/bancorHelper.js @@ -0,0 +1,143 @@ +'use strict' + +const { assert, expect } = require('chai') +const {BN, time} = require('@openzeppelin/test-helpers') +const IBancorNetworkTest = artifacts.require('IBancorNetworkTest') +const ILiquidityProtectionTest = artifacts.require('ILiquidityProtectionTest') +const ILiquidityProtectionStatsTest = artifacts.require('ILiquidityProtectionStatsTest') +const ILiquidityProtectionStoreTest = artifacts.require('ILiquidityProtectionStoreTest') +const ILiquidityProtectionSystemStoreTest = artifacts.require('ILiquidityProtectionSystemStoreTest') +const ERC20 = artifacts.require('ERC20') +const DECIMAL = new BN('1000000000000000000') +const bancorNetworkAddress = '0x2F9EC37d6CcFFf1caB21733BdaDEdE11c823cCB0' +const liquidityProtectionAddress = '0xeead394A017b8428E2D5a976a054F303F78f3c0C' +const liquidityProtectionStatsAddress = '0x9712bb50dc6efb8a3d7d12cea500a50967d2d471' +const liquidityProtectionStoreAddress = '0xf5fab5dbd2f3bf675de4cb76517d4767013cfb55' +const liquidityProtectionSystemStoreAddress = '0xc4c5634de585d43daec8fa2a6fb6286cd9b87131' + +const ETH = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' +const ETHBNT = "0xb1cd6e4153b2a390cf00a6556b0fc1458c4a5533" +const BNT = "0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c" +const ZERO = '0x0000000000000000000000000000000000000000' + +async function mineBlocks(numberOfBlocks) { + await time.advanceBlockTo((await time.latestBlock()).add(new BN(numberOfBlocks))) +} + +/** + * @param {string} amount token amount + * @param {string} beneficiary Address of token receiver + * @returns {string} Output amount of token swap + */ +async function swapBNTForETH(amount, beneficiary) { + assert(amount > 0, "need something to swap!") + const bancorNetwork = await IBancorNetworkTest.at(bancorNetworkAddress) + const block = await web3.eth.getBlock('latest') + const priorETH = await web3.eth.getBalance(beneficiary) + const token = await ERC20.at(BNT) + const priorBnt = await token.balanceOf(beneficiary) + + await token.approve(bancorNetworkAddress, 0, {from: beneficiary}) + await token.approve(bancorNetworkAddress, amount, {from: beneficiary}) + + const response = await bancorNetwork.convertByPath([BNT, ETHBNT, ETH], amount, 1, beneficiary, ZERO, 0, {from: beneficiary}) + mineBlocks(10) + const postETH = await web3.eth.getBalance(beneficiary) + const postBnt = await token.balanceOf(beneficiary) + assert(priorBnt > postBnt, 'BNT did not get sent') + assert(new BN(postETH).gt(new BN(priorETH)), 'ETH balance is not correct') + return postETH +} + +/** + * @param {string} amount token amount in ETH + * @param {string} beneficiary Address of token receiver + * @returns {string} Output amount of token swap + */ +async function swapETHForBNT(amount, beneficiary) { + assert(amount > 0, "need something to swap!") + const bigAmount = new BN(amount).mul(DECIMAL).toString() + const bancorNetwork = await IBancorNetworkTest.at(bancorNetworkAddress) + const block = await web3.eth.getBlock('latest') + const token = await ERC20.at(BNT) + const prior = await token.balanceOf(beneficiary) + + await bancorNetwork.convertByPath([ETH, ETHBNT, BNT], bigAmount, 1, beneficiary, ZERO, 0, + { value: bigAmount, from: beneficiary }) + const result = (await token.balanceOf(beneficiary)).sub(prior) + assert(result.gt(new BN('0')), 'Result balance is not correct') + return result +} + +async function poolAvailableSpace() { + const liquidityProtection = await ILiquidityProtectionTest.at(liquidityProtectionAddress) + return await liquidityProtection.poolAvailableSpace.call(ETHBNT) +} + +async function addLiquidity(provider, amount) { + const bigAmount = new BN(amount).mul(DECIMAL).toString() + const liquidityProtection = await ILiquidityProtectionTest.at(liquidityProtectionAddress) + await liquidityProtection.addLiquidity(ETHBNT, ETH, bigAmount, {from: provider, value: bigAmount}) + await mineBlocks(1) + + const liquidityProtectionStore = await ILiquidityProtectionStoreTest.at(liquidityProtectionStoreAddress) + const ids = await liquidityProtectionStore.protectedLiquidityIds.call(provider) + return ids[ids.length - 1].toString() +} + +async function addLiquidityBnt(provider, amount) { + const liquidityProtection = await ILiquidityProtectionTest.at(liquidityProtectionAddress) + const token = await ERC20.at(BNT) + await token.approve(liquidityProtectionAddress, 0, {from: provider}) + await token.approve(liquidityProtectionAddress, amount, {from: provider}) + await liquidityProtection.addLiquidity(ETHBNT, BNT, amount, {from: provider}) + await mineBlocks(1) + + const liquidityProtectionStore = await ILiquidityProtectionStoreTest.at(liquidityProtectionStoreAddress) + const ids = await liquidityProtectionStore.protectedLiquidityIds.call(provider) + return ids[ids.length - 1].toString() +} + +async function removeLiquidity(provider, depositId) { + const liquidityProtection = await ILiquidityProtectionTest.at(liquidityProtectionAddress) + await liquidityProtection.removeLiquidity(depositId, '1000000', {from: provider}) +} + +async function totalPoolAmount() { + const liquidityProtectionStats = await ILiquidityProtectionStatsTest.at(liquidityProtectionStatsAddress) + return await liquidityProtectionStats.totalPoolAmount.call(ETHBNT); +} + +async function totalReserveAmount() { + const liquidityProtectionStats = await ILiquidityProtectionStatsTest.at(liquidityProtectionStatsAddress) + return await liquidityProtectionStats.totalReserveAmount.call(ETHBNT, BNT); +} + +async function protectedLiquidityIds(provider) { + const liquidityProtectionStore = await ILiquidityProtectionStoreTest.at(liquidityProtectionStoreAddress) + return await liquidityProtectionStore.protectedLiquidityIds.call(provider); +} + +async function incNetworkTokensMinted(owner, amount) { + const liquidityProtectionSystemStore = await ILiquidityProtectionSystemStoreTest.at(liquidityProtectionSystemStoreAddress) + await liquidityProtectionSystemStore.incNetworkTokensMinted(ETHBNT, amount, {from: owner}); +} + +async function decNetworkTokensMinted(owner, amount) { + const liquidityProtectionSystemStore = await ILiquidityProtectionSystemStoreTest.at(liquidityProtectionSystemStoreAddress) + await liquidityProtectionSystemStore.decNetworkTokensMinted(ETHBNT, amount, {from: owner}); +} + +module.exports = { + swapBNTForETH, + swapETHForBNT, + poolAvailableSpace, + totalPoolAmount, + totalReserveAmount, + addLiquidity, + addLiquidityBnt, + removeLiquidity, + protectedLiquidityIds, + incNetworkTokensMinted, + decNetworkTokensMinted +} \ No newline at end of file diff --git a/test/utils/setupHelper.js b/test/utils/setupHelper.js index f633cbd..4a5b773 100644 --- a/test/utils/setupHelper.js +++ b/test/utils/setupHelper.js @@ -106,6 +106,16 @@ async function createVesperMakerStrategy(obj, collateralManager, strategy, vPool async function createStrategy(obj, strategy) { obj.strategy = await strategy.new(obj.controller.address, obj.pool.address) } +/** + * Create Bancor strategy instance and set it in test class object + * + * @param {*} obj Test class object + * @param {*} strategy Strategy artifact + * @param {*} liquidityLockPeriod days withdrawals are locked + */ +async function createBancorStrategy(obj, strategy, liquidityLockPeriod) { + obj.strategy = await strategy.new(obj.controller.address, obj.pool.address, liquidityLockPeriod) +} /** * @typedef {object} PoolData @@ -133,6 +143,7 @@ async function setupVPool(obj, poolData) { underlayStrategy, vPool, contracts, + liquidityLockPeriod, } = poolData const interestFee = '50000000000000000' // 5% obj.feeCollector = feeCollector @@ -143,6 +154,8 @@ async function setupVPool(obj, poolData) { await obj.controller.addPool(obj.pool.address) if (strategyType === 'maker' || strategyType === 'compoundMaker') { await createMakerStrategy(obj, collateralManager, strategy) + } else if (strategyType === 'bancor') { + await createBancorStrategy(obj, strategy, liquidityLockPeriod) } else if (strategyType === 'vesperMaker') { await createVesperMakerStrategy(obj, collateralManager, strategy, vPool) } else { diff --git a/test/veth-bancor.js b/test/veth-bancor.js new file mode 100644 index 0000000..b0d4ae8 --- /dev/null +++ b/test/veth-bancor.js @@ -0,0 +1,27 @@ +'use strict' + +const {shouldBehaveLikePool} = require('./behavior/vesper-pool') +const {shouldBehaveLikeStrategy} = require('./behavior/bancor-strategy') +const {setupVPool} = require('./utils/setupHelper') + +const VETH = artifacts.require('VETH') +const BancorStrategy = artifacts.require('BancorStrategyETH') +const Controller = artifacts.require('Controller') + +contract('vETH Pool with Bancor strategy', function (accounts) { + beforeEach(async function () { + await setupVPool(this, { + controller: Controller, + pool: VETH, + strategy: BancorStrategy, + feeCollector: accounts[9], + strategyType: 'bancor', + liquidityLockPeriod: 1, + }) + + this.newStrategy = BancorStrategy + }) + + shouldBehaveLikePool('vETH', 'WETH', 'vETH', accounts) + shouldBehaveLikeStrategy('vETH', 'WETH', accounts) +})