From 7884bb31e0c77a17c5cb79b9c0eda0fd854a27a3 Mon Sep 17 00:00:00 2001 From: Pedro Date: Mon, 20 Dec 2021 17:50:05 -0400 Subject: [PATCH] Add Pangolin Stake Connector and Tests Resolve errors Resolve review issues Fix comment Co-authored-by: 0xPradyuman <63545809+pradyuman-verma@users.noreply.github.com> Added NatSpec to Staking Connector Fix NatSpecs --- .../connectors/pangolin/exchange/main.sol | 2 +- .../connectors/pangolin/staking/events.sol | 67 ++ .../connectors/pangolin/staking/helpers.sol | 160 ++++ .../connectors/pangolin/staking/interface.sol | 49 ++ .../connectors/pangolin/staking/main.sol | 190 ++++ ...olin.test.ts => pangolin_exchange.test.ts} | 21 +- .../avalanche/pangolin/pangolin_stake.test.ts | 821 ++++++++++++++++++ 7 files changed, 1298 insertions(+), 12 deletions(-) create mode 100644 contracts/avalanche/connectors/pangolin/staking/events.sol create mode 100644 contracts/avalanche/connectors/pangolin/staking/helpers.sol create mode 100644 contracts/avalanche/connectors/pangolin/staking/interface.sol create mode 100644 contracts/avalanche/connectors/pangolin/staking/main.sol rename test/avalanche/pangolin/{pangolin.test.ts => pangolin_exchange.test.ts} (95%) create mode 100644 test/avalanche/pangolin/pangolin_stake.test.ts diff --git a/contracts/avalanche/connectors/pangolin/exchange/main.sol b/contracts/avalanche/connectors/pangolin/exchange/main.sol index 0f436906..c4e866d9 100644 --- a/contracts/avalanche/connectors/pangolin/exchange/main.sol +++ b/contracts/avalanche/connectors/pangolin/exchange/main.sol @@ -16,7 +16,7 @@ abstract contract PangolinResolver is Helpers, Events { * @param tokenA The address of token A.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) * @param tokenB The address of token B.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) * @param amtA The amount of A tokens to deposit. - * @param unitAmt The unit amount of of amtB/amtA with slippage. + * @param unitAmt The unit amount of amtB/amtA with slippage. * @param slippage Slippage amount. * @param getId ID to retrieve amtA. * @param setId ID stores the amount of pools tokens received. diff --git a/contracts/avalanche/connectors/pangolin/staking/events.sol b/contracts/avalanche/connectors/pangolin/staking/events.sol new file mode 100644 index 00000000..84c61559 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/staking/events.sol @@ -0,0 +1,67 @@ +pragma solidity ^0.7.0; + +contract Events { + event LogDepositLpStake( + address indexed lptoken, + uint256 indexed pid, + uint256 stakedAmount, + uint256 getId, + uint256 setId + ); + + event LogWithdrawLpStake( + address indexed lptoken, + uint256 indexed pid, + uint256 withdrawAmount, + uint256 getId, + uint256 setId + ); + + event LogWithdrawLpAndClaim( + address indexed lptoken, + uint256 indexed pid, + uint256 withdrawAmount, + uint256 rewardAmount, + uint256 getId, + uint256 setId + ); + + event LogClaimLpReward( + address indexed lptoken, + uint256 indexed pid, + uint256 rewardAmount + ); + + event LogEmergencyWithdrawLpStake( + address indexed lptoken, + uint256 indexed pid, + uint256 withdrawAmount + ); + + event LogDepositPNGStake( + address indexed stakingContract, + uint256 stakedAmount, + uint256 getId, + uint256 setId + ); + + event LogWithdrawPNGStake( + address indexed stakingContract, + uint256 withdrawAmount, + uint256 getId, + uint256 setId + ); + + event LogExitPNGStake( + address indexed stakingContract, + uint256 exitAmount, + uint256 rewardAmount, + address indexed rewardToken + ); + + event LogClaimPNGStakeReward( + address indexed stakingContract, + uint256 rewardAmount, + address indexed rewardToken + ); +} \ No newline at end of file diff --git a/contracts/avalanche/connectors/pangolin/staking/helpers.sol b/contracts/avalanche/connectors/pangolin/staking/helpers.sol new file mode 100644 index 00000000..34aa6e91 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/staking/helpers.sol @@ -0,0 +1,160 @@ +pragma solidity ^0.7.0; +pragma abicoder v2; + +import { TokenInterface } from "../../../common/interfaces.sol"; +import { DSMath } from "../../../common/math.sol"; +import { Basic } from "../../../common/basic.sol"; +import { IERC20, IMiniChefV2, IStakingRewards } from "./interface.sol"; + +abstract contract Helpers is DSMath, Basic { + + /** + * @dev Pangolin MiniChefV2 + */ + IMiniChefV2 internal constant minichefv2 = IMiniChefV2(0x1f806f7C8dED893fd3caE279191ad7Aa3798E928); + + /** + * @dev Pangolin Token + */ + IERC20 internal constant PNG = IERC20(0x60781C2586D68229fde47564546784ab3fACA982); + + // LP Staking, use minichefv2 to staking lp tokens and earn png + function _depositLPStake( + uint pid, + uint amount + ) internal returns (address lpTokenAddr) { + require(pid < minichefv2.poolLength(), "Invalid pid!"); + IERC20 lptoken = minichefv2.lpToken(pid); + + require(amount > 0, "Invalid amount, amount cannot be 0"); + require(lptoken.balanceOf(address(this)) > 0, "Invalid LP token balance"); + require(lptoken.balanceOf(address(this)) >= amount, "Invalid amount, amount greater than balance of LP token"); + + approve( + lptoken, + address(minichefv2), + amount + ); + + minichefv2.deposit(pid, amount, address(this)); + lpTokenAddr = address(lptoken); + } + + function _withdraw_LP_Stake( + uint pid, + uint amount + ) internal returns (address lpTokenAddr) { + require(pid < minichefv2.poolLength(), "Invalid pid!"); + + IMiniChefV2.UserInfo memory userinfo = minichefv2.userInfo(pid, address(this)); + + require(userinfo.amount >= amount, "Invalid amount, amount greater than balance of staking"); + require(amount > 0, "Invalid amount, amount cannot be 0"); + + minichefv2.withdraw(pid, amount, address(this)); + + IERC20 lptoken = minichefv2.lpToken(pid); + lpTokenAddr = address(lptoken); + } + + function _withdraw_and_getRewards_LP_Stake( + uint pid, + uint amount + ) internal returns (uint256 rewardAmount, address lpTokenAddr) { + require(pid < minichefv2.poolLength(), "Invalid pid!"); + + IMiniChefV2.UserInfo memory userinfo = minichefv2.userInfo(pid, address(this)); + + require(userinfo.amount >= amount, "Invalid amount, amount greater than balance of staking"); + require(amount > 0, "Invalid amount, amount cannot be 0"); + + rewardAmount = minichefv2.pendingReward(pid, address(this)); + + minichefv2.withdrawAndHarvest(pid, amount, address(this)); + + IERC20 lptoken = minichefv2.lpToken(pid); + lpTokenAddr = address(lptoken); + } + + function _getLPStakeReward( + uint pid + ) internal returns (uint256 rewardAmount, address lpTokenAddr) { + require(pid < minichefv2.poolLength(), "Invalid pid!"); + + rewardAmount = minichefv2.pendingReward(pid, address(this)); + + require(rewardAmount > 0, "No rewards to claim"); + + minichefv2.harvest(pid, address(this)); + + IERC20 lptoken = minichefv2.lpToken(pid); + lpTokenAddr = address(lptoken); + } + + function _emergencyWithdraw_LP_Stake( + uint pid + ) internal returns (uint256 lpAmount, address lpTokenAddr) { + require(pid < minichefv2.poolLength(), "Invalid pid!"); + + IMiniChefV2.UserInfo memory userinfo = minichefv2.userInfo(pid, address(this)); + lpAmount = userinfo.amount; + + minichefv2.emergencyWithdraw(pid, address(this)); + IERC20 lptoken = minichefv2.lpToken(pid); + lpTokenAddr = address(lptoken); + } + + // PNG Staking (Stake PNG, earn another token) + function _depositPNGStake( + address stakingContract_addr, + uint amount + ) internal { + IStakingRewards stakingContract = IStakingRewards(stakingContract_addr); + + require(amount > 0, "Invalid amount, amount cannot be 0"); + require(PNG.balanceOf(address(this)) > 0, "Invalid PNG balance"); + require(PNG.balanceOf(address(this)) >= amount, "Invalid amount, amount greater than balance of PNG"); + + approve(PNG, stakingContract_addr, amount); + + stakingContract.stake(amount); + } + + function _withdrawPNGStake( + address stakingContract_addr, + uint amount + ) internal { + IStakingRewards stakingContract = IStakingRewards(stakingContract_addr); + + require(stakingContract.balanceOf(address(this)) >= amount, "Invalid amount, amount greater than balance of staking"); + require(amount > 0, "Invalid amount, amount cannot be 0"); + + stakingContract.withdraw(amount); + } + + function _exitPNGStake( + address stakingContract_addr + ) internal returns (uint256 exitAmount, uint256 rewardAmount, address rewardToken){ + IStakingRewards stakingContract = IStakingRewards(stakingContract_addr); + + exitAmount = stakingContract.balanceOf(address(this)); + rewardAmount = stakingContract.rewards(address(this)); + + require(exitAmount > 0, "No balance to exit"); + + stakingContract.exit(); + } + + function _claimPNGStakeReward( + address stakingContract_addr + ) internal returns (uint256 rewardAmount, address rewardToken) { + IStakingRewards stakingContract = IStakingRewards(stakingContract_addr); + + rewardAmount = stakingContract.rewards(address(this)); + rewardToken = stakingContract.rewardsToken(); + + require(rewardAmount > 0, "No rewards to claim"); + + stakingContract.getReward(); + } +} diff --git a/contracts/avalanche/connectors/pangolin/staking/interface.sol b/contracts/avalanche/connectors/pangolin/staking/interface.sol new file mode 100644 index 00000000..8f1d8bb3 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/staking/interface.sol @@ -0,0 +1,49 @@ +pragma solidity >=0.6.2; +pragma abicoder v2; + +import { TokenInterface } from "../../../common/interfaces.sol"; + +interface IERC20 is TokenInterface{ + + // EIP 2612 + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external; +} + +interface IStakingRewards { + // Storage + function rewards(address account) view external returns (uint256); + + // View + function balanceOf(address account) external view returns (uint256); + function rewardsToken() external view returns (address); + + // Mutative + function exit() external; + function getReward() external; + function stake(uint256 amount) external; + function withdraw(uint256 amount) external; +} + +interface IMiniChefV2 { + struct UserInfo { + uint256 amount; + int256 rewardDebt; + } + + // Storage + function addedTokens(address token) external returns (bool); + function lpToken(uint256 _pid) external view returns (IERC20); + function userInfo(uint256 _pid, address _user) external view returns (UserInfo memory); + + // View + function pendingReward(uint256 _pid, address _user) external view returns (uint256); + function poolLength() external view returns (uint256); + + // Mutative + function deposit(uint256 pid, uint256 amount, address to) external; + function depositWithPermit(uint256 pid, uint256 amount, address to, uint deadline, uint8 v, bytes32 r, bytes32 s) external; + function withdraw(uint256 pid, uint256 amount, address to) external; + function harvest(uint256 pid, address to) external; + function withdrawAndHarvest(uint256 pid, uint256 amount, address to) external; + function emergencyWithdraw(uint256 pid, address to) external; +} \ No newline at end of file diff --git a/contracts/avalanche/connectors/pangolin/staking/main.sol b/contracts/avalanche/connectors/pangolin/staking/main.sol new file mode 100644 index 00000000..117d5909 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/staking/main.sol @@ -0,0 +1,190 @@ +pragma solidity ^0.7.0; + +/** + * @title Pangolin. + * @dev Decentralized Exchange. + */ + +import { TokenInterface } from "../../../common/interfaces.sol"; +import { Helpers } from "./helpers.sol"; +import { Events } from "./events.sol"; + +abstract contract PangolinStakeResolver is Helpers, Events { + + // LP Staking + /** + * @notice Deposit LP token in MiniChefV2 + * @dev Use the Pangolin Stake resolver to get the pid + * @param pid The index of the LP token in MiniChefV2. + * @param amount The amount of the LP token to deposit. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function depositLpStake( + uint pid, + uint amount, + uint256 getId, + uint256 setId + ) external returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amount); + + address lpTokenAddr = _depositLPStake(pid, _amt); + + setUint(setId, _amt); + _eventName = "LogDepositLpStake(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(lpTokenAddr, pid, _amt, getId, setId); + } + + /** + * @notice Withdraw LP token from MiniChefV2 + * @dev Use the Pangolin Stake resolver to get the pid + * @param pid The index of the LP token in MiniChefV2. + * @param amount The amount of the LP token to withdraw. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function withdrawLpStake( + uint pid, + uint amount, + uint256 getId, + uint256 setId + ) external returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amount); + + address lpTokenAddr = _withdraw_LP_Stake(pid, _amt); + + setUint(setId, _amt); + + _eventName = "LogWithdrawLpStake(address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(lpTokenAddr, pid, _amt, getId, setId); + } + + /** + * @notice Withdraw LP token staked and claim rewards from MiniChefV2 + * @dev Use the Pangolin Stake resolver to get the pid + * @param pid The index of the LP token in MiniChefV2. + * @param amount The amount of the LP token to withdraw. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function withdrawAndClaimLpRewards( + uint pid, + uint amount, + uint256 getId, + uint256 setId + ) external returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amount); + + (uint256 rewardAmount, address lpTokenAddr) = _withdraw_and_getRewards_LP_Stake(pid, _amt); + + setUint(setId, _amt); + + _eventName = "LogWithdrawLpAndClaim(address,uint256,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(lpTokenAddr, pid, _amt, rewardAmount, getId, setId); + } + + /** + * @notice Claim rewards from MiniChefV2 + * @dev Use the Pangolin Stake resolver to get the pid + * @param pid The index of the LP token in MiniChefV2. + */ + function claimLpRewards( + uint pid + ) external returns (string memory _eventName, bytes memory _eventParam) { + (uint256 rewardAmount, address lpTokenAddr) = _getLPStakeReward(pid); + + _eventName = "LogClaimLpReward(address,uint256,uint256)"; + _eventParam = abi.encode(lpTokenAddr, pid, rewardAmount); + } + + /** + * @notice Emergency withdraw all LP token staked from MiniChefV2 + * @dev Use the Pangolin Stake resolver to get the pid + * @param pid The index of the LP token in MiniChefV2. + */ + function emergencyWithdrawLpStake( + uint pid + ) external returns (string memory _eventName, bytes memory _eventParam) { + (uint amount, address lpTokenAddr) = _emergencyWithdraw_LP_Stake(pid); + + _eventName = "LogEmergencyWithdrawLpStake(address,uint256,uint256)"; + _eventParam = abi.encode(lpTokenAddr, pid, amount); + } + + // PNG Staking + /** + * @notice Deposit PNG in staking contract + * @param stakingContract The address of the single PNG staking contract + * @param amount The amount of the PNG to deposit. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function depositPNGStake( + address stakingContract, + uint256 amount, + uint256 getId, + uint256 setId + ) external returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amount); + + _depositPNGStake(stakingContract, _amt); + + setUint(setId, _amt); + + _eventName = "LogDepositPNGStake(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(stakingContract, _amt, getId, setId); + } + + /** + * @notice Withdraw PNG staked from staking contract + * @param stakingContract The address of the single PNG staking contract + * @param amount The amount of the PNG to withdraw. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function withdrawPNGStake( + address stakingContract, + uint256 amount, + uint256 getId, + uint256 setId + ) external returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amount); + + _withdrawPNGStake(stakingContract, _amt); + + setUint(setId, _amt); + + _eventName = "LogWithdrawPNGStake(address,uint256,uint256,uint256)"; + _eventParam = abi.encode(stakingContract, _amt, getId, setId); + } + + /** + * @notice Withdraw all PNG staked from staking contract + * @param stakingContract The address of the single PNG staking contract + */ + function exitPNGStake( + address stakingContract + ) external returns (string memory _eventName, bytes memory _eventParam) { + (uint256 exitAmount, uint256 rewardAmount, address rewardToken) = _exitPNGStake(stakingContract); + + _eventName = "LogExitPNGStake(address,uint256,uint256,address)"; + _eventParam = abi.encode(stakingContract, exitAmount, rewardAmount, rewardToken); + } + + /** + * @notice Claim rewards from staking contract + * @param stakingContract The address of the single PNG staking contract + */ + function claimPNGStakeReward( + address stakingContract + ) external returns (string memory _eventName, bytes memory _eventParam) { + (uint256 rewardAmount, address rewardToken) = _claimPNGStakeReward(stakingContract); + + _eventName = "LogClaimPNGStakeReward(address,uint256,address)"; + _eventParam = abi.encode(stakingContract, rewardAmount, rewardToken); + } +} + +contract ConnectV2PngStakeAvalanche is PangolinStakeResolver { + string public constant name = "Pangolin-Stake-v1"; +} diff --git a/test/avalanche/pangolin/pangolin.test.ts b/test/avalanche/pangolin/pangolin_exchange.test.ts similarity index 95% rename from test/avalanche/pangolin/pangolin.test.ts rename to test/avalanche/pangolin/pangolin_exchange.test.ts index 97f80eab..d4bd37f9 100644 --- a/test/avalanche/pangolin/pangolin.test.ts +++ b/test/avalanche/pangolin/pangolin_exchange.test.ts @@ -1,17 +1,16 @@ import { expect } from "chai"; import hre from "hardhat"; -const { web3, deployments, waffle, ethers } = hre; -const { provider, deployContract } = waffle; +const { waffle, ethers } = hre; +const { provider } = waffle; import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; import { encodeSpells } from "../../../scripts/tests/encodeSpells"; import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; -import { addLiquidity } from "../../../scripts/tests/addLiquidity"; import { addresses } from "../../../scripts/tests/avalanche/addresses"; import { abis } from "../../../scripts/constant/abis"; -import type { Signer, Contract } from "ethers"; +import { Signer, Contract } from "ethers"; import { ConnectV2PngAvalanche__factory } from "../../../typechain"; @@ -22,13 +21,13 @@ const PNG_AVAX_LP_ADDRESS = "0xd7538cABBf8605BdE1f4901B47B8D42c61DE0367"; describe("Pangolin DEX - Avalanche", function () { const pangolinConnectorName = "PANGOLIN-TEST-A" - let dsaWallet0: any; - let masterSigner: any; - let instaConnectorsV2: any; - let pangolinConnector: any; + let dsaWallet0: Contract; + let masterSigner: Signer; + let instaConnectorsV2: Contract; + let pangolinConnector: Contract; const wallets = provider.getWallets() - const [wallet0, wallet1, wallet2, wallet3] = wallets + const [wallet0, wallet1] = wallets before(async () => { await hre.network.provider.request({ method: "hardhat_reset", @@ -61,12 +60,12 @@ describe("Pangolin DEX - Avalanche", function () { it("Should have contracts deployed.", async function () { expect(!!instaConnectorsV2.address).to.be.true; expect(!!pangolinConnector.address).to.be.true; - expect(!!masterSigner.address).to.be.true; + expect(!!(await masterSigner.getAddress())).to.be.true; }); describe("DSA wallet setup", function () { it("Should build DSA v2", async function () { - dsaWallet0 = await buildDSAv2(wallet0.address) + dsaWallet0 = await buildDSAv2(wallet0.getAddress()) expect(!!dsaWallet0.address).to.be.true; }); diff --git a/test/avalanche/pangolin/pangolin_stake.test.ts b/test/avalanche/pangolin/pangolin_stake.test.ts new file mode 100644 index 00000000..7297b574 --- /dev/null +++ b/test/avalanche/pangolin/pangolin_stake.test.ts @@ -0,0 +1,821 @@ +import { expect } from "chai"; +import hre from "hardhat"; + +const { waffle, ethers } = hre; +const { provider } = waffle; + +import { deployAndEnableConnector } from "../../../scripts/tests/deployAndEnableConnector"; +import { buildDSAv2 } from "../../../scripts/tests/buildDSAv2"; +import { encodeSpells } from "../../../scripts/tests/encodeSpells"; +import { getMasterSigner } from "../../../scripts/tests/getMasterSigner"; +import { addresses } from "../../../scripts/tests/avalanche/addresses"; +import { abis } from "../../../scripts/constant/abis"; +import { Signer, Contract, BigNumber } from "ethers"; + +import { ConnectV2PngAvalanche__factory, ConnectV2PngStakeAvalanche__factory } from "../../../typechain"; + +const PNG_ADDRESS = "0x60781C2586D68229fde47564546784ab3fACA982"; +const WAVAX_ADDRESS = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"; +const PNG_AVAX_LP_ADDRESS = "0xd7538cABBf8605BdE1f4901B47B8D42c61DE0367"; +const PNG_STAKING_ADDRESS = "0x88afdaE1a9F58Da3E68584421937E5F564A0135b"; + +describe("Pangolin Stake - Avalanche", function () { + const pangolinConnectorName = "PANGOLIN-TEST-A" + const pangolinStakeConnectorName = "PANGOLIN-STAKE-TEST-A" + + let dsaWallet0: Contract; + let masterSigner: Signer; + let instaConnectorsV2: Contract; + let pangolinConnector: Contract; + let pangolinStakeConnector: Contract; + + let PNG: Contract; + + const wallets = provider.getWallets() + const [wallet0, wallet1] = wallets + before(async () => { + await hre.network.provider.request({ + method: "hardhat_reset", + params: [ + { + forking: { + jsonRpcUrl: `https://api.avax.network/ext/bc/C/rpc`, + blockNumber: 8197390 + }, + }, + ], + }); + + PNG = await ethers.getContractAt( + abis.basic.erc20, + PNG_ADDRESS + ); + + masterSigner = await getMasterSigner(); + instaConnectorsV2 = await ethers.getContractAt( + abis.core.connectorsV2, + addresses.core.connectorsV2 + ); + + // Deploy and enable Pangolin Connector + pangolinConnector = await deployAndEnableConnector({ + connectorName: pangolinConnectorName, + contractArtifact: ConnectV2PngAvalanche__factory, + signer: masterSigner, + connectors: instaConnectorsV2 + }); + console.log("Pangolin Connector address: "+ pangolinConnector.address); + + // Deploy and enable Pangolin Stake Connector + pangolinStakeConnector = await deployAndEnableConnector({ + connectorName: pangolinStakeConnectorName, + contractArtifact: ConnectV2PngStakeAvalanche__factory, + signer: masterSigner, + connectors: instaConnectorsV2 + }); + console.log("Pangolin Stake Connector address: "+ pangolinStakeConnector.address); + }) + + it("Should have contracts deployed.", async function () { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!pangolinConnector.address).to.be.true; + expect(!!pangolinStakeConnector.address).to.be.true; + expect(!!(await masterSigner.getAddress())).to.be.true; + }); + + describe("DSA wallet setup", function () { + it("Should build DSA v2", async function () { + dsaWallet0 = await buildDSAv2(wallet0.getAddress()) + expect(!!dsaWallet0.address).to.be.true; + }); + + it("Deposit 10 AVAX into DSA wallet", async function () { + await wallet0.sendTransaction({ + to: dsaWallet0.address, + value: ethers.utils.parseEther("10") + }); + expect(await ethers.provider.getBalance(dsaWallet0.address)).to.be.gte(ethers.utils.parseEther("10")); + }); + }); + + describe("Pangolin Staking - LP Stake Test", function () { + let lpAmount: BigNumber; + let pangolinLPToken: Contract; + // Buy 100 PNG and deposity in PNG/AVAX LP + before(async () => { + const amount = ethers.utils.parseEther("100"); // 100 PNG + const int_slippage = 0.03 + const slippage = ethers.utils.parseEther(int_slippage.toString()); + const setId = "0"; + + const PangolinRouterABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)" + ]; + + // Get amount of AVAX for 200 PNG from Pangolin + const PangolinRouter = await ethers.getContractAt( + PangolinRouterABI, + "0xE54Ca86531e17Ef3616d22Ca28b0D458b6C89106" + ); + const amounts = await PangolinRouter.getAmountsOut( + amount, + [ + PNG_ADDRESS, + WAVAX_ADDRESS + ] + ); + + const amtA = amounts[0]; + const amtB = amounts[1]; + const unitAmt = (amtB * (1 + int_slippage)) / amtA; + const unitAmount = ethers.utils.parseEther(unitAmt.toString()); + + const spells = [ + { + connector: pangolinConnectorName, + method: "buy", + args: [ + PNG_ADDRESS, + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + amount, + unitAmount, + 0, + 0 + ] + }, + { + connector: pangolinConnectorName, + method: "deposit", + args: [ + PNG_ADDRESS, + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + amount, + unitAmount, + slippage, + 0, + setId + ] + }, + ]; + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), wallet1.address + ); + const receipt = await tx.wait(); + pangolinLPToken = await ethers.getContractAt( + abis.basic.erc20, + PNG_AVAX_LP_ADDRESS + ); + }); + + it("Check if has PNG/AVAX LP", async function () { + const pangolinPoolAVAXBalance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(pangolinPoolAVAXBalance, `Pangolin PNG/AVAX LP greater than 0`).to.be.gt(0); + console.log("PNG/AVAX LP: ", ethers.utils.formatUnits(pangolinPoolAVAXBalance, "ether").toString()) + lpAmount = pangolinPoolAVAXBalance; + }); + + it("Check if all functions reverts by: Invalid pid!", async function () { + const pid = BigNumber.from("999999999999"); + const amount = ethers.utils.parseEther("1"); + const getId = 0; + const setId = 0; + + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + pid, + amount, + getId, + setId + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid pid!"); + + spells[0].method = "withdrawLpStake" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid pid!"); + + spells[0].method = "withdrawAndClaimLpRewards" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid pid!"); + + spells = [ + { + connector: pangolinStakeConnectorName, + method: "claimLpRewards", + args: [ + pid + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid pid!"); + + spells[0].method = "emergencyWithdrawLpStake" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid pid!"); + }); + + it("Check if all functions reverts by: 'Invalid amount, amount cannot be 0'", async function () { + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + 0, + 0, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + + spells[0].method = "withdrawLpStake" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + + spells[0].method = "withdrawLpStake" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + + spells[0].method = "withdrawAndClaimLpRewards" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + }); + + describe("depositLpStake function", function () { + it("Check if depositLpStake function reverts by: Invalid amount, amount greater than balance of LP token", async function () { + const amount = lpAmount.mul(2); + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + 0, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount greater than balance of LP token"); + }); + + it("Check if success in depositLpStake", async function () { + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + 0, + lpAmount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + // Check if PNG/AVAX LP is equal 0 + const balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(0); + }); + + it("Check if depositLpStake function reverts by: Invalid LP token balance", async function () { + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + 0, + lpAmount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid LP token balance"); + }); + }); + + describe("claimLpRewards function", function () { + it("Check if success in claimLpRewards", async function () { + // Increase Time in 20 seconds + await hre.network.provider.send("evm_increaseTime", [20]); + // Mine new block + await hre.network.provider.send("evm_mine"); + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "claimLpRewards", + args: [0] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + // Checks if the wallet has more than 100 PNG + const balance = await PNG.balanceOf(dsaWallet0.address); + expect(balance).to.be.gt(0); + }); + }); + + describe("withdrawLpStake function", function () { + it("Check if withdrawLpStake function reverts by: Invalid amount, amount greater than balance of staking", async function () { + const amount = lpAmount.mul(2); + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawLpStake", + args: [ + 0, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount greater than balance of staking"); + }); + + it("Check if success in withdrawLpStake", async function () { + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawLpStake", + args: [ + 0, + lpAmount.div(2), + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + // Check if PNG/AVAX LP is equal 0 + const balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(lpAmount.div(2)); + }); + }); + + describe("withdrawAndClaimLpRewards function", function () { + it("Check if withdrawAndClaimLpRewards function reverts by: Invalid amount, amount greater than balance of staking", async function () { + const amount = lpAmount.mul(2); + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawAndClaimLpRewards", + args: [ + 0, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount greater than balance of staking"); + }); + + it("Check if success in withdrawAndClaimLpRewards", async function () { + let balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + const png_balance = await PNG.balanceOf(dsaWallet0.address); + const amount = lpAmount.sub(balance) + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawAndClaimLpRewards", + args: [ + 0, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + // Check if PNG/AVAX LP is equal 0 + balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(lpAmount); + const new_png_balance = await PNG.balanceOf(dsaWallet0.address); + expect(new_png_balance).to.be.gt(png_balance); + }); + }); + + describe("emergencyWithdrawLpStake function", function () { + // Deposit LP again + before(async () => { + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositLpStake", + args: [ + 0, + lpAmount, + 0, + 0 + ] + } + ]; + await dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + }); + + it("Check if success in emergencyWithdrawLpStake", async function () { + let balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + const amount = lpAmount.sub(balance) + const spells = [ + { + connector: pangolinStakeConnectorName, + method: "emergencyWithdrawLpStake", + args: [0] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + // Check if PNG/AVAX LP is equal 0 + balance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(lpAmount); + }); + }); + }); + + describe("Pangolin Staking - Single Stake Test (PNG)", function () { + let pngToken: Contract; + let stakingContract: Contract; + let stakingBalance: BigNumber; + before(async () => { + const amount = ethers.utils.parseEther("100"); // 100 PNG + const int_slippage = 0.03 + + const PangolinRouterABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)" + ]; + + // Get amount of AVAX for 200 PNG from Pangolin + const PangolinRouter = await ethers.getContractAt( + PangolinRouterABI, + "0xE54Ca86531e17Ef3616d22Ca28b0D458b6C89106" + ); + const amounts = await PangolinRouter.getAmountsOut( + amount, + [ + PNG_ADDRESS, + WAVAX_ADDRESS + ] + ); + + const amtA = amounts[0]; + const amtB = amounts[1]; + const unitAmt = (amtB * (1 + int_slippage)) / amtA; + const unitAmount = ethers.utils.parseEther(unitAmt.toString()); + + const spells = [ + { + connector: pangolinConnectorName, + method: "buy", + args: [ + PNG_ADDRESS, + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + amount, + unitAmount, + 0, + 0 + ] + } + ]; + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), wallet1.address + ); + const receipt = await tx.wait(); + + pngToken = await ethers.getContractAt(abis.basic.erc20, PNG_ADDRESS); + stakingContract = await ethers.getContractAt(abis.basic.erc20, PNG_STAKING_ADDRESS); + }); + + it("Check if has 100 PNG", async function () { + const amount = ethers.utils.parseEther("100"); + const pngBalance = await pngToken.balanceOf(dsaWallet0.address); + expect(pngBalance, `PNG Token is equal 100`).to.be.gt(amount.toString()); + }); + + it("Check if some functions reverts by: Invalid amount, amount cannot be 0", async function () { + const amount = 0; + const getId = 0; + const setId = 0; + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + getId, + setId + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + + spells[0].method = "withdrawPNGStake" + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount cannot be 0"); + }); + + describe("depositPNGStake function", function () { + it("Check if reverts by: Invalid amount, amount greater than balance of PNG", async function () { + const amount = ethers.utils.parseEther("200") + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount greater than balance of PNG"); + }); + + it("Check if success in depositPNGStake", async function () { + const amount = await pngToken.balanceOf(dsaWallet0.address); + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + const new_png_balance = await pngToken.balanceOf(dsaWallet0.address); + expect(new_png_balance).to.be.eq(0); + const staking_balance = await stakingContract.balanceOf(dsaWallet0.address); + expect(staking_balance).to.be.gt(0); + stakingBalance = staking_balance + }); + + it("Check if reverts by: Invalid PNG balance", async function () { + const amount = ethers.utils.parseEther("100") + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "depositPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid PNG balance"); + }); + }); + + describe("withdrawPNGStake function", function () { + it("Check if reverts by: Invalid amount, amount greater than balance of staking", async function () { + const amount = ethers.utils.parseEther("200") + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("Invalid amount, amount greater than balance of staking"); + }); + + it("Check if success in withdrawPNGStake", async function () { + const amount = ethers.utils.parseEther("50"); + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "withdrawPNGStake", + args: [ + PNG_STAKING_ADDRESS, + amount, + 0, + 0 + ] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + + const balance = await pngToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(amount); + }); + }); + + describe("claimPNGStakeReward function", function () { + it("Check if success in claimPNGStakeReward", async function () { + // Increase Time in 20 seconds + await hre.network.provider.send("evm_increaseTime", [20]); + // Mine new block + await hre.network.provider.send("evm_mine"); + const amount = ethers.utils.parseEther("50"); + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "claimPNGStakeReward", + args: [PNG_STAKING_ADDRESS] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + + const balance = await pngToken.balanceOf(dsaWallet0.address); + expect(balance).to.be.gt(amount); + }); + + it("Check if reverts by: No rewards to claim", async function () { + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "claimPNGStakeReward", + args: [PNG_STAKING_ADDRESS] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("No rewards to claim"); + }); + }); + + describe("exitPNGStake function", function () { + it("Check if success in exitPNGStake", async function () { + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "exitPNGStake", + args: [PNG_STAKING_ADDRESS] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.not.reverted; + + const balance = await stakingContract.balanceOf(dsaWallet0.address); + expect(balance).to.be.eq(0); + }); + + it("Check if reverts by: No balance to exit", async function () { + let spells = [ + { + connector: pangolinStakeConnectorName, + method: "exitPNGStake", + args: [PNG_STAKING_ADDRESS] + } + ]; + await expect( + dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ) + ).to.be.revertedWith("No balance to exit"); + }); + }); + }); +});