import { SignatureLike } from '@ethersproject/bytes'; import { BigNumber, constants, providers, Signature, utils } from 'ethers'; import BaseService from '../commons/BaseService'; import { eEthereumTxType, EthereumTransactionTypeExtended, tEthereumAddress, transactionType, } from '../commons/types'; import { DEFAULT_APPROVE_AMOUNT, valueToWei } from '../commons/utils'; import { SignStakingValidator, StakingValidator, } from '../commons/validators/methodValidators'; import { is0OrPositiveAmount, isEthAddress, isPositiveAmount, isPositiveOrMinusOneAmount, } from '../commons/validators/paramValidators'; import { ERC20Service, IERC20ServiceInterface } from '../erc20-contract'; import { StakedToken } from './typechain/StakedToken'; import { StakedTokenFactory } from './typechain/StakedTokenFactory'; export interface AssetData { emissionPerSecond: BigNumber; lastUpdateTimestamp: BigNumber; index: BigNumber; '0': BigNumber; '1': BigNumber; '2': BigNumber; } export interface StakingInterface { stakingContractAddress: tEthereumAddress; exchangeRate: () => Promise; UNSTAKE_WINDOW: () => Promise; COOLDOWN_SECONDS: () => Promise; stakersCooldowns: (user: tEthereumAddress) => Promise; balanceOf: (user: tEthereumAddress) => Promise; getTotalRewardsBalance: (user: tEthereumAddress) => Promise; getStakerRedeemWindow: ( user: tEthereumAddress, ) => Promise<[BigNumber, BigNumber, BigNumber]>; assets: (asset: tEthereumAddress) => Promise; stake: ( user: tEthereumAddress, amount: string, onBehalfOf?: tEthereumAddress, ) => Promise; redeem: ( user: tEthereumAddress, amount: string, ) => Promise; cooldown: (user: tEthereumAddress) => EthereumTransactionTypeExtended[]; claimRewards: ( user: tEthereumAddress, amount: string, ) => Promise; claimRewardsAndRedeem: ( user: tEthereumAddress, claimAmount: string, redeemAmount: string, ) => Promise; signStaking: ( user: tEthereumAddress, amount: string, nonce: string, ) => Promise; stakeWithPermit: ( user: tEthereumAddress, amount: string, signature: string, ) => Promise; } type StakingServiceConfig = { TOKEN_STAKING_ADDRESS: tEthereumAddress; }; export class StakingService extends BaseService implements StakingInterface { public readonly stakingContractAddress: tEthereumAddress; public readonly stakingContractInstance: StakedToken; readonly erc20Service: IERC20ServiceInterface; constructor( provider: providers.Provider, stakingServiceConfig: StakingServiceConfig, ) { super(provider, StakedTokenFactory); this.erc20Service = new ERC20Service(provider); this.stakingContractAddress = stakingServiceConfig.TOKEN_STAKING_ADDRESS; this.stakingContractInstance = this.getContractInstance( this.stakingContractAddress, ); } @SignStakingValidator public async exchangeRate(): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); return stakingContract.exchangeRate(); } @SignStakingValidator public async assets( @isEthAddress() asset: tEthereumAddress, ): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); return stakingContract.assets(asset); } @SignStakingValidator public async UNSTAKE_WINDOW(): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); // eslint-disable-next-line new-cap return stakingContract.UNSTAKE_WINDOW(); } @SignStakingValidator public async COOLDOWN_SECONDS(): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); // eslint-disable-next-line new-cap return stakingContract.COOLDOWN_SECONDS(); } @SignStakingValidator public async stakersCooldowns( @isEthAddress() user: tEthereumAddress, ): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); return stakingContract.stakersCooldowns(user); } @SignStakingValidator public async getTotalRewardsBalance( @isEthAddress() user: tEthereumAddress, ): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); return stakingContract.getTotalRewardsBalance(user); } @SignStakingValidator public async balanceOf( @isEthAddress() user: tEthereumAddress, ): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); return stakingContract.balanceOf(user); } @SignStakingValidator public async getStakerRedeemWindow( user: tEthereumAddress, ): Promise<[BigNumber, BigNumber, BigNumber]> { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); const stakerCoolDown = await stakingContract.stakersCooldowns(user); // eslint-disable-next-line new-cap const COOLDOWN_SECONDS = await stakingContract.COOLDOWN_SECONDS(); // eslint-disable-next-line new-cap const UNSTAKE_WINDOW = await stakingContract.UNSTAKE_WINDOW(); return Promise.resolve([ stakerCoolDown, stakerCoolDown.add(COOLDOWN_SECONDS), stakerCoolDown.add(COOLDOWN_SECONDS).add(UNSTAKE_WINDOW), ]); } @SignStakingValidator public async signStaking( @isEthAddress() user: tEthereumAddress, @isPositiveAmount() amount: string, @is0OrPositiveAmount() nonce: string, ): Promise { const { getTokenData } = this.erc20Service; const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); // eslint-disable-next-line new-cap const stakedToken: string = await stakingContract.STAKED_TOKEN(); const { name, decimals } = await getTokenData(stakedToken); const convertedAmount: string = valueToWei(amount, decimals); const { chainId } = await this.provider.getNetwork(); const typeData = { types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, ], Permit: [ { name: 'owner', type: 'address' }, { name: 'spender', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'nonce', type: 'uint256' }, { name: 'deadline', type: 'uint256' }, ], }, primaryType: 'Permit', domain: { name, version: '1', chainId, verifyingContract: stakedToken, }, message: { owner: user, spender: this.stakingContractAddress, value: convertedAmount, nonce, deadline: constants.MaxUint256.toString(), }, }; return JSON.stringify(typeData); } @SignStakingValidator public async stakeWithPermit( @isEthAddress() user: tEthereumAddress, @isPositiveAmount() amount: string, signature: SignatureLike, ): Promise { const txs: EthereumTransactionTypeExtended[] = []; const { decimalsOf } = this.erc20Service; const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); // eslint-disable-next-line new-cap const stakedToken: string = await stakingContract.STAKED_TOKEN(); const stakedTokenDecimals: number = await decimalsOf(stakedToken); const convertedAmount: string = valueToWei(amount, stakedTokenDecimals); const sig: Signature = utils.splitSignature(signature); const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.stakeWithPermit( user, user, convertedAmount, constants.MaxUint256.toString(), sig.v, sig.r, sig.s, ), from: user, }); txs.push({ tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation(txs, txCallback), }); return txs; } @StakingValidator public async stake( @isEthAddress() user: tEthereumAddress, @isPositiveAmount() amount: string, @isEthAddress() onBehalfOf?: tEthereumAddress, ): Promise { const txs: EthereumTransactionTypeExtended[] = []; const { decimalsOf, isApproved, approve } = this.erc20Service; const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); // eslint-disable-next-line new-cap const stakedToken: string = await stakingContract.STAKED_TOKEN(); const stakedTokenDecimals: number = await decimalsOf(stakedToken); const convertedAmount: string = valueToWei(amount, stakedTokenDecimals); const approved: boolean = await isApproved({ token: stakedToken, user, spender: this.stakingContractAddress, amount, }); if (!approved) { const approveTx = approve({ user, token: stakedToken, spender: this.stakingContractAddress, amount: DEFAULT_APPROVE_AMOUNT, }); txs.push(approveTx); } const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.stake( onBehalfOf ?? user, convertedAmount, ), from: user, }); txs.push({ tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation(txs, txCallback), }); return txs; } @StakingValidator public async redeem( @isEthAddress() user: tEthereumAddress, @isPositiveOrMinusOneAmount() amount: string, ): Promise { let convertedAmount: string; const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); if (amount === '-1') { convertedAmount = constants.MaxUint256.toString(); } else { const { decimalsOf } = this.erc20Service; // eslint-disable-next-line new-cap const stakedToken: string = await stakingContract.STAKED_TOKEN(); const stakedTokenDecimals: number = await decimalsOf(stakedToken); convertedAmount = valueToWei(amount, stakedTokenDecimals); } const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.redeem(user, convertedAmount), from: user, gasSurplus: 20, }); return [ { tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation([], txCallback), }, ]; } @StakingValidator public cooldown( @isEthAddress() user: tEthereumAddress, ): EthereumTransactionTypeExtended[] { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.cooldown(), from: user, }); return [ { tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation([], txCallback), }, ]; } @StakingValidator public async claimRewards( @isEthAddress() user: tEthereumAddress, @isPositiveOrMinusOneAmount() amount: string, ): Promise { let convertedAmount: string; const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); if (amount === '-1') { convertedAmount = constants.MaxUint256.toString(); } else { const { decimalsOf } = this.erc20Service; // eslint-disable-next-line new-cap const stakedToken: string = await stakingContract.REWARD_TOKEN(); const stakedTokenDecimals: number = await decimalsOf(stakedToken); convertedAmount = valueToWei(amount, stakedTokenDecimals); } const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.claimRewards(user, convertedAmount), from: user, gasSurplus: 20, }); return [ { tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation([], txCallback), }, ]; } @StakingValidator public async claimRewardsAndRedeem( @isEthAddress() user: tEthereumAddress, @isPositiveOrMinusOneAmount() claimAmount: string, @isPositiveOrMinusOneAmount() redeemAmount: string, ): Promise { const stakingContract: StakedToken = this.getContractInstance( this.stakingContractAddress, ); const txCallback: () => Promise = this.generateTxCallback({ rawTxMethod: async () => stakingContract.populateTransaction.claimRewardsAndRedeem( user, claimAmount, redeemAmount, ), from: user, gasSurplus: 20, }); return [ { tx: txCallback, txType: eEthereumTxType.STAKE_ACTION, gas: this.generateTxPriceEstimation([], txCallback), }, ]; } }