Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions contracts/fake/MarketsRegistryFake.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
41 changes: 41 additions & 0 deletions contracts/fake/SafeBEP20.sol
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
8 changes: 8 additions & 0 deletions contracts/interfaces/IMarketsRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pragma solidity 0.7.4;


import "./IBEP20.sol";

interface IMarketsRegistry {
function recoverTokens(IBEP20 token, address destination) external;
}
4 changes: 2 additions & 2 deletions contracts/vaults/HodlerVaultSpace.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions contracts/vaults/MarketsHodlerVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +43,7 @@ contract MarketsHodlerVault is Ownable {

struct HodlerVaultConfig {
IBEP20 infinityToken;
IMarketsRegistry registry;
IUniswapV2Router02 uniswapRouter;
IUniswapV2Pair tokenPair;
address weth;
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand Down
194 changes: 194 additions & 0 deletions test/markets-hodler-vault.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('MarketsHodlerVault', function () {
let owner;
let user;
let acceleratorVaultFake;
let registryFake;

let weth;
let infinity;
Expand Down Expand Up @@ -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,
Expand All @@ -85,6 +91,7 @@ describe('MarketsHodlerVault', function () {
await expect(hodlerVault.connect(user).seed(
stakeDuration,
infinity.address,
registryFake.address,
pairAddress,
uniswapRouter.address,
acceleratorVaultFake.address,
Expand Down Expand Up @@ -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);

});

});

});