diff --git a/contracts/avalanche/connectors/pangolin/exchange/events.sol b/contracts/avalanche/connectors/pangolin/exchange/events.sol new file mode 100644 index 00000000..2717b0a3 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/exchange/events.sol @@ -0,0 +1,41 @@ +pragma solidity ^0.7.0; + +contract Events { + event LogDepositLiquidity( + address indexed tokenA, + address indexed tokenB, + uint256 amtA, + uint256 amtB, + uint256 uniAmount, + uint256 getId, + uint256 setId + ); + + event LogWithdrawLiquidity( + address indexed tokenA, + address indexed tokenB, + uint256 amountA, + uint256 amountB, + uint256 uniAmount, + uint256 getId, + uint256[] setId + ); + + event LogBuy( + address indexed buyToken, + address indexed sellToken, + uint256 buyAmt, + uint256 sellAmt, + uint256 getId, + uint256 setId + ); + + event LogSell( + address indexed buyToken, + address indexed sellToken, + uint256 buyAmt, + uint256 sellAmt, + uint256 getId, + uint256 setId + ); +} \ No newline at end of file diff --git a/contracts/avalanche/connectors/pangolin/exchange/helpers.sol b/contracts/avalanche/connectors/pangolin/exchange/helpers.sol new file mode 100644 index 00000000..3e56046f --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/exchange/helpers.sol @@ -0,0 +1,146 @@ +pragma solidity ^0.7.0; + +import { TokenInterface } from "../../../common/interfaces.sol"; +import { DSMath } from "../../../common/math.sol"; +import { Basic } from "../../../common/basic.sol"; +import { IPangolinRouter, IPangolinFactory } from "./interface.sol"; + +abstract contract Helpers is DSMath, Basic { + + /** + * @dev Pangolin Router + */ + IPangolinRouter internal constant router = IPangolinRouter(0xE54Ca86531e17Ef3616d22Ca28b0D458b6C89106); + + function getExpectedBuyAmt( + address[] memory paths, + uint sellAmt + ) internal view returns(uint buyAmt) { + uint[] memory amts = router.getAmountsOut( + sellAmt, + paths + ); + buyAmt = amts[1]; + } + + function getExpectedSellAmt( + address[] memory paths, + uint buyAmt + ) internal view returns(uint sellAmt) { + uint[] memory amts = router.getAmountsIn( + buyAmt, + paths + ); + sellAmt = amts[0]; + } + + function checkPair( + address[] memory paths + ) internal view { + address pair = IPangolinFactory(router.factory()).getPair(paths[0], paths[1]); + require(pair != address(0), "No-exchange-address"); + } + + function getPaths( + address buyAddr, + address sellAddr + ) internal pure returns(address[] memory paths) { + paths = new address[](2); + paths[0] = address(sellAddr); + paths[1] = address(buyAddr); + } + + function getMinAmount( + TokenInterface token, + uint amt, + uint slippage + ) internal view returns(uint minAmt) { + uint _amt18 = convertTo18(token.decimals(), amt); + minAmt = wmul(_amt18, sub(WAD, slippage)); + minAmt = convert18ToDec(token.decimals(), minAmt); + } + + function _addLiquidity( + address tokenA, + address tokenB, + uint _amt, + uint unitAmt, + uint slippage + ) internal returns (uint _amtA, uint _amtB, uint _liquidity) { + (TokenInterface _tokenA, TokenInterface _tokenB) = changeAvaxAddress(tokenA, tokenB); + + _amtA = _amt == uint(-1) ? getTokenBal(TokenInterface(tokenA)) : _amt; + _amtB = convert18ToDec(_tokenB.decimals(), wmul(unitAmt, convertTo18(_tokenA.decimals(), _amtA))); + + bool isAvax = address(_tokenA) == wavaxAddr; + convertAvaxToWavax(isAvax, _tokenA, _amtA); + + isAvax = address(_tokenB) == wavaxAddr; + convertAvaxToWavax(isAvax, _tokenB, _amtB); + + approve(_tokenA, address(router), _amtA); + approve(_tokenB, address(router), _amtB); + + uint minAmtA = getMinAmount(_tokenA, _amtA, slippage); + uint minAmtB = getMinAmount(_tokenB, _amtB, slippage); + (_amtA, _amtB, _liquidity) = router.addLiquidity( + address(_tokenA), + address(_tokenB), + _amtA, + _amtB, + minAmtA, + minAmtB, + address(this), + block.timestamp + 1 + ); + } + + function _removeLiquidity( + address tokenA, + address tokenB, + uint _amt, + uint unitAmtA, + uint unitAmtB + ) internal returns (uint _amtA, uint _amtB, uint _uniAmt) { + TokenInterface _tokenA; + TokenInterface _tokenB; + (_tokenA, _tokenB, _uniAmt) = _getRemoveLiquidityData( + tokenA, + tokenB, + _amt + ); + { + uint minAmtA = convert18ToDec(_tokenA.decimals(), wmul(unitAmtA, _uniAmt)); + uint minAmtB = convert18ToDec(_tokenB.decimals(), wmul(unitAmtB, _uniAmt)); + (_amtA, _amtB) = router.removeLiquidity( + address(_tokenA), + address(_tokenB), + _uniAmt, + minAmtA, + minAmtB, + address(this), + block.timestamp + 1 + ); + } + + bool isAvax = address(_tokenA) == wavaxAddr; + convertWavaxToAvax(isAvax, _tokenA, _amtA); + + isAvax = address(_tokenB) == wavaxAddr; + convertWavaxToAvax(isAvax, _tokenB, _amtB); + } + + function _getRemoveLiquidityData( + address tokenA, + address tokenB, + uint _amt + ) internal returns (TokenInterface _tokenA, TokenInterface _tokenB, uint _uniAmt) { + (_tokenA, _tokenB) = changeAvaxAddress(tokenA, tokenB); + address exchangeAddr = IPangolinFactory(router.factory()).getPair(address(_tokenA), address(_tokenB)); + require(exchangeAddr != address(0), "pair-not-found."); + + TokenInterface pngToken = TokenInterface(exchangeAddr); + _uniAmt = _amt == uint(-1) ? pngToken.balanceOf(address(this)) : _amt; + approve(pngToken, address(router), _uniAmt); + } +} \ No newline at end of file diff --git a/contracts/avalanche/connectors/pangolin/exchange/interface.sol b/contracts/avalanche/connectors/pangolin/exchange/interface.sol new file mode 100644 index 00000000..ac308099 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/exchange/interface.sol @@ -0,0 +1,164 @@ +pragma solidity >=0.6.2; + +interface IPangolinRouter { + function factory() external pure returns (address); + function WAVAX() external pure returns (address); + + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB, uint liquidity); + + function addLiquidityAVAX( + address token, + uint amountTokenDesired, + uint amountTokenMin, + uint amountAVAXMin, + address to, + uint deadline + ) external payable returns (uint amountToken, uint amountAVAX, uint liquidity); + + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + + function removeLiquidityAVAX( + address token, + uint liquidity, + uint amountTokenMin, + uint amountAVAXMin, + address to, + uint deadline + ) external returns (uint amountToken, uint amountAVAX); + + function removeLiquidityWithPermit( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountA, uint amountB); + + function removeLiquidityAVAXWithPermit( + address token, + uint liquidity, + uint amountTokenMin, + uint amountAVAXMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountToken, uint amountAVAX); + + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + function swapExactAVAXForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function swapTokensForExactAVAX(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + + function swapExactTokensForAVAX(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) + external + returns (uint[] memory amounts); + + function swapAVAXForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) + external + payable + returns (uint[] memory amounts); + + function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); + function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); + function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); + function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); + + function removeLiquidityAVAXSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountAVAXMin, + address to, + uint deadline + ) external returns (uint amountAVAX); + + function removeLiquidityAVAXWithPermitSupportingFeeOnTransferTokens( + address token, + uint liquidity, + uint amountTokenMin, + uint amountAVAXMin, + address to, + uint deadline, + bool approveMax, uint8 v, bytes32 r, bytes32 s + ) external returns (uint amountAVAX); + + function swapExactTokensForTokensSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; + + function swapExactAVAXForTokensSupportingFeeOnTransferTokens( + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external payable; + + function swapExactTokensForAVAXSupportingFeeOnTransferTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external; +} + +interface IPangolinFactory { + event PairCreated(address indexed token0, address indexed token1, address pair, uint); + + function getPair(address tokenA, address tokenB) external view returns (address pair); + function allPairs(uint) external view returns (address pair); + function allPairsLength() external view returns (uint); + + function feeTo() external view returns (address); + function feeToSetter() external view returns (address); + + function createPair(address tokenA, address tokenB) external returns (address pair); + + function setFeeTo(address) external; + function setFeeToSetter(address) external; +} \ No newline at end of file diff --git a/contracts/avalanche/connectors/pangolin/exchange/main.sol b/contracts/avalanche/connectors/pangolin/exchange/main.sol new file mode 100644 index 00000000..c4e866d9 --- /dev/null +++ b/contracts/avalanche/connectors/pangolin/exchange/main.sol @@ -0,0 +1,196 @@ +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 PangolinResolver is Helpers, Events { + /** + * @dev Deposit Liquidity. + * @notice Deposit Liquidity to a Pangolin pool. + * @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 amtB/amtA with slippage. + * @param slippage Slippage amount. + * @param getId ID to retrieve amtA. + * @param setId ID stores the amount of pools tokens received. + */ + function deposit( + address tokenA, + address tokenB, + uint256 amtA, + uint256 unitAmt, + uint256 slippage, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, amtA); + + (uint _amtA, uint _amtB, uint _uniAmt) = _addLiquidity( + tokenA, + tokenB, + _amt, + unitAmt, + slippage + ); + setUint(setId, _uniAmt); + + _eventName = "LogDepositLiquidity(address,address,uint256,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(tokenA, tokenB, _amtA, _amtB, _uniAmt, getId, setId); + } + + /** + * @dev Withdraw Liquidity. + * @notice Withdraw Liquidity from a Pangolin pool. + * @param tokenA The address of token A.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param tokenB The address of token B.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param uniAmt The amount of pool tokens to withdraw. + * @param unitAmtA The unit amount of amtA/uniAmt with slippage. + * @param unitAmtB The unit amount of amtB/uniAmt with slippage. + * @param getId ID to retrieve uniAmt. + * @param setIds Array of IDs to store the amount tokens received. + */ + function withdraw( + address tokenA, + address tokenB, + uint256 uniAmt, + uint256 unitAmtA, + uint256 unitAmtB, + uint256 getId, + uint256[] calldata setIds + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _amt = getUint(getId, uniAmt); + + (uint _amtA, uint _amtB, uint _uniAmt) = _removeLiquidity( + tokenA, + tokenB, + _amt, + unitAmtA, + unitAmtB + ); + + setUint(setIds[0], _amtA); + setUint(setIds[1], _amtB); + + _eventName = "LogWithdrawLiquidity(address,address,uint256,uint256,uint256,uint256,uint256[])"; + _eventParam = abi.encode(tokenA, tokenB, _amtA, _amtB, _uniAmt, getId, setIds); + } + + /** + * @dev Buy AVAX/ERC20_Token. + * @notice Buy a token using a Pangolin + * @param buyAddr The address of the token to buy.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAddr The address of the token to sell.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param buyAmt The amount of tokens to buy. + * @param unitAmt The unit amount of sellAmt/buyAmt with slippage. + * @param getId ID to retrieve buyAmt. + * @param setId ID to store the amount of tokens sold. + */ + function buy( + address buyAddr, + address sellAddr, + uint256 buyAmt, + uint256 unitAmt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _buyAmt = getUint(getId, buyAmt); + (TokenInterface _buyAddr, TokenInterface _sellAddr) = changeAvaxAddress(buyAddr, sellAddr); + address[] memory paths = getPaths(address(_buyAddr), address(_sellAddr)); + + uint _slippageAmt = convert18ToDec(_sellAddr.decimals(), + wmul(unitAmt, convertTo18(_buyAddr.decimals(), _buyAmt)) + ); + + checkPair(paths); + uint _expectedAmt = getExpectedSellAmt(paths, _buyAmt); + require(_slippageAmt >= _expectedAmt, "Too much slippage"); + + bool isAvax = address(_sellAddr) == wavaxAddr; + convertAvaxToWavax(isAvax, _sellAddr, _expectedAmt); + approve(_sellAddr, address(router), _expectedAmt); + + uint _sellAmt = router.swapTokensForExactTokens( + _buyAmt, + _expectedAmt, + paths, + address(this), + block.timestamp + 1 + )[0]; + + isAvax = address(_buyAddr) == wavaxAddr; + convertWavaxToAvax(isAvax, _buyAddr, _buyAmt); + + setUint(setId, _sellAmt); + + _eventName = "LogBuy(address,address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(buyAddr, sellAddr, _buyAmt, _sellAmt, getId, setId); + } + + /** + * @dev Sell AVAX/ERC20_Token. + * @notice Sell a token using a Pangolin + * @param buyAddr The address of the token to buy.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAddr The address of the token to sell.(For AVAX: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) + * @param sellAmt The amount of the token to sell. + * @param unitAmt The unit amount of buyAmt/sellAmt with slippage. + * @param getId ID to retrieve sellAmt. + * @param setId ID stores the amount of token brought. + */ + function sell( + address buyAddr, + address sellAddr, + uint256 sellAmt, + uint256 unitAmt, + uint256 getId, + uint256 setId + ) external payable returns (string memory _eventName, bytes memory _eventParam) { + uint _sellAmt = getUint(getId, sellAmt); + (TokenInterface _buyAddr, TokenInterface _sellAddr) = changeAvaxAddress(buyAddr, sellAddr); + address[] memory paths = getPaths(address(_buyAddr), address(_sellAddr)); + + if (_sellAmt == uint(-1)) { + _sellAmt = sellAddr == avaxAddr ? + address(this).balance : + _sellAddr.balanceOf(address(this)); + } + + uint _slippageAmt = convert18ToDec(_buyAddr.decimals(), + wmul(unitAmt, convertTo18(_sellAddr.decimals(), _sellAmt)) + ); + + checkPair(paths); + uint _expectedAmt = getExpectedBuyAmt(paths, _sellAmt); + require(_slippageAmt <= _expectedAmt, "Too much slippage"); + + bool isAvax = address(_sellAddr) == wavaxAddr; + convertAvaxToWavax(isAvax, _sellAddr, _sellAmt); + approve(_sellAddr, address(router), _sellAmt); + + uint _buyAmt = router.swapExactTokensForTokens( + _sellAmt, + _expectedAmt, + paths, + address(this), + block.timestamp + 1 + )[1]; + + isAvax = address(_buyAddr) == wavaxAddr; + convertWavaxToAvax(isAvax, _buyAddr, _buyAmt); + + setUint(setId, _buyAmt); + + _eventName = "LogSell(address,address,uint256,uint256,uint256,uint256)"; + _eventParam = abi.encode(buyAddr, sellAddr, _buyAmt, _sellAmt, getId, setId); + } +} + +contract ConnectV2PngAvalanche is PangolinResolver { + string public constant name = "Pangolin-v1"; +} 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_exchange.test.ts b/test/avalanche/pangolin/pangolin_exchange.test.ts new file mode 100644 index 00000000..d4bd37f9 --- /dev/null +++ b/test/avalanche/pangolin/pangolin_exchange.test.ts @@ -0,0 +1,282 @@ +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 } from "ethers"; + +import { ConnectV2PngAvalanche__factory } from "../../../typechain"; + +const PNG_ADDRESS = "0x60781C2586D68229fde47564546784ab3fACA982"; +const WAVAX_ADDRESS = "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7"; +const PNG_AVAX_LP_ADDRESS = "0xd7538cABBf8605BdE1f4901B47B8D42c61DE0367"; + +describe("Pangolin DEX - Avalanche", function () { + const pangolinConnectorName = "PANGOLIN-TEST-A" + + let dsaWallet0: Contract; + let masterSigner: Signer; + let instaConnectorsV2: Contract; + let pangolinConnector: 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 + }, + }, + ], + }); + + 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); + }) + + it("Should have contracts deployed.", async function () { + expect(!!instaConnectorsV2.address).to.be.true; + expect(!!pangolinConnector.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("Main - PANGOLIN PNG/AVAX Liquidity Test", function () { + + it("Should use pangolin to swap AVAX for PNG, and deposit to PNG/AVAX LP", async function () { + const amount = ethers.utils.parseEther("100"); // 100 PNG + const int_slippage = 0.03 + const slippage = ethers.utils.parseEther(int_slippage.toString()); + const setId = "83528353"; + + const PangolinRouterABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)" + ]; + + // Get amount of AVAX for 100 POOL 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, + setId + ] + }, + { + connector: pangolinConnectorName, + method: "deposit", + args: [ + PNG_ADDRESS, + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + amount, + unitAmount, + slippage, + 0, + setId + ] + }, + ]; + + // Before Spell + let avaxBalance = await ethers.provider.getBalance(dsaWallet0.address); + expect(avaxBalance, `AVAX Balance equals 10`).to.be.eq(ethers.utils.parseEther("10")); + + let pngToken = await ethers.getContractAt(abis.basic.erc20, PNG_ADDRESS); + const pngBalance = await pngToken.balanceOf(dsaWallet0.address); + expect(pngBalance, `PNG Token greater than 0`).to.be.eq(0); + + let pangolinLPToken = await ethers.getContractAt( + abis.basic.erc20, + PNG_AVAX_LP_ADDRESS + ); + const pangolinPoolAVAXBalance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(pangolinPoolAVAXBalance, `Pangolin PNG/AVAX LP equals 0`).to.be.eq(0); + + // Run spell transaction + const tx = await dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), wallet1.address + ); + const receipt = await tx.wait(); + + // After spell + avaxBalance = await ethers.provider.getBalance(dsaWallet0.address); + expect(avaxBalance, `AVAX Balance less than 10`).to.be.lt(ethers.utils.parseEther("10")); + + const pngBalanceAfter = await pngToken.balanceOf(dsaWallet0.address) + expect(pngBalanceAfter, `PNG Token to be same after spell`).to.be.eq(pngBalance); + + const pangolinPoolAVAXBalanceAfter = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect( + pangolinPoolAVAXBalanceAfter, + `Pangolin PNG/AVAX LP greater than 0` + ).to.be.gt(0); + }); + + it("Should use pangolin to withdraw to PNG/AVAX LP, and swap PNG for AVAX", async function () { + const amount = ethers.utils.parseEther("100"); // 100 PNG + const int_slippage = 0.03 + + // Before Spell + let avaxBalance = await ethers.provider.getBalance(dsaWallet0.address); + let pngToken = await ethers.getContractAt(abis.basic.erc20, PNG_ADDRESS); + let pangolinLPToken = await ethers.getContractAt( + abis.basic.erc20, + PNG_AVAX_LP_ADDRESS + ); + + const pngBalance = await pngToken.balanceOf(dsaWallet0.address) + expect(pngBalance, `PNG Token balance equal to 0`).to.be.eq(0); + + const pangolinPoolAVAXBalance = await pangolinLPToken.balanceOf(dsaWallet0.address); + expect(pangolinPoolAVAXBalance, `Pangolin PNG/AVAX LP greater than 0`).to.be.gt(0); + + const PangolinRouterABI = [ + "function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts)" + ]; + + // Get amount of avax for 100 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 unitAmtA = ethers.utils.parseEther( + (amtA * (1 - int_slippage) / pangolinPoolAVAXBalance).toString() + ); + const unitAmtB = ethers.utils.parseEther( + (amtB * (1 - int_slippage) / pangolinPoolAVAXBalance).toString() + ); + + let spells = [ + { + connector: pangolinConnectorName, + method: "withdraw", + args: [ + PNG_ADDRESS, + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + pangolinPoolAVAXBalance, + unitAmtA, + unitAmtB, + 0, + [ + 0, + 0 + ] + ] + }, + ]; + + // Run spell transaction (withdraw token of pool) + const tx = await dsaWallet0.connect(wallet0).cast( + ...encodeSpells(spells), + wallet1.address + ); + const receipt = await tx.wait(); + + // After spell + const pangolinPoolAVAXBalanceAfter = await pangolinLPToken.balanceOf( + dsaWallet0.address + ); + expect(pangolinPoolAVAXBalanceAfter, `Pangolin PNG/AVAX LP equal 0`).to.be.eq(0); + + let pngBalanceAfter = await pngToken.balanceOf(dsaWallet0.address); + expect(pngBalanceAfter, `PNG Token balance greater than`).to.be.gt(0); + const unitAmt = amount.div(pngBalanceAfter); + + spells = [ + { + connector: pangolinConnectorName, + method: "sell", + args: [ + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + PNG_ADDRESS, + pngBalanceAfter, + unitAmt, + 0, + 0 + ] + }, + ]; + + // Run spell transaction (withdraw token of pool) + const tx2 = await dsaWallet0.connect(wallet0).cast(...encodeSpells(spells), wallet1.address); + const receipt2 = await tx2.wait(); + + let avaxBalanceAfter = await ethers.provider.getBalance(dsaWallet0.address); + expect( + avaxBalanceAfter, + `AVAX Balance After greater than AVAX Balance Before` + ).to.be.gt(avaxBalance); + + pngBalanceAfter = await pngToken.balanceOf(dsaWallet0.address); + expect(pngBalanceAfter, `PNG Token balance equal 0`).to.be.eq(0); + }); + }) +}); 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"); + }); + }); + }); +});