From 067d133b077482299c2ea0e5d486ca4928878d4a Mon Sep 17 00:00:00 2001 From: Yaroslav Tumanov Date: Wed, 9 Jun 2021 18:38:48 +0300 Subject: [PATCH] add recover and tests --- contracts/fake/MarketsRegistryFake.sol | 34 ++++ contracts/fake/SafeBEP20.sol | 41 +++++ contracts/interfaces/IMarketsRegistry.sol | 8 + contracts/vaults/HodlerVaultSpace.sol | 4 +- contracts/vaults/MarketsHodlerVault.sol | 17 ++ test/markets-hodler-vault.js | 194 ++++++++++++++++++++++ 6 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 contracts/fake/MarketsRegistryFake.sol create mode 100644 contracts/fake/SafeBEP20.sol create mode 100644 contracts/interfaces/IMarketsRegistry.sol diff --git a/contracts/fake/MarketsRegistryFake.sol b/contracts/fake/MarketsRegistryFake.sol new file mode 100644 index 0000000..c984bcb --- /dev/null +++ b/contracts/fake/MarketsRegistryFake.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.4; +import "../interfaces/IMarketsRegistry.sol"; +import "../interfaces/IBEP20.sol"; +import "./SafeBEP20.sol"; + +contract MarketsRegistryFake is IMarketsRegistry { + using SafeBEP20 for IBEP20; + address public secondary; + + /** + * Allow owner to move tokens from the registry + */ + function recoverTokens(IBEP20 token, address destination) + override + public + { + + // Get the balance + uint256 balance = token.balanceOf(address(this)); + + if (secondary == address(0)) { + token.safeTransfer(destination, balance); + } else { + token.safeTransfer(destination, balance * 80 / 100); + token.safeTransfer(secondary, balance * 20 / 100); + } + + } + + function enableSecondaryReceiver(address _secondary) public { + secondary = _secondary; + } +} \ No newline at end of file diff --git a/contracts/fake/SafeBEP20.sol b/contracts/fake/SafeBEP20.sol new file mode 100644 index 0000000..5d76713 --- /dev/null +++ b/contracts/fake/SafeBEP20.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.7.4; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "../interfaces/IBEP20.sol"; + +/** + * @title SafeBEP20 + * @dev Wrappers around BEP20 operations that throw on failure (when the token + * contract returns false). Tokens that return no value (and instead revert or + * throw on failure) are also supported, non-reverting calls are assumed to be + * successful. + * To use this library you can add a `using SafeBEP20 for IBEP20;` statement to your contract, + * which allows you to call the safe operations as `token.safeTransfer(...)`, etc. + */ +library SafeBEP20 { + using Address for address; + + function safeTransfer(IBEP20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); + } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + */ + function _callOptionalReturn(IBEP20 token, bytes memory data) private { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that + // the target address contains contract code and also asserts for success in the low-level call. + + bytes memory returndata = address(token).functionCall(data, "SafeBEP20: low-level call failed"); + if (returndata.length > 0) { // Return data is optional + // solhint-disable-next-line max-line-length + require(abi.decode(returndata, (bool)), "SafeBEP20: BEP20 operation did not succeed"); + } + } +} diff --git a/contracts/interfaces/IMarketsRegistry.sol b/contracts/interfaces/IMarketsRegistry.sol new file mode 100644 index 0000000..4a76968 --- /dev/null +++ b/contracts/interfaces/IMarketsRegistry.sol @@ -0,0 +1,8 @@ +pragma solidity 0.7.4; + + +import "./IBEP20.sol"; + +interface IMarketsRegistry { + function recoverTokens(IBEP20 token, address destination) external; +} diff --git a/contracts/vaults/HodlerVaultSpace.sol b/contracts/vaults/HodlerVaultSpace.sol index 8701b0b..5dfa510 100644 --- a/contracts/vaults/HodlerVaultSpace.sol +++ b/contracts/vaults/HodlerVaultSpace.sol @@ -78,7 +78,7 @@ contract HodlerVaultSpace is Ownable { (uint reserve1, uint reserve2,) = config.tokenPair.getReserves(); - if (address(config.infinityToken) < address(config.weth)) { + if (address(config.infinityToken) < config.weth) { infinityMaxAllowed = config.uniswapRouter.quote( totalETH, reserve2, @@ -174,7 +174,7 @@ contract HodlerVaultSpace is Ownable { uint ethRequired; - if (address(config.infinityToken) > address(config.weth)) { + if (address(config.infinityToken) > config.weth) { ethRequired = config.uniswapRouter.quote( netInfinity, reserve2, diff --git a/contracts/vaults/MarketsHodlerVault.sol b/contracts/vaults/MarketsHodlerVault.sol index 569a99c..014d4bb 100644 --- a/contracts/vaults/MarketsHodlerVault.sol +++ b/contracts/vaults/MarketsHodlerVault.sol @@ -6,6 +6,7 @@ import "@uniswap/v2-periphery/contracts/interfaces/IWETH.sol"; import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; import '@openzeppelin/contracts/math/SafeMath.sol'; import "../interfaces/IBEP20.sol"; +import "../interfaces/IMarketsRegistry.sol"; contract MarketsHodlerVault is Ownable { using SafeMath for uint; @@ -42,6 +43,7 @@ contract MarketsHodlerVault is Ownable { struct HodlerVaultConfig { IBEP20 infinityToken; + IMarketsRegistry registry; IUniswapV2Router02 uniswapRouter; IUniswapV2Pair tokenPair; address weth; @@ -66,6 +68,8 @@ contract MarketsHodlerVault is Ownable { mapping(address => LPbatch[]) public lockedLP; mapping(address => uint) public queueCounter; + uint public constant MINIMUM_REGISTRY_RECOVER = 1e14; //0,0001 ETH minimum + receive() external payable {} function maxTokensToInvest() external view returns (uint) { @@ -121,12 +125,14 @@ contract MarketsHodlerVault is Ownable { function seed( uint32 duration, IBEP20 infinityToken, + IMarketsRegistry registry, address uniswapPair, address uniswapRouter, address payable feeReceiver, uint8 purchaseFee // INFINITY ) external onlyOwner { config.infinityToken = infinityToken; + config.registry = registry; config.uniswapRouter = IUniswapV2Router02(uniswapRouter); config.tokenPair = IUniswapV2Pair(uniswapPair); config.weth = config.uniswapRouter.WETH(); @@ -162,11 +168,22 @@ contract MarketsHodlerVault is Ownable { config.feeReceiver = feeReceiver; } + function registryRecover() public { + uint wethRegistryBalance = IBEP20(config.weth).balanceOf(address(config.registry)); + + if (wethRegistryBalance >= MINIMUM_REGISTRY_RECOVER) { + config.registry.recoverTokens(IBEP20(config.weth), address(this)); + IWETH(config.weth).withdraw(IBEP20(config.weth).balanceOf(address(this))); + } + } + function purchaseLP(uint amount) external lock { require(amount > 0, "MarketsHodlerVault: INFINITY required to mint LP"); require(config.infinityToken.balanceOf(msg.sender) >= amount, "MarketsHodlerVault: Not enough INFINITY tokens"); require(config.infinityToken.allowance(msg.sender, address(this)) >= amount, "MarketsHodlerVault: Not enough INFINITY tokens allowance"); + registryRecover(); + uint infinityFee = amount.mul(config.purchaseFee).div(100); uint netInfinity = amount.sub(infinityFee); diff --git a/test/markets-hodler-vault.js b/test/markets-hodler-vault.js index a1c2264..e8a7bca 100644 --- a/test/markets-hodler-vault.js +++ b/test/markets-hodler-vault.js @@ -22,6 +22,7 @@ describe('MarketsHodlerVault', function () { let owner; let user; let acceleratorVaultFake; + let registryFake; let weth; let infinity; @@ -58,9 +59,14 @@ describe('MarketsHodlerVault', function () { pairAddress = await uniswapFactory.getPair(weth.address, infinity.address); uniswapPair = await ethers.getContractAt(UniswapV2Pair.abi, pairAddress); + const MarketsRegistryFake = await ethers.getContractFactory('MarketsRegistryFake'); + registryFake = await MarketsRegistryFake.deploy(); + await registryFake.deployed(); + await hodlerVault.seed( stakeDuration, infinity.address, + registryFake.address, pairAddress, uniswapRouter.address, acceleratorVaultFake.address, @@ -85,6 +91,7 @@ describe('MarketsHodlerVault', function () { await expect(hodlerVault.connect(user).seed( stakeDuration, infinity.address, + registryFake.address, pairAddress, uniswapRouter.address, acceleratorVaultFake.address, @@ -253,4 +260,191 @@ describe('MarketsHodlerVault', function () { assertBNequal(amount3, lockedLP3[1]); assertBNequal(lpBalanceAfter3.sub(lpBalanceBefore3), expectedLpAmount3); }); + + describe('Registry recover', function() { + it('should be possible to topup weth balance on LV from MarketsRegistry via registryRecover', async function() { + const recoverValue = utils.parseEther('0.0001'); + const oneEth = utils.parseEther('1'); + + assertBNequal(await weth.balanceOf(owner.address), 0); + await weth.deposit({ value: oneEth }); + assertBNequal(await weth.balanceOf(owner.address), oneEth); + + await weth.transfer(registryFake.address, recoverValue); + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), 0) + + + await hodlerVault.registryRecover(); + + assertBNequal(await weth.balanceOf(registryFake.address), 0); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), recoverValue); + }); + + it('should NOT be possible to topup weth balance on LV from MarketsRegistry via registryRecover if registry balance less than min 0.0001', async function() { + const recoverValue = utils.parseEther('0.00009'); + const oneEth = utils.parseEther('1'); + + assertBNequal(await weth.balanceOf(owner.address), 0); + await weth.deposit({ value: oneEth }); + assertBNequal(await weth.balanceOf(owner.address), oneEth); + + await weth.transfer(registryFake.address, recoverValue); + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), 0) + + + await hodlerVault.registryRecover(); + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), 0); + }); + + it('should be possible to topup weth balance on LV from MarketsRegistry via registryRecover if some fee goes to secondary address', async function() { + const recoverValue = utils.parseEther('0.0001'); + const oneEth = utils.parseEther('1'); + + assertBNequal(await weth.balanceOf(owner.address), 0); + await weth.deposit({ value: oneEth }); + assertBNequal(await weth.balanceOf(owner.address), oneEth); + + await weth.transfer(registryFake.address, recoverValue); + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), 0) + + await registryFake.enableSecondaryReceiver(accounts[5].address); + + await hodlerVault.registryRecover(); + + assertBNequal(await weth.balanceOf(registryFake.address), 0); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), recoverValue.mul(80).div(100)); + }); + + it('should be possible to topup weth balance on LV from MarketsRegistry inside purchaseLP', async function() { + const recoverValue = utils.parseEther('0.0001'); + const oneEth = utils.parseEther('1'); + + assertBNequal(await weth.balanceOf(owner.address), 0); + await weth.deposit({ value: oneEth }); + assertBNequal(await weth.balanceOf(owner.address), oneEth); + + await weth.transfer(registryFake.address, recoverValue); + + const transferToHodlerVault = utils.parseEther('10'); + const purchaseValue = utils.parseUnits('5000', baseUnit); + + await owner.sendTransaction({ to: hodlerVault.address, value: transferToHodlerVault }); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), transferToHodlerVault); + + const feeReceiverBalanceBefore = await infinity.balanceOf(acceleratorVaultFake.address); + await infinity.approve(hodlerVault.address, ethers.constants.MaxUint256); + + + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + + const purchaseLP = await hodlerVault.purchaseLP(purchaseValue); + + const hodlerBalanceAfterPurchaseLP = utils.parseEther('6.5'); + assertBNequal(await weth.balanceOf(registryFake.address), 0); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), hodlerBalanceAfterPurchaseLP.add(recoverValue)); + + + + + + const receipt = await purchaseLP.wait(); + + const lockedLpLength = await hodlerVault.lockedLPLength(owner.address); + assertBNequal(lockedLpLength, 1); + + const lockedLP = await hodlerVault.getLockedLP(owner.address, 0); + const amount = receipt.events[11].args[1]; + const timestamp = receipt.events[11].args[4]; + assert.equal(lockedLP[0], owner.address); + assertBNequal(lockedLP[1], amount); + assertBNequal(lockedLP[2], timestamp); + + const { feeReceiver: expectedFeeReceiver } = await hodlerVault.config(); + const { percentageAmount } = receipt.events[12].args; + const estimatedReceiverAmount = (purchaseValue * purchaseFee) / 100; + const feeReceiverBalanceAfter = await infinity.balanceOf(acceleratorVaultFake.address); + + assert.equal(expectedFeeReceiver, acceleratorVaultFake.address); + assertBNequal(feeReceiverBalanceAfter.sub(feeReceiverBalanceBefore), estimatedReceiverAmount); + assertBNequal(estimatedReceiverAmount, percentageAmount); + + }); + + it('should NOT topup weth balance on LV from MarketsRegistry inside purchaseLP if registry balance less than min 0.0001', async function() { + const recoverValue = utils.parseEther('0.00009'); + const oneEth = utils.parseEther('1'); + + assertBNequal(await weth.balanceOf(owner.address), 0); + await weth.deposit({ value: oneEth }); + assertBNequal(await weth.balanceOf(owner.address), oneEth); + + await weth.transfer(registryFake.address, recoverValue); + + const transferToHodlerVault = utils.parseEther('10'); + const purchaseValue = utils.parseUnits('5000', baseUnit); + + await owner.sendTransaction({ to: hodlerVault.address, value: transferToHodlerVault }); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), transferToHodlerVault); + + const feeReceiverBalanceBefore = await infinity.balanceOf(acceleratorVaultFake.address); + await infinity.approve(hodlerVault.address, ethers.constants.MaxUint256); + + + + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + + const purchaseLP = await hodlerVault.purchaseLP(purchaseValue); + + const hodlerBalanceAfterPurchaseLP = utils.parseEther('6.5'); + assertBNequal(await weth.balanceOf(registryFake.address), recoverValue); + assertBNequal(await weth.balanceOf(hodlerVault.address), 0); + assertBNequal(await ethers.provider.getBalance(hodlerVault.address), hodlerBalanceAfterPurchaseLP); + + + + + + const receipt = await purchaseLP.wait(); + + const lockedLpLength = await hodlerVault.lockedLPLength(owner.address); + assertBNequal(lockedLpLength, 1); + + const lockedLP = await hodlerVault.getLockedLP(owner.address, 0); + const amount = receipt.events[9].args[1]; + const timestamp = receipt.events[9].args[4]; + assert.equal(lockedLP[0], owner.address); + assertBNequal(lockedLP[1], amount); + assertBNequal(lockedLP[2], timestamp); + + const { feeReceiver: expectedFeeReceiver } = await hodlerVault.config(); + const { percentageAmount } = receipt.events[10].args; + const estimatedReceiverAmount = (purchaseValue * purchaseFee) / 100; + const feeReceiverBalanceAfter = await infinity.balanceOf(acceleratorVaultFake.address); + + assert.equal(expectedFeeReceiver, acceleratorVaultFake.address); + assertBNequal(feeReceiverBalanceAfter.sub(feeReceiverBalanceBefore), estimatedReceiverAmount); + assertBNequal(estimatedReceiverAmount, percentageAmount); + + }); + + }); + });