@@ -247,7 +181,12 @@ const LiquidityRewardCard = ({
className={"liquidity__withdraw btn btn-secondary btn-lg w-100"}
disabled={!gt(rewardBalance, 0) && !gt(lpBalance, 0)}
onSubmitAction={(awaitingPromise) =>
- withdrawLiquidityRewards(liquidityPairContractName, awaitingPromise)
+ withdrawLiquidityRewards(
+ liquidityPairContractName,
+ lpBalance,
+ pool,
+ awaitingPromise
+ )
}
>
withdraw all
diff --git a/solidity/dashboard/src/components/MaxAmountAddon.jsx b/solidity/dashboard/src/components/MaxAmountAddon.jsx
new file mode 100644
index 0000000000..ab8b081949
--- /dev/null
+++ b/solidity/dashboard/src/components/MaxAmountAddon.jsx
@@ -0,0 +1,16 @@
+import React from "react"
+import * as Icons from "./Icons"
+import Tag from "./Tag"
+
+const MaxAmountAddon = ({ onClick, text, ...otherProps }) => {
+ return (
+
+ )
+}
+
+export default MaxAmountAddon
diff --git a/solidity/dashboard/src/components/MetricsTile.jsx b/solidity/dashboard/src/components/MetricsTile.jsx
new file mode 100644
index 0000000000..1a168ebf52
--- /dev/null
+++ b/solidity/dashboard/src/components/MetricsTile.jsx
@@ -0,0 +1,27 @@
+import React from "react"
+import Tooltip from "./Tooltip"
+import * as Icons from "./Icons"
+
+const MetricsTile = ({ className, children, style = {} }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+MetricsTile.Tooltip = ({ className, children, ...restTooltipProps }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default MetricsTile
diff --git a/solidity/dashboard/src/components/WithdrawETHModal.jsx b/solidity/dashboard/src/components/WithdrawETHModal.jsx
index 06d883820d..db261fcd2e 100644
--- a/solidity/dashboard/src/components/WithdrawETHModal.jsx
+++ b/solidity/dashboard/src/components/WithdrawETHModal.jsx
@@ -5,7 +5,7 @@ import web3Utils from "web3-utils"
import { useWeb3Context } from "./WithWeb3Context"
import * as Icons from "./Icons"
import AvailableEthAmount from "./AvailableEthAmount"
-import AvailableETHForm from "./AvailableETHForm"
+import AvailableETHForm from "./AvailableTokenForm"
import {
withdrawUnbondedEth,
withdrawUnbondedEthAsManagedGrantee,
diff --git a/solidity/dashboard/src/components/liquidity/APY.jsx b/solidity/dashboard/src/components/liquidity/APY.jsx
new file mode 100644
index 0000000000..dbd0342f55
--- /dev/null
+++ b/solidity/dashboard/src/components/liquidity/APY.jsx
@@ -0,0 +1,66 @@
+import React, { useMemo } from "react"
+import CountUp from "react-countup"
+import BigNumber from "bignumber.js"
+import { Skeleton } from "../skeletons"
+import {
+ displayPercentageValue,
+ formatPercentage,
+} from "../../utils/general.utils"
+
+export const APY = ({
+ apy,
+ isFetching = false,
+ skeletonProps = { tag: "h2", shining: true, color: "grey-10" },
+ className = "",
+}) => {
+ const formattedApy = useMemo(() => {
+ const bn = new BigNumber(apy).multipliedBy(100)
+ if (bn.isEqualTo(Infinity)) {
+ return Infinity
+ } else if (bn.isLessThan(0.01) && bn.isGreaterThan(0)) {
+ return 0.01
+ } else if (bn.isGreaterThan(999)) {
+ return 999
+ }
+
+ return formatPercentage(bn)
+ }, [apy])
+
+ return isFetching ? (
+
+ ) : (
+
+ {formattedApy === Infinity ? (
+ ∞
+ ) : (
+
+ )}
+
+ )
+}
+
+APY.TooltipContent = () => {
+ return (
+ <>
+ Pool APY is calculated using the
+
+ Uniswap subgraph API
+
+ to fetch the total pool value and KEEP token in USD.
+ >
+ )
+}
+
+export default APY
diff --git a/solidity/dashboard/src/components/liquidity/ShareOfPool.jsx b/solidity/dashboard/src/components/liquidity/ShareOfPool.jsx
new file mode 100644
index 0000000000..3eb90efa01
--- /dev/null
+++ b/solidity/dashboard/src/components/liquidity/ShareOfPool.jsx
@@ -0,0 +1,39 @@
+import React, { useMemo } from "react"
+import CountUp from "react-countup"
+import BigNumber from "bignumber.js"
+import { Skeleton } from "../skeletons"
+import {
+ displayPercentageValue,
+ formatPercentage,
+} from "../../utils/general.utils"
+
+const ShareOfPool = ({
+ percentageOfTotalPool,
+ isFetching = false,
+ skeletonProps = { tag: "h2", shining: true, color: "grey-10" },
+ className = "",
+}) => {
+ const formattedPercentageOfTotalPool = useMemo(() => {
+ const bn = new BigNumber(percentageOfTotalPool)
+ return bn.isLessThan(0.01) && bn.isGreaterThan(0)
+ ? 0.01
+ : formatPercentage(bn)
+ }, [percentageOfTotalPool])
+
+ return isFetching ? (
+
+ ) : (
+
+
+
+ )
+}
+
+export default ShareOfPool
diff --git a/solidity/dashboard/src/components/liquidity/index.js b/solidity/dashboard/src/components/liquidity/index.js
new file mode 100644
index 0000000000..12a5399c23
--- /dev/null
+++ b/solidity/dashboard/src/components/liquidity/index.js
@@ -0,0 +1,4 @@
+import APY from "./APY"
+import ShareOfPool from "./ShareOfPool"
+
+export { APY, ShareOfPool }
diff --git a/solidity/dashboard/src/constants/constants.js b/solidity/dashboard/src/constants/constants.js
index ddf5b563d3..aa01109d2c 100644
--- a/solidity/dashboard/src/constants/constants.js
+++ b/solidity/dashboard/src/constants/constants.js
@@ -18,6 +18,7 @@ export const LP_REWARDS_TBTC_SADDLE_CONTRACT_NAME = "LPRewardsTBTCSaddle"
export const LP_REWARDS_KEEP_ETH_CONTRACT_NAME = "LPRewardsKEEPETHContract"
export const LP_REWARDS_TBTC_ETH_CONTRACT_NAME = "LPRewardsTBTCETHContract"
export const LP_REWARDS_KEEP_TBTC_CONTRACT_NAME = "LPRewardsKEEPTBTCContract"
+export const KEEP_TOKEN_GEYSER_CONTRACT_NAME = "keepTokenGeyserContract"
export const PENDING_STATUS = "PENDING"
export const COMPLETE_STATUS = "COMPLETE"
@@ -72,4 +73,9 @@ export const LIQUIDITY_REWARD_PAIRS = {
address: "0x854056fd40c1b52037166285b2e54fee774d33f6",
pool: "UNISWAP",
},
+ KEEP_ONLY: {
+ contractName: KEEP_TOKEN_GEYSER_CONTRACT_NAME,
+ label: "KEEP",
+ pool: "TOKEN_GEYSER",
+ },
}
diff --git a/solidity/dashboard/src/contracts.js b/solidity/dashboard/src/contracts.js
index 537b944f98..33fe25b463 100644
--- a/solidity/dashboard/src/contracts.js
+++ b/solidity/dashboard/src/contracts.js
@@ -21,6 +21,7 @@ import LPRewardsKEEPETH from "@keep-network/keep-ecdsa/artifacts/LPRewardsKEEPET
import LPRewardsTBTCETH from "@keep-network/keep-ecdsa/artifacts/LPRewardsTBTCETH.json"
import LPRewardsKEEPTBTC from "@keep-network/keep-ecdsa/artifacts/LPRewardsKEEPTBTC.json"
import LPRewardsTBTCSaddle from "@keep-network/keep-ecdsa/artifacts/LPRewardsTBTCSaddle.json"
+import KeepOnlyPool from "@keep-network/keep-core/artifacts/KeepTokenGeyser.json"
import IERC20 from "@keep-network/keep-core/artifacts/IERC20.json"
import SaddleSwap from "./contracts-artifacts/SaddleSwap.json"
import Web3 from "web3"
@@ -44,6 +45,7 @@ import {
LP_REWARDS_TBTC_ETH_CONTRACT_NAME,
LP_REWARDS_KEEP_TBTC_CONTRACT_NAME,
LP_REWARDS_TBTC_SADDLE_CONTRACT_NAME,
+ KEEP_TOKEN_GEYSER_CONTRACT_NAME,
} from "./constants/constants"
export const CONTRACT_DEPLOY_BLOCK_NUMBER = {
@@ -125,6 +127,10 @@ const contracts = {
artifact: LPRewardsTBTCSaddle,
withDeployBlock: true,
},
+ [KEEP_TOKEN_GEYSER_CONTRACT_NAME]: {
+ artifact: KeepOnlyPool,
+ withDeployBlock: true,
+ },
}
export async function getKeepTokenContractDeployerAddress(web3) {
diff --git a/solidity/dashboard/src/css/commons.less b/solidity/dashboard/src/css/commons.less
index 6ec993d09c..3e900c20d0 100644
--- a/solidity/dashboard/src/css/commons.less
+++ b/solidity/dashboard/src/css/commons.less
@@ -25,6 +25,38 @@
padding: 2rem;
margin-bottom: 1.2rem;
.box-shadow(0px, 4px, 4px,rgba(196, 196, 196, 0.3));
+
+ &--metrics {
+ background-color: @color-grey-20;
+ transition: background-color 0.5s ease;
+ text-align: center;
+ border-radius: 8px;
+ padding: 1rem;
+ margin: 0 0.2rem;
+ height: 9rem;
+ width: 10rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ position: relative;
+
+ &__tooltip {
+ &.tooltip--simple {
+ &--top, &--bottom {
+ width: 12px;
+ height: 12px;
+ position: absolute;
+ top: 0;
+ right: 8px;
+
+ .tooltip__content-wrapper {
+ text-align: left;
+ min-width: 16rem;
+ }
+ }
+ }
+ }
+ }
}
//background colors
@@ -284,7 +316,7 @@ svg.wallet-icon {
}
}
-svg.time-icon, svg.success-icon, svg.keep-outline, svg.tbtc-icon {
+svg.time-icon, svg.success-icon, svg.keep-outline, svg.tbtc-icon, svg.reward-icon {
&--black {
path {
fill: @color-black;
diff --git a/solidity/dashboard/src/css/liquidity-page.less b/solidity/dashboard/src/css/liquidity-page.less
index 8fea003dbd..1411ddbfca 100644
--- a/solidity/dashboard/src/css/liquidity-page.less
+++ b/solidity/dashboard/src/css/liquidity-page.less
@@ -51,33 +51,6 @@
.liquidity__info, .liquidity__info--locked {
display: flex;
justify-content: center;
-
- .liquidity__info-tile {
- transition: background-color 0.5s ease;
- text-align: center;
- border-radius: 8px;
- padding: 1rem;
- margin: 0 0.2rem;
- height: 9rem;
- width: 10rem;
- display: flex;
- flex-direction: column;
- justify-content: center;
- position: relative;
-
- .liquidity__info-tile__tooltip {
- width: 12px;
- height: 12px;
- position: absolute;
- top: 0;
- right: 8px;
-
- .tooltip__content-wrapper {
- text-align: left;
- min-width: 16rem;
- }
- }
- }
}
.liquidity__new-user-info {
@@ -108,15 +81,6 @@
}
}
- .liquidity__info--locked {
- .liquidity__info-tile {
- &:extend(.bg-grey-20);
- .liquidity__info-tile__title {
- color: @color-grey-60!important;
- }
- }
- }
-
.liquidity__reward-balance {
margin: 2rem 0;
@@ -155,6 +119,73 @@
}
+.liquidity__info--locked {
+ .liquidity__info-tile {
+ &:extend(.bg-grey-20);
+ .liquidity__info-tile__title {
+ color: @color-grey-60!important;
+ }
+ }
+}
+
+
+
+.keep-only-pool {
+ display: flex;
+ flex-direction: column;
+ margin: 0 3.2rem;
+
+ &__overview {
+ flex: 1 0 0;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 0;
+
+ > section:first-of-type {
+ flex: 1 1 auto;
+ margin-right: 2rem;
+ }
+
+ &__info-tiles {
+ display: flex;
+ justify-content: space-evenly;
+ margin-top: 1rem;
+
+ > .liquidity__info-tile:first-of-type {
+ margin: 0;
+ margin-bottom: 1rem;
+ }
+ }
+ }
+
+ &__icon {
+ background: url("../static/svg/keep-only-pool.svg");
+ background-color: @color-grey-10;
+ background-size: cover;
+ background-position: center center;
+ background-repeat:no-repeat;
+ height: 400px;
+ }
+
+ @media screen and (min-width: 1000px) {
+ flex-direction: row;
+ &__overview {
+ flex-direction: row;
+ margin-bottom: 1.2rem;
+
+ &__info-tiles {
+ display: block;
+ }
+ }
+
+ &__icon {
+ flex-grow: 0.5;
+ height: auto;
+ margin-bottom: 1.2rem;
+ }
+ }
+}
+
.empty-page--liquidity-page {
display: grid;
gap: 1rem;
diff --git a/solidity/dashboard/src/css/typography.less b/solidity/dashboard/src/css/typography.less
index 7eb31b5fb5..2e820faf8a 100644
--- a/solidity/dashboard/src/css/typography.less
+++ b/solidity/dashboard/src/css/typography.less
@@ -215,6 +215,10 @@ label, .text-label {
text-align: right;
}
+ &-left {
+ text-align: left;
+ }
+
&-caption {
font-weight: 500;
font-size: 0.5rem;
diff --git a/solidity/dashboard/src/hooks/useSetMaxAmountToken.js b/solidity/dashboard/src/hooks/useSetMaxAmountToken.js
new file mode 100644
index 0000000000..665cfb8d90
--- /dev/null
+++ b/solidity/dashboard/src/hooks/useSetMaxAmountToken.js
@@ -0,0 +1,18 @@
+import BigNumber from "bignumber.js"
+import { useFormikContext } from "formik"
+import { toTokenUnit } from "../utils/token.utils"
+
+const useSetMaxAmountToken = (filedName, availableAmount) => {
+ const { setFieldValue } = useFormikContext()
+
+ const setMaxAvailableAmount = () => {
+ setFieldValue(
+ filedName,
+ toTokenUnit(availableAmount).toFixed(0, BigNumber.ROUND_DOWN)
+ )
+ }
+
+ return setMaxAvailableAmount
+}
+
+export default useSetMaxAmountToken
diff --git a/solidity/dashboard/src/pages/liquidity/LiquidityPage.jsx b/solidity/dashboard/src/pages/liquidity/LiquidityPage.jsx
index 61ff8ddbb5..73ed5ace36 100644
--- a/solidity/dashboard/src/pages/liquidity/LiquidityPage.jsx
+++ b/solidity/dashboard/src/pages/liquidity/LiquidityPage.jsx
@@ -17,14 +17,17 @@ import Banner from "../../components/Banner"
import { useHideComponent } from "../../hooks/useHideComponent"
import { gt } from "../../utils/arithmetics.utils"
+import KeepOnlyPool from "../../components/KeepOnlyPool"
+
const LiquidityPage = ({ headerTitle }) => {
const [isBannerVisible, hideBanner] = useHideComponent(false)
const { isConnected } = useWeb3Context()
const keepTokenBalance = useSelector((state) => state.keepTokenBalance)
- const { TBTC_SADDLE, KEEP_ETH, TBTC_ETH, KEEP_TBTC } = useSelector(
+ const { TBTC_SADDLE, KEEP_ETH, TBTC_ETH, KEEP_TBTC, KEEP_ONLY } = useSelector(
(state) => state.liquidityRewards
)
+
const dispatch = useDispatch()
const address = useWeb3Address()
@@ -66,10 +69,17 @@ const LiquidityPage = ({ headerTitle }) => {
const withdrawLiquidityRewards = (
liquidityPairContractName,
+ amount,
+ pool,
awaitingPromise
) => {
dispatch(
- withdrawAllLiquidityRewards(liquidityPairContractName, awaitingPromise)
+ withdrawAllLiquidityRewards(
+ liquidityPairContractName,
+ amount,
+ pool,
+ awaitingPromise
+ )
)
}
@@ -111,6 +121,15 @@ const LiquidityPage = ({ headerTitle }) => {
)}
+
{
@@ -55,11 +56,6 @@ const liquidityRewardsReducer = (state = initialState, action) => {
},
}
case `liquidity_rewards/${liquidityRewardPairName}_staked`: {
- const lpBalance = add(
- state[liquidityRewardPairName].lpBalance,
- restPayload.amount
- ).toString()
-
return {
...state,
[liquidityRewardPairName]: {
@@ -68,9 +64,9 @@ const liquidityRewardsReducer = (state = initialState, action) => {
state[liquidityRewardPairName].wrappedTokenBalance,
restPayload.amount
).toString(),
- lpBalance,
+ lpBalance: restPayload.lpBalance,
shareOfPoolInPercent: percentageOf(
- lpBalance,
+ restPayload.lpBalance,
restPayload.totalSupply
).toString(),
reward: restPayload.reward,
@@ -79,11 +75,6 @@ const liquidityRewardsReducer = (state = initialState, action) => {
}
}
case `liquidity_rewards/${liquidityRewardPairName}_withdrawn`: {
- const lpBalance = sub(
- state[liquidityRewardPairName].lpBalance,
- restPayload.amount
- ).toString()
-
return {
...state,
[liquidityRewardPairName]: {
@@ -92,9 +83,9 @@ const liquidityRewardsReducer = (state = initialState, action) => {
state[liquidityRewardPairName].wrappedTokenBalance,
restPayload.amount
).toString(),
- lpBalance,
+ lpBalance: restPayload.lpBalance,
shareOfPoolInPercent: percentageOf(
- lpBalance,
+ restPayload.lpBalance,
restPayload.totalSupply
).toString(),
reward: restPayload.reward,
diff --git a/solidity/dashboard/src/sagas/liquidity-rewards.js b/solidity/dashboard/src/sagas/liquidity-rewards.js
index bedbf0cb9a..5f0a4a7e7a 100644
--- a/solidity/dashboard/src/sagas/liquidity-rewards.js
+++ b/solidity/dashboard/src/sagas/liquidity-rewards.js
@@ -56,7 +56,8 @@ function* fetchLiquidityRewardsData(liquidityRewardPair, address) {
// Fetching available reward balance from `LPRewards` contract.
reward = yield call(
[LiquidityRewards, LiquidityRewards.rewardBalance],
- address
+ address,
+ lpBalance
)
// % of total pool in the `LPRewards` contract.
shareOfPoolInPercent = percentageOf(lpBalance, totalSupply).toString()
@@ -114,8 +115,8 @@ function* stakeTokens(action) {
yield call(sendTransaction, {
payload: {
contract: LiquidityRewards.LPRewardsContract,
- methodName: "stake",
- args: [amount],
+ methodName: LiquidityRewards.stakeFnName,
+ args: LiquidityRewards.stakeArgs(amount),
},
})
}
@@ -186,3 +187,26 @@ export function* watchFetchLiquidityRewardsAPY() {
fetchAllLiquidityRewardsAPY
)
}
+
+function* withdrawTokens(action) {
+ const { contractName, amount, pool } = action.payload
+
+ /** @type LiquidityRewards */
+ const LiquidityRewards = yield getLPRewardsWrapper({ contractName, pool })
+
+ yield call(sendTransaction, {
+ payload: {
+ contract: LiquidityRewards.LPRewardsContract,
+ methodName: LiquidityRewards.withdrawTokensFnName,
+ args: LiquidityRewards.withdrawTokensArgs(amount),
+ },
+ })
+}
+
+function* withdrawTokensWorker(action) {
+ yield call(submitButtonHelper, withdrawTokens, action)
+}
+
+export function* watchWithdrawTokens() {
+ yield takeEvery("liquidity_rewards/withdraw_tokens", withdrawTokensWorker)
+}
diff --git a/solidity/dashboard/src/sagas/subscriptions.js b/solidity/dashboard/src/sagas/subscriptions.js
index 4975093ad9..af70a77d8e 100644
--- a/solidity/dashboard/src/sagas/subscriptions.js
+++ b/solidity/dashboard/src/sagas/subscriptions.js
@@ -612,7 +612,7 @@ function* observeLiquidityTokenStakedEvent(liquidityRewardPair) {
const contractEventCahnnel = yield call(
createSubcribeToContractEventChannel,
LiquidityRewards.LPRewardsContract,
- "Staked"
+ LiquidityRewards.stakedEventName
)
while (true) {
@@ -640,7 +640,7 @@ function* observeLiquidityTokenWithdrawnEvent(liquidityRewardPair) {
const contractEventCahnnel = yield call(
createSubcribeToContractEventChannel,
LiquidityRewards.LPRewardsContract,
- "Withdrawn"
+ LiquidityRewards.depositWithdrawnEventName
)
while (true) {
@@ -681,9 +681,23 @@ function* lpTokensStakedOrWithdrawn(
totalSupply
)
+ const { lpBalance } = yield select(
+ (state) => state.liquidityRewards[liquidityRewardPairName]
+ )
+
+ let updatedlpBalance = lpBalance
+ let emittedAmountValue = 0
+ // Update only if this transacion relates to the current logged account.
+ if (isSameEthAddress(defaultAccount, user)) {
+ emittedAmountValue = amount
+ const arithmeticOpration = actionType.includes("withdrawn") ? sub : add
+ updatedlpBalance = arithmeticOpration(lpBalance, amount).toString()
+ }
+
const reward = yield call(
[LiquidityRewards, LiquidityRewards.rewardBalance],
- defaultAccount
+ defaultAccount,
+ updatedlpBalance
)
// If the `Withdrawn` or `Staked` event was emitted the total pool of the LPRewards,
@@ -691,8 +705,8 @@ function* lpTokensStakedOrWithdrawn(
yield put({
type: actionType,
payload: {
- // Update only if this transacion relates to the current logged account.
- amount: isSameEthAddress(defaultAccount, user) ? amount : 0,
+ amount: emittedAmountValue,
+ lpBalance: updatedlpBalance,
totalSupply,
reward,
apy,
@@ -712,16 +726,19 @@ function* observeLiquidityRewardPaidEvent(liquidityRewardPair) {
const contractEventCahnnel = yield call(
createSubcribeToContractEventChannel,
LiquidityRewards.LPRewardsContract,
- "RewardPaid"
+ LiquidityRewards.rewardClaimedEventName
)
while (true) {
try {
- const {
- returnValues: { user, reward },
- } = yield take(contractEventCahnnel)
-
- if (isSameEthAddress(defaultAccount, user)) {
+ const { returnValues } = yield take(contractEventCahnnel)
+ // LPRewards and TokenGeyser contract have different param names in an
+ // emitted event which is triggered when the reward is claimed but param
+ // which points to claimed reward amount is at the same index- 1. So we
+ // can get claimed amount by index eg. `event.returnValues["1"]`.
+ const reward = returnValues["1"]
+
+ if (isSameEthAddress(defaultAccount, returnValues.user)) {
yield put({
type: `liquidity_rewards/${liquidityRewardPair.name}_reward_paid`,
payload: {
diff --git a/solidity/dashboard/src/services/liquidity-rewards.js b/solidity/dashboard/src/services/liquidity-rewards.js
index 919709a9c8..35f143261e 100644
--- a/solidity/dashboard/src/services/liquidity-rewards.js
+++ b/solidity/dashboard/src/services/liquidity-rewards.js
@@ -1,7 +1,11 @@
import web3Utils from "web3-utils"
-import { createERC20Contract, createSaddleSwapContract } from "../contracts"
+import {
+ createERC20Contract,
+ createSaddleSwapContract,
+ CONTRACT_DEPLOY_BLOCK_NUMBER,
+} from "../contracts"
import BigNumber from "bignumber.js"
-import { toTokenUnit } from "../utils/token.utils"
+import { toTokenUnit, fromTokenUnit } from "../utils/token.utils"
import {
getPairData,
getKeepTokenPriceInUSD,
@@ -9,6 +13,7 @@ import {
} from "./uniswap-api"
import moment from "moment"
import { add } from "../utils/arithmetics.utils"
+import { isEmptyArray } from "../utils/array.utils"
/** @typedef {import("web3").default} Web3 */
/** @typedef {LiquidityRewards} LiquidityRewards */
@@ -17,6 +22,10 @@ const LPRewardsToWrappedTokenCache = {}
const WEEKS_IN_YEAR = 52
class LiquidityRewards {
+ static async _getWrappedTokenAddress(LPRewardsContract) {
+ return await LPRewardsContract.methods.wrappedToken().call()
+ }
+
constructor(_wrappedTokenContract, _LPRewardsContract, _web3) {
this.wrappedToken = _wrappedTokenContract
this.LPRewardsContract = _LPRewardsContract
@@ -31,6 +40,34 @@ class LiquidityRewards {
return this.LPRewardsContract.options.address
}
+ get rewardClaimedEventName() {
+ return "RewardPaid"
+ }
+
+ get depositWithdrawnEventName() {
+ return "Withdrawn"
+ }
+
+ get withdrawTokensFnName() {
+ return "exit"
+ }
+
+ withdrawTokensArgs() {
+ return []
+ }
+
+ get stakedEventName() {
+ return "Staked"
+ }
+
+ get stakeFnName() {
+ return "stake"
+ }
+
+ stakeArgs(amount) {
+ return [amount]
+ }
+
wrappedTokenBalance = async (address) => {
return await this.wrappedToken.methods.balanceOf(address).call()
}
@@ -188,20 +225,118 @@ class SaddleLPRewards extends LiquidityRewards {
}
}
+class TokenGeyserLPRewards extends LiquidityRewards {
+ static async _getWrappedTokenAddress(LPRewardsContract) {
+ return await LPRewardsContract.methods.token().call()
+ }
+
+ get rewardClaimedEventName() {
+ return "TokensClaimed"
+ }
+
+ get depositWithdrawnEventName() {
+ return "Unstaked"
+ }
+
+ get withdrawTokensFnName() {
+ return "unstake"
+ }
+
+ withdrawTokensArgs(amount) {
+ return [amount, []]
+ }
+
+ stakeArgs(amount) {
+ return [amount, []]
+ }
+
+ stakedBalance = async (address) => {
+ return await this.LPRewardsContract.methods.totalStakedFor(address).call()
+ }
+
+ totalSupply = async () => {
+ return await this.LPRewardsContract.methods.totalStaked().call()
+ }
+
+ rewardBalance = async (address, amount) => {
+ try {
+ // The `TokenGeyser.unstakeQuery` throws an error in case when eg. the
+ // amount param is greater than the real user's stake or when
+ // the user stakes KEEP in block `X` and call unstakeQuery in block `X`
+ // (`SafeMath: division by zero` error is thrown.). The web3 parses the
+ // error message in the wrong way when the `hanleRevert` option is enabled
+ // [1]. So here we clone the rewards contract instance and disable the
+ // `hanldeRevert` option.
+ // References: [1]:
+ // https://github.com/ChainSafe/web3.js/issues/3742
+ const clonedLPRewardsContract = this.LPRewardsContract.clone()
+ clonedLPRewardsContract.handleRevert = false
+ return await clonedLPRewardsContract.methods.unstakeQuery(amount).call()
+ } catch (error) {
+ return 0
+ }
+ }
+
+ calculateAPY = async (totalSupplyOfLPRewards) => {
+ totalSupplyOfLPRewards = toTokenUnit(totalSupplyOfLPRewards)
+
+ const rewardPoolPerWeek = await this.rewardPoolPerWeek()
+ const keepTokenInUSD = await getKeepTokenPriceInUSD()
+
+ const lpRewardsPoolInUSD = totalSupplyOfLPRewards.multipliedBy(
+ keepTokenInUSD
+ )
+
+ const r = this._calculateR(
+ keepTokenInUSD,
+ rewardPoolPerWeek,
+ lpRewardsPoolInUSD
+ )
+
+ return this._calculateAPY(r, WEEKS_IN_YEAR)
+ }
+
+ rewardPoolPerWeek = async () => {
+ const tokensLockedEvents = await this.LPRewardsContract.getPastEvents(
+ "TokensLocked",
+ {
+ fromBlock: CONTRACT_DEPLOY_BLOCK_NUMBER.keepTokenGeyserContract,
+ }
+ )
+
+ // The KEEP-only pool will earn 100k KEEP per month.
+ let rewardPoolPerMonth = fromTokenUnit(10e4)
+ const weeksInMonth = new BigNumber(
+ moment.duration(1, "months").asSeconds()
+ ).div(moment.duration(7, "days").asSeconds())
+
+ if (!isEmptyArray(tokensLockedEvents)) {
+ rewardPoolPerMonth = new BigNumber(
+ tokensLockedEvents.reverse()[0].returnValues.amount
+ )
+ }
+
+ return toTokenUnit(rewardPoolPerMonth.div(weeksInMonth))
+ }
+}
+
const LiquidityRewardsPoolStrategy = {
UNISWAP: UniswapLPRewards,
SADDLE: SaddleLPRewards,
+ TOKEN_GEYSER: TokenGeyserLPRewards,
}
export class LiquidityRewardsFactory {
/**
*
- * @param {('UNISWAP' | 'SADDLE')} pool - The supported type of pools.
+ * @param {('UNISWAP' | 'SADDLE' | 'TOKEN_GEYSER')} pool - The supported type of pools.
* @param {Object} LPRewardsContract - The LPRewardsContract as web3 contract instance.
* @param {Web3} web3 - web3
* @return {LiquidityRewards} - The Liquidity Rewards Wrapper
*/
static async initialize(pool, LPRewardsContract, web3) {
+ const PoolStrategy = LiquidityRewardsPoolStrategy[pool]
+
const lpRewardsContractAddress = web3Utils.toChecksumAddress(
LPRewardsContract.options.address
)
@@ -209,9 +344,9 @@ export class LiquidityRewardsFactory {
if (
!LPRewardsToWrappedTokenCache.hasOwnProperty(lpRewardsContractAddress)
) {
- const wrappedTokenAddress = await LPRewardsContract.methods
- .wrappedToken()
- .call()
+ const wrappedTokenAddress = await PoolStrategy._getWrappedTokenAddress(
+ LPRewardsContract
+ )
LPRewardsToWrappedTokenCache[
lpRewardsContractAddress
] = wrappedTokenAddress
@@ -222,8 +357,6 @@ export class LiquidityRewardsFactory {
LPRewardsToWrappedTokenCache[lpRewardsContractAddress]
)
- const PoolStrategy = LiquidityRewardsPoolStrategy[pool]
-
return new PoolStrategy(wrappedTokenContract, LPRewardsContract, web3)
}
}
diff --git a/solidity/dashboard/src/static/svg/keep-only-pool.svg b/solidity/dashboard/src/static/svg/keep-only-pool.svg
new file mode 100644
index 0000000000..e43a73cbee
--- /dev/null
+++ b/solidity/dashboard/src/static/svg/keep-only-pool.svg
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solidity/dashboard/src/utils/general.utils.js b/solidity/dashboard/src/utils/general.utils.js
index ac89038eed..db79b93dab 100644
--- a/solidity/dashboard/src/utils/general.utils.js
+++ b/solidity/dashboard/src/utils/general.utils.js
@@ -74,3 +74,22 @@ export const formatPercentage = (value) => {
return value.decimalPlaces(2, BigNumber.ROUND_DOWN).toNumber()
}
+
+export const displayPercentageValue = (
+ value,
+ isFormattedValue = true,
+ min = 0.01,
+ max = 999
+) => {
+ if (!isFormattedValue) {
+ value = formatPercentage(value)
+ }
+
+ let prefix = ""
+ if (value > 0 && value <= min) {
+ prefix = `<`
+ } else if (value >= max) {
+ prefix = `>`
+ }
+ return `${prefix}${value}%`
+}
diff --git a/solidity/dashboard/src/utils/token.utils.js b/solidity/dashboard/src/utils/token.utils.js
index 4f12928f0b..09b00300f4 100644
--- a/solidity/dashboard/src/utils/token.utils.js
+++ b/solidity/dashboard/src/utils/token.utils.js
@@ -84,3 +84,7 @@ export const getNumberWithMetricSuffix = (number) => {
export const displayAmountWithMetricSuffix = (amount) => {
return getNumberWithMetricSuffix(toTokenUnit(amount)).formattedValue
}
+
+export const displayNumberWithMetricSuffix = (number) => {
+ return getNumberWithMetricSuffix(number).formattedValue
+}
diff --git a/solidity/migrations/2_deploy_contracts.js b/solidity/migrations/2_deploy_contracts.js
index 051f52b146..de0464ee4a 100644
--- a/solidity/migrations/2_deploy_contracts.js
+++ b/solidity/migrations/2_deploy_contracts.js
@@ -45,6 +45,7 @@ const KeepRegistry = artifacts.require("./KeepRegistry.sol")
const GasPriceOracle = artifacts.require("./GasPriceOracle.sol")
const StakingPortBacker = artifacts.require("./StakingPortBacker.sol")
const BeaconRewards = artifacts.require("./BeaconRewards.sol")
+const KeepTokenGeyser = artifacts.require("./geyser/KeepTokenGeyser.sol")
let initializationPeriod = 43200 // ~12 hours
const dkgContributionMargin = 1 // 1%
@@ -161,4 +162,23 @@ module.exports = async function (deployer, network) {
KeepRandomBeaconOperator.address,
TokenStaking.address
)
+
+ // KEEP token geyser contract
+ const maxUnlockSchedules = 12
+ const startBonus = 30 // 30%
+ const bonusPeriodSec = 2592000 // 30 days in seconds
+ const initialSharesPerToken = 1
+ const durationSec = 2592000 // 30 days in seconds
+
+ await deployer.deploy(
+ KeepTokenGeyser,
+ // KEEP token is a distribution and staking token.
+ KeepToken.address,
+ KeepToken.address,
+ maxUnlockSchedules,
+ startBonus,
+ bonusPeriodSec,
+ initialSharesPerToken,
+ durationSec
+ )
}
diff --git a/solidity/scripts/keep-token-geyser-init.js b/solidity/scripts/keep-token-geyser-init.js
new file mode 100644
index 0000000000..aa428427cd
--- /dev/null
+++ b/solidity/scripts/keep-token-geyser-init.js
@@ -0,0 +1,58 @@
+const KeepTokenGeyser = artifacts.require("./geyser/KeepTokenGeyser.sol")
+const KeepToken = artifacts.require("./KeepToken.sol")
+const BatchedPhasedEscrow = artifacts.require("./BatchedPhasedEscrow")
+const KeepTokenGeyserRewardsEscrowBeneficiary = artifacts.require(
+ "./KeepTokenGeyserRewardsEscrowBeneficiary"
+)
+
+module.exports = async function () {
+ try {
+ const accounts = await web3.eth.getAccounts()
+ const keepToken = await KeepToken.deployed()
+ const tokenGeyser = await KeepTokenGeyser.deployed()
+ const rewardsAmount = web3.utils.toWei("100000", "ether")
+
+ const owner = accounts[0]
+
+ const initialEscrowBalance = web3.utils.toWei("500000", "ether") // 500k KEEP
+
+ const escrow = await BatchedPhasedEscrow.new(keepToken.address, {
+ from: owner,
+ })
+
+ // Configure escrow beneficiary.
+ const escrowBeneficiary = await KeepTokenGeyserRewardsEscrowBeneficiary.new(
+ keepToken.address,
+ tokenGeyser.address,
+ {
+ from: owner,
+ }
+ )
+
+ await escrowBeneficiary.transferOwnership(escrow.address, {
+ from: owner,
+ })
+
+ await escrow.approveBeneficiary(escrowBeneficiary.address, {
+ from: owner,
+ })
+
+ await tokenGeyser.setRewardDistribution(escrowBeneficiary.address, {
+ from: owner,
+ })
+
+ await keepToken.approveAndCall(escrow.address, initialEscrowBalance, [], {
+ from: owner,
+ })
+
+ // Initiate withdraw.
+ await escrow.batchedWithdraw([escrowBeneficiary.address], [rewardsAmount], {
+ from: owner,
+ })
+ } catch (err) {
+ console.error("unexpected error:", err)
+ process.exit(1)
+ }
+
+ process.exit()
+}