import assert from 'assert';
import BigNumber from 'bignumber.js';
import { loophole } from '../../../interfaces';
import ERC20Contract from '../../ERC20/ERC20Contract';
import ETHUtils from '../../../utils/ETHUtils';
import Numbers from '../../../utils/Numbers';
import IContract from '../../IContract';
/** @typedef {Object} Loophole~Options
* @property {Boolean} test
* @property {Boolean} localtest
* @property {Web3Connection} [web3Connection=Web3Connection]
* @property {address} [contractAddress]
* @property {address} [lpTokenAddress]
* @property {address} [ethUtilsAddress]
*/
/**
* Loophole Object
* @class Loophole
* @param {Loophole~Options} options
*/
export default class Loophole extends IContract {
constructor(params = {}) {
super({ abi: loophole, ...params });
if (!params.lpTokenAddress) {
throw new Error('Please provide an LP token address');
}
if (!params.ethUtilsAddress) {
throw new Error('Please provide an ETHUtils contract address');
}
if (!params.swapRouterAddress) {
throw new Error('Please provide an SwapRouter contract address');
}
this.params.LPTokenContract = new ERC20Contract({
web3Connection: this.web3Connection,
contractAddress: params.lpTokenAddress,
});
this.params.ETHUtils = new ETHUtils({
web3Connection: this.web3Connection,
contractAddress: params.ethUtilsAddress,
});
this.params.LPTokenAddress = params.lpTokenAddress; // LP token address
this.params.ethUtilsAddress = params.ethUtilsAddress; // ETHUtils contract
this.params.swapRouterAddress = params.swapRouterAddress; // swapRouter exchange contract address
}
/**
* @returns {Promise<address>}
*/
async lpToken() {
// return await this.getContract().methods.lpToken().call();
return this.params.LPTokenAddress;
}
/**
* @returns {Promise<uint256>}
*/
async lpTokensPerBlock() {
const decimals = await this.ETHUtils().decimals(this.params.LPTokenAddress);
const res = await this.getContract().methods.lpTokensPerBlock().call();
return Numbers.fromDecimalsToBN(res, decimals);
}
/**
* @param {address}
* @returns {Promise<bool>}
*/
poolExists(address) {
return this.getContract().methods.poolExists(address).call();
}
/**
* @returns {Promise<uint24>}
*/
poolFee() {
// TODO: ???
return this.getContract().methods.poolFee().call();
}
/**
* @returns {Promise<uint256>}
*/
startBlock() {
return this.getContract().methods.startBlock().call();
}
/**
* @returns {Promise<uint256>}
*/
async exitPenalty() {
// penalty is stored as 20 for 20%
const res = await this.getContract().methods.exitPenalty().call();
return Number(res / 100.0);
}
/**
* @returns {Promise<uint256>}
*/
async exitPenaltyLP() {
// penalty is stored as 20 for 20%
const res = await this.getContract().methods.exitPenaltyLP().call();
return Number(res / 100.0);
}
/**
* @returns {Promise<address>}
*/
async swapRouter() {
// return await this.getContract().methods.swapRouter().call();
return this.params.swapRouterAddress;
}
/**
* @returns {Promise<uint256>}
*/
totalAllocPoint() {
return this.getContract().methods.totalAllocPoint().call();
}
/**
* @param {address} newOwner
* @returns {Promise<void>}
*/
// transferOwnership({ newOwner }, options) {
// return this.__sendTx(this.getContract().methods.transferOwnership(newOwner, options))
// };
/**
* Add/enable new pool, only owner mode
* @dev ADD | NEW TOKEN POOL
* @param {Object} params
* @param {address} params.token Token address as IERC20
* @param {uint256} params.allocPoint Pool allocation point/share distributed to this pool from mining rewards
* @returns {Promise<uint256>} pid added token pool index
*/
add({ token, allocPoint }, options) {
return this.__sendTx(
this.getContract().methods.add(token, allocPoint),
options,
);
}
/**
* Update pool allocation point/share
* @dev UPDATE | ALLOCATION POINT
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {uint256} params.allocPoint Set allocation point/share for pool id
* @param {bool} params.withUpdate Update all pools and distribute mining reward for all
* @returns {Promise<void>}
*/
set({ pid, allocPoint, withUpdate }, options) {
return this.__sendTx(
this.getContract().methods.set(pid, allocPoint, withUpdate),
options,
);
}
/**
* Stake tokens on given pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {uint256} params.amount The token amount user wants to stake to the pool.
* @returns {Promise<void>}
*/
async stake({ pid, amount }, options) {
const amount2 = await this.fromBNToDecimals(amount, pid);
return this.__sendTx(
this.getContract().methods.stake(pid, amount2),
options,
);
}
/**
* User exit staking amount from main pool, require main pool only
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {uint256} params.amount The token amount user wants to exit/unstake from the pool.
* @param {uint256} params.amountOutMinimum The min LP token amount expected to be received from exchange,
* needed from outside for security reasons, using zero value in production is discouraged.
* @returns {Promise<uint256>} net tokens amount sent to user address
*/
async exit({ pid, amount, amountOutMinimum }, options) {
const amount2 = await this.fromBNToDecimals(amount, pid);
return this.__sendTx(
this.getContract().methods.exit(pid, amount2, amountOutMinimum),
options,
);
}
/**
* User exit staking amount from LOOP pool, require LOOP pool only
* @param {Object} params
* @param {uint256} params.amount The token amount user wants to exit/unstake from the pool.
* @returns {Promise<uint256>} net tokens amount sent to user address
*/
exitLP({ amount }, options) {
const amount2 = Numbers.fromBNToDecimals(amount, this.LPTokenContract().getDecimals());
return this.__sendTx(
this.getContract().methods.exit(amount2),
options,
);
}
/**
* View pending LP token rewards for user
* @dev VIEW | PENDING REWARD
* @param {Object} params
* @param {uint256} params.pid Pool id of main pool
* @param {address} params.user User address to check pending rewards for
* @returns {Promise<uint256>} Pending LP token rewards for user
*/
async getUserReward({ pid, user }) {
const res = await this.getContract().methods.getUserReward(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
/**
* User collects his share of LP tokens reward
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<uint256>} LP reward tokens amount sent to user address
*/
collectRewards({ pid }, options) {
return this.__sendTx(
this.getContract().methods.collectRewards(pid),
options,
);
}
// TODO: fix this fn, it always returns zero
/**
* @param {Object} params
* @param {uint256} params.pid
* @returns {Promise<uint256>}
*/
async collectRewardsCall({ pid }) {
const res = await this.getContract().methods.collectRewards(pid).call();
return this.fromDecimalsToBN(res, pid);
}
/**
* @param {uint256} pid
* @returns {Promise<uint256[]>}
*/
// collectRewardsAll(options) {
// return this.__sendTx(this.getContract().methods.collectRewardsAll(), options);
// }
/**
* @param {uint256} pid
* @returns {Promise<uint256[]>}
*/
// async collectRewardsAllCall() {
// const res = await this.getContract().methods.collectRewardsAll().call();
// return Promise.all(res.map((val, idx) => this.fromDecimalsToBN(val, idx)));
// }
/**
* Current total user stake in a given pool
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user The user address
* @returns {Promise<uint256>} stake tokens amount
*/
async currentStake({ pid, user }) {
const res = await this.getContract().methods.currentStake(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
/**
* Percentage of how much a user has earned so far from the other users exit, would be just a statistic
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user The user address
* @returns {Promise<uint256>} earnings percent as integer
*/
async earnings({ pid, user }) {
const res = await this.getContract().methods.earnings(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
/**
* Get blocks range given two block numbers, usually computes blocks elapsed since last mining reward block.
* @dev RETURN | BLOCK RANGE SINCE LAST REWARD AS REWARD MULTIPLIER | INCLUDES START BLOCK
* @param {Object} params
* @param {uint256} params.from block start
* @param {uint256} params.to block end
* @returns {Promise<uint256>} blocks count
*/
async getBlocksFromRange({ from, to }) {
const res = await this.getContract().methods.getBlocksFromRange(from, to).call();
return BigNumber(res);
}
/**
* Update all pools for mining rewards
* @dev UPDATE | (ALL) REWARD VARIABLES | BEWARE: HIGH GAS POTENTIAL
* @returns {Promise<void>}
*/
massUpdatePools(options) {
return this.__sendTx(
this.getContract().methods.massUpdatePools(),
options,
);
}
/**
* Update pool to trigger LP tokens reward since last reward mining block
* @dev UPDATE | (ONE POOL) REWARD VARIABLES
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<void>}
*/
updatePool({ pid }, options) {
return this.__sendTx(
this.getContract().methods.updatePool(pid),
options,
);
}
/**
* Update pool to trigger LP tokens reward since last reward mining block, function call for results and no transaction
* @dev UPDATE | (ONE POOL) REWARD VARIABLES
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<uint256>} blocksElapsed Blocks elapsed since last reward block
* @returns {Promise<uint256>} lpTokensReward Amount of LP tokens reward since last reward block
* @returns {Promise<uint256>} accLPtokensPerShare Pool accumulated LP tokens per pool token (per share)
*/
async updatePoolCall({ pid }) {
const res = await this.getContract().methods.updatePool(pid).call();
// res.blocksElapsed, res.lpTokensReward, res.accLPtokensPerShare
const decimals = this.LPTokenContract().getDecimals();
const lpPerTokenMult = await this.getContract().methods.LPtokensPerShareMultiplier().call();
const accLPperToken = BigNumber(res.accLPtokensPerShare).div(lpPerTokenMult);
return {
blocksElapsed: BigNumber(res.blocksElapsed),
lpTokensReward: Numbers.fromDecimalsToBN(res.lpTokensReward, decimals),
accLPtokensPerShare: accLPperToken,
};
}
/**
* Get LP tokens reward for given pool id, only MAIN pool, LOOP pool reward will always be zero
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<uint256>} tokensReward Tokens amount as reward based on last mining block
*/
async getPoolReward({ pid }) {
// LP tokens reward
const res = await this.getContract().methods.getPoolReward(pid).call();
const decimals = this.LPTokenContract().getDecimals();
return Numbers.fromDecimalsToBN(res, decimals);
}
/**
* Get pool token decimals given pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<uint256>} poolTokenDecimals
*/
async getPoolTokenDecimals({ pid }) {
// TODO: cache decimals by pid for token address
const res = await this.getContract().methods.getPool(pid).call();
return this.ETHUtils().decimals(res[0]); // token = res[0]
}
/**
* Convert given tokens amount integer to float number with decimals for UI.
* @function
* @param {number} amount Tokens amount to convert
* @param {number} pid Pool id
* @returns {Promise<number>} tokensAmount
*/
async fromDecimalsToBN(amount, pid) {
return Numbers.fromDecimalsToBN(
amount,
await this.getPoolTokenDecimals({ pid }),
);
}
/**
* Convert float number with decimals from UI to tokens amount integer for smart contract function.
* @function
* @param {number} amount Tokens float amount to convert
* @param {number} pid Pool id
* @returns {Promise<number>} tokensAmount
*/
async fromBNToDecimals(amount, pid) {
return Numbers.fromBNToDecimals(
amount,
await this.getPoolTokenDecimals({ pid }),
);
}
/**
* Get current block timestamp
* @returns {Promise<uint256>} current block timestamp
*/
getBlockTimestamp() {
return this.getContract().methods.getBlockTimestamp().call();
}
/**
* Get current block number
* @returns {Promise<uint256>} current block number
*/
getBlockNumber() {
return this.getContract().methods.getBlockNumber().call();
}
/** @typedef {Object} Loophole~PoolInfo
* @property {address} token
* @property {uint256} allocPoint
* @property {uint256} lastRewardBlock
* @property {uint256} totalPool
* @property {uint256} entryStakeTotal
* @property {uint256} totalDistributedPenalty
* @property {uint256} accLPtokensPerShare
*/
/**
* Get pool attributes
* @param {Object} params
* @param {uint256} params.pid Pool id
* @returns {Promise<Loophole~PoolInfo>}
*/
async getPool({ pid }) {
const res = await this.getContract().methods.getPool(pid).call();
const token = res[0];
const decimals = await this.ETHUtils().decimals(token);
// const lpDecimals = this.LPTokenContract().getDecimals();
const lpPerTokenMult = await this.getContract().methods.LPtokensPerShareMultiplier().call();
const accLPperToken = BigNumber(res[6]).div(lpPerTokenMult);
return {
token: res[0],
allocPoint: BigNumber(res[1]),
lastRewardBlock: BigNumber(res[2]),
totalPool: Numbers.fromDecimalsToBN(res[3], decimals),
entryStakeTotal: Numbers.fromDecimalsToBN(res[4], decimals),
totalDistributedPenalty: Numbers.fromDecimalsToBN(res[5], decimals),
accLPtokensPerShare: accLPperToken,
};
}
/**
* Get pool attributes, raw with no conversion.
* @param {Object} params
* @param {uint256} params.pid
* @returns {Promise<Loophole~PoolInfo>}
*/
getPoolInfo({ pid }) {
return this.getContract().methods.getPoolInfo(pid).call();
}
/**
* Get pools array length
* @returns {Promise<uint256>} pools count
*/
poolsCount() {
return this.getContract().methods.poolsCount().call();
}
/** @typedef {Object} Loophole~UserInfo
* @property {uint256} entryStake Accumulated staked amount
* @property {uint256} unstake Accumulated net unstaked amount or exitStake
* @property {uint256} entryStakeAdjusted Current user adjusted stake in the pool
* @property {uint256} payRewardMark LP tokens reward mark to control new rewards and already paid ones
*/
/**
* Get pool attributes as struct
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user User address
* @returns {Promise<Loophole~UserInfo>}
*/
async getUserInfo({ pid, user }) {
const res = await this.getContract().methods.getUserInfo(pid, user).call();
return {
...res,
entryStake: await this.fromDecimalsToBN(res.entryStake, pid),
unstake: await this.fromDecimalsToBN(res.unstake, pid),
entryStakeAdjusted: await this.fromDecimalsToBN(res.entryStakeAdjusted, pid),
payRewardMark: await this.fromDecimalsToBN(res.payRewardMark, 0), // 0 is LOOP pool id token
};
}
/**
* Get total accumulated 'entry stake' so far for a given user address in a pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user User address
* @returns {Promise<uint256>} user entry stake amount in a given pool
*/
async getTotalEntryStakeUser({ pid, user }) {
const res = await this.getContract().methods.getTotalEntryStakeUser(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
/**
* Get total accumulated 'unstake' so far for a given user address in a pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user User address
* @returns {Promise<uint256>} user unstake amount in a given pool
*/
async getTotalUnstakeUser({ pid, user }) {
const res = await this.getContract().methods.getTotalUnstakeUser(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
// WARNING: Function NOT fully working:
// when user had profit from others exits and entryStake is less than what he had withdrown
// SOLUTION? maybe return only what is greather than zero?
/**
* Get 'entry stake adjusted' for a given user address in a pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user User address
* @returns {Promise<uint256>} user entry stake adjusted amount in given pool
*/
async getCurrentEntryStakeUser({ pid, user }) {
// const res = await this.getContract().methods.getCurrentEntryStakeUser(pid, user).call();
// return await this.fromDecimalsToBN(res, pid);
let selExitPenalty;
if (this.isLoopPoolId(pid)) {
selExitPenalty = await this.exitPenaltyLP();
}
else {
selExitPenalty = await this.exitPenalty();
}
const userInfo = await this.getUserInfo({ pid, user });
const totalGrossUnstaked = userInfo.unstake.div(1 - selExitPenalty);
return userInfo.entryStake.minus(totalGrossUnstaked);
}
/**
* Returns true if given pis is a LOOP pool id, false otherwise.
* @param {uint256} pid Pool id
* @returns {Boolean}
*/
// eslint-disable-next-line class-methods-use-this
isLoopPoolId(pid) {
return (pid === 0);
}
/**
* Get 'entry stake adjusted' for a given user address in a pool id
* @param {Object} params
* @param {uint256} params.pid Pool id
* @param {address} params.user User address
* @returns {Promise<uint256>} user entry stake adjusted amount in given pool
*/
async getEntryStakeAdjusted({ pid, user }) {
const res = await this.getContract().methods.getEntryStakeAdjusted(pid, user).call();
return this.fromDecimalsToBN(res, pid);
}
/**
*
* @return {Promise<void>}
* @throws {Error} Contract is not deployed, first deploy it and provide a contract address
*/
__assert = async () => {
if (!this.getAddress()) {
throw new Error(
'Contract is not deployed, first deploy it and provide a contract address',
);
}
/* Use ABI */
this.params.contract.use(loophole, this.getAddress());
if (!this.params.LPTokenContract) {
this.params.LPTokenContract = new ERC20Contract({
web3Connection: this.web3Connection,
contractAddress: this.params.LPTokenAddress,
});
}
await this.params.LPTokenContract.__assert();
if (!this.params.ETHUtils) {
this.params.ETHUtils = new ETHUtils({
web3Connection: this.web3Connection,
contractAddress: this.params.ethUtilsAddress,
});
}
await this.params.ETHUtils.__assert();
};
/**
* Deploy the Loophole Contract
* @function
* @param {Object} params Parameters
* @param {string} params.name Name of token
* @param {address} swapRouter
* @param {address} lpToken
* @param {uint256} lpTokensPerBlock
* @param {uint256} startBlock
* @param {uint256} exitPenalty
* @param {uint256} exitPenaltyLP
* @param {IContract~TxOptions} options
* @return {Promise<*|undefined>}
* @throws {Error} No Token Address Provided
*/
deploy = async (
{
swapRouter, lpToken, lpTokensPerBlock, startBlock, exitPenalty, exitPenaltyLP,
},
options,
) => {
// if (!this.LPTokenContract()) {
// throw new Error('No LPTokenContract Address Provided');
// }
assert(lpToken === this.LPTokenContract().getAddress()); // LPTokenAddress
assert(swapRouter === this.params.swapRouterAddress);
const lpTokenDecimals = await this.ETHUtils().decimals(lpToken);
const lpTokensPerBlock1 = Numbers.fromBNToDecimals(lpTokensPerBlock, lpTokenDecimals);
const params = [ swapRouter, lpToken, lpTokensPerBlock1, startBlock, exitPenalty, exitPenaltyLP ];
const res = await this.__deploy(params, options);
this.params.contractAddress = res.contractAddress;
/* Call to Backend API */
await this.__assert();
return res;
};
/**
* @function
* @return LPTokenContract|undefined
*/
LPTokenContract() {
return this.params.LPTokenContract;
}
/**
* @function
* @return ETHUtils|undefined
*/
ETHUtils() {
return this.params.ETHUtils;
}
}
Source