From 797849db20ff70bf6689d0b54054e7e8447a8c95 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 5 Jul 2023 08:37:11 +0800 Subject: [PATCH] chore: pair staking test case --- contracts/apestaking/ParaApeStaking.sol | 43 ++- .../apestaking/logic/ApeStakingVaultLogic.sol | 180 ++++++----- contracts/interfaces/IApeStakingVault.sol | 1 + contracts/interfaces/IPoolApeStaking.sol | 2 + .../protocol/libraries/logic/BorrowLogic.sol | 5 + contracts/protocol/pool/PoolApeStaking.sol | 14 +- .../tokenization/NTokenApeStaking.sol | 3 + .../protocol/tokenization/NTokenBAKC.sol | 3 + helpers/contracts-deployments.ts | 153 ++++++++- helpers/contracts-getters.ts | 12 + helpers/types.ts | 4 + package.json | 2 +- scripts/deployments/steps/06_pool.ts | 3 + .../deployments/steps/23_renounceOwnership.ts | 26 ++ test/para_ape_staking.spec.ts | 299 ++++++++++++++++++ 15 files changed, 667 insertions(+), 83 deletions(-) create mode 100644 test/para_ape_staking.spec.ts diff --git a/contracts/apestaking/ParaApeStaking.sol b/contracts/apestaking/ParaApeStaking.sol index b28d84e50..897285481 100644 --- a/contracts/apestaking/ParaApeStaking.sol +++ b/contracts/apestaking/ParaApeStaking.sol @@ -15,6 +15,7 @@ import {WadRayMath} from "../protocol/libraries/math/WadRayMath.sol"; import "./logic/ApeStakingP2PLogic.sol"; import "./logic/ApeStakingVaultLogic.sol"; import "./logic/ApeStakingCommonLogic.sol"; +import "hardhat/console.sol"; contract ParaApeStaking is Initializable, @@ -239,6 +240,12 @@ contract ParaApeStaking is return ApeStakingP2PLogic.getApeCoinStakingCap(stakingType, vars); } + uint256 constant BAYC_BAKC_PAIR_POOL_ID = 1; + uint256 constant MAYC_BAKC_PAIR_POOL_ID = 2; + uint256 constant BAYC_SINGLE_POOL_ID = 3; + uint256 constant MAYC_SINGLE_POOL_ID = 4; + uint256 constant BAKC_SINGLE_POOL_ID = 5; + mapping(uint256 => PoolState) public poolStates; //address public vaultBot; @@ -248,9 +255,13 @@ contract ParaApeStaking is uint32[] calldata apeTokenIds, uint32[] calldata bakcTokenIds ) external { + console.log("depositPairNFT---------------------0"); ApeStakingVaultCacheVars memory vars = _createCacheVars(); + uint256 poolId = isBAYC + ? BAYC_BAKC_PAIR_POOL_ID + : MAYC_BAKC_PAIR_POOL_ID; ApeStakingVaultLogic.depositPairNFT( - poolStates, + poolStates[poolId], vars, isBAYC, apeTokenIds, @@ -263,9 +274,13 @@ contract ParaApeStaking is uint32[] calldata apeTokenIds, uint32[] calldata bakcTokenIds ) external { + console.log("stakingPairNFT---------------------0"); ApeStakingVaultCacheVars memory vars = _createCacheVars(); + uint256 poolId = isBAYC + ? BAYC_BAKC_PAIR_POOL_ID + : MAYC_BAKC_PAIR_POOL_ID; ApeStakingVaultLogic.stakingPairNFT( - poolStates, + poolStates[poolId], vars, isBAYC, apeTokenIds, @@ -279,8 +294,11 @@ contract ParaApeStaking is uint32[] calldata bakcTokenIds ) external { ApeStakingVaultCacheVars memory vars = _createCacheVars(); + uint256 poolId = isBAYC + ? BAYC_BAKC_PAIR_POOL_ID + : MAYC_BAKC_PAIR_POOL_ID; ApeStakingVaultLogic.withdrawPairNFT( - poolStates, + poolStates[poolId], vars, isBAYC, apeTokenIds, @@ -295,8 +313,11 @@ contract ParaApeStaking is uint32[] calldata bakcTokenIds ) external { ApeStakingVaultCacheVars memory vars = _createCacheVars(); + uint256 poolId = isBAYC + ? BAYC_BAKC_PAIR_POOL_ID + : MAYC_BAKC_PAIR_POOL_ID; ApeStakingVaultLogic.claimPairNFT( - poolStates, + poolStates[poolId], vars, isBAYC, apeTokenIds, @@ -310,8 +331,11 @@ contract ParaApeStaking is uint32[] calldata bakcTokenIds ) external { ApeStakingVaultCacheVars memory vars = _createCacheVars(); + uint256 poolId = isBAYC + ? BAYC_BAKC_PAIR_POOL_ID + : MAYC_BAKC_PAIR_POOL_ID; ApeStakingVaultLogic.compoundPairNFT( - poolStates, + poolStates[poolId], vars, isBAYC, apeTokenIds, @@ -347,4 +371,13 @@ contract ParaApeStaking is vars.bakcMatchedCap = bakcMatchedCap; return vars; } + + function onERC721Received( + address, + address, + uint256, + bytes memory + ) external pure returns (bytes4) { + return this.onERC721Received.selector; + } } diff --git a/contracts/apestaking/logic/ApeStakingVaultLogic.sol b/contracts/apestaking/logic/ApeStakingVaultLogic.sol index 4220e2775..d4ba3dfdd 100644 --- a/contracts/apestaking/logic/ApeStakingVaultLogic.sol +++ b/contracts/apestaking/logic/ApeStakingVaultLogic.sol @@ -11,6 +11,7 @@ import "../../interfaces/ICApe.sol"; import {SignatureChecker} from "../../dependencies/looksrare/contracts/libraries/SignatureChecker.sol"; import "../../dependencies/openzeppelin/contracts/SafeCast.sol"; import {WadRayMath} from "../../protocol/libraries/math/WadRayMath.sol"; +import "hardhat/console.sol"; /** * @title ApeStakingVaultLogic library @@ -26,12 +27,7 @@ library ApeStakingVaultLogic { uint256 constant BAYC_POOL_ID = 1; uint256 constant MAYC_POOL_ID = 2; uint256 constant BAKC_POOL_ID = 3; - uint256 constant BAYC_BAKC_PAIR_POOL_ID = 1; - uint256 constant MAYC_BAKC_PAIR_POOL_ID = 2; - uint256 constant BAYC_SINGLE_POOL_ID = 3; - uint256 constant MAYC_SINGLE_POOL_ID = 4; - uint256 constant BAKC_SINGLE_POOL_ID = 5; - uint256 internal constant WAD = 1e18; + event PairNFTDeposited( bool isBAYC, uint256 apeTokenId, @@ -47,7 +43,7 @@ library ApeStakingVaultLogic { ); function depositPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, @@ -59,13 +55,6 @@ library ApeStakingVaultLogic { "wrong param" ); - IParaApeStaking.PoolState storage poolState; - { - uint256 poolId = isBAYC - ? BAYC_BAKC_PAIR_POOL_ID - : MAYC_BAKC_PAIR_POOL_ID; - poolState = poolStates[poolId]; - } vars.apeStakingPoolId = isBAYC ? BAYC_POOL_ID : MAYC_POOL_ID; vars.apeToken = isBAYC ? vars.bayc : vars.mayc; vars.nApe = isBAYC ? vars.nBayc : vars.nMayc; @@ -127,25 +116,19 @@ library ApeStakingVaultLogic { } function stakingPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, uint32[] calldata bakcTokenIds ) external { + console.log("stakingPairNFT---------------------1"); uint256 arrayLength = apeTokenIds.length; require( arrayLength == bakcTokenIds.length && arrayLength > 0, "wrong param" ); - IParaApeStaking.PoolState storage poolState; - { - uint256 poolId = isBAYC - ? BAYC_BAKC_PAIR_POOL_ID - : MAYC_BAKC_PAIR_POOL_ID; - poolState = poolStates[poolId]; - } ApeCoinStaking.SingleNft[] memory _nfts = new ApeCoinStaking.SingleNft[](arrayLength); ApeCoinStaking.PairNftDepositWithAmount[] @@ -188,10 +171,19 @@ library ApeStakingVaultLogic { } // prepare Ape coin - uint256 totalNeed = (vars.positionCap + vars.bakcMatchedCap) * + console.log("---------------------0"); + uint256 totalBorrow = (vars.positionCap + vars.bakcMatchedCap) * arrayLength; - IPool(vars.pool).borrowPoolCApe(totalNeed); - IAutoCompoundApe(vars.cApe).withdraw(totalNeed); + uint256 latestBorrowIndex = IPool(vars.pool).borrowPoolCApe( + totalBorrow + ); + IAutoCompoundApe(vars.cApe).withdraw(totalBorrow); + uint256 cApeExchangeRate = ICApe(vars.cApe).getPooledApeByShares( + WadRayMath.RAY + ); + poolState.cApeDebtShare += totalBorrow.rayDiv(latestBorrowIndex).rayDiv( + cApeExchangeRate + ); //stake in ApeCoinStaking ApeCoinStaking.PairNftDepositWithAmount[] @@ -211,7 +203,7 @@ library ApeStakingVaultLogic { } function withdrawPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, @@ -223,15 +215,8 @@ library ApeStakingVaultLogic { "wrong param" ); - _claimPairNFT(poolStates, vars, isBAYC, apeTokenIds, bakcTokenIds); + _claimPairNFT(poolState, vars, isBAYC, apeTokenIds, bakcTokenIds); - IParaApeStaking.PoolState storage poolState; - { - uint256 poolId = isBAYC - ? BAYC_BAKC_PAIR_POOL_ID - : MAYC_BAKC_PAIR_POOL_ID; - poolState = poolStates[poolId]; - } vars.apeStakingPoolId = isBAYC ? BAYC_POOL_ID : MAYC_POOL_ID; vars.apeToken = isBAYC ? vars.bayc : vars.mayc; vars.nApe = isBAYC ? vars.nBayc : vars.nMayc; @@ -319,14 +304,14 @@ library ApeStakingVaultLogic { } vars.balanceAfter = IERC20(vars.apeCoin).balanceOf(address(this)); uint256 balanceDiff = vars.balanceAfter - vars.balanceBefore; - - //update reward index IAutoCompoundApe(vars.cApe).deposit(address(this), balanceDiff); - uint256 cApeExchangeRate = ICApe(vars.cApe).getPooledApeByShares( - WAD + + _reayAndCompound( + poolState, + vars, + balanceDiff, + vars.positionCap + vars.bakcMatchedCap ); - uint256 shareAmount = balanceDiff.wadDiv(cApeExchangeRate); - poolState.accumulatedRewardsPerNft += shareAmount / totalPosition; } //transfer ape and BAKC bakc to nToken @@ -351,7 +336,7 @@ library ApeStakingVaultLogic { } function claimPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, @@ -363,11 +348,11 @@ library ApeStakingVaultLogic { "wrong param" ); - _claimPairNFT(poolStates, vars, isBAYC, apeTokenIds, bakcTokenIds); + _claimPairNFT(poolState, vars, isBAYC, apeTokenIds, bakcTokenIds); } function compoundPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, @@ -379,13 +364,6 @@ library ApeStakingVaultLogic { "wrong param" ); - IParaApeStaking.PoolState storage poolState; - { - uint256 poolId = isBAYC - ? BAYC_BAKC_PAIR_POOL_ID - : MAYC_BAKC_PAIR_POOL_ID; - poolState = poolStates[poolId]; - } uint256[] memory _nfts = new uint256[](arrayLength); ApeCoinStaking.PairNft[] memory _nftPairs = new ApeCoinStaking.PairNft[](arrayLength); @@ -416,41 +394,39 @@ library ApeStakingVaultLogic { vars.balanceBefore = IERC20(vars.apeCoin).balanceOf(address(this)); //claim from ApeCoinStaking - ApeCoinStaking.PairNft[] + { + ApeCoinStaking.PairNft[] memory _otherPairs = new ApeCoinStaking.PairNft[](0); - if (isBAYC) { - vars.apeCoinStaking.claimSelfBAYC(_nfts); - vars.apeCoinStaking.claimSelfBAKC(_nftPairs, _otherPairs); - } else { - vars.apeCoinStaking.claimSelfMAYC(_nfts); - vars.apeCoinStaking.claimSelfBAKC(_otherPairs, _nftPairs); + if (isBAYC) { + vars.apeCoinStaking.claimSelfBAYC(_nfts); + vars.apeCoinStaking.claimSelfBAKC(_nftPairs, _otherPairs); + } else { + vars.apeCoinStaking.claimSelfMAYC(_nfts); + vars.apeCoinStaking.claimSelfBAKC(_otherPairs, _nftPairs); + } } + vars.balanceAfter = IERC20(vars.apeCoin).balanceOf(address(this)); uint256 balanceDiff = vars.balanceAfter - vars.balanceBefore; - - //update reward index IAutoCompoundApe(vars.cApe).deposit(address(this), balanceDiff); - uint256 cApeExchangeRate = ICApe(vars.cApe).getPooledApeByShares(WAD); - uint256 shareAmount = balanceDiff.wadDiv(cApeExchangeRate); - poolState.accumulatedRewardsPerNft += - shareAmount / - poolState.totalPosition; + + //repay and compound + vars.positionCap = isBAYC ? vars.baycMatchedCap : vars.maycMatchedCap; + _reayAndCompound( + poolState, + vars, + balanceDiff, + vars.positionCap + vars.bakcMatchedCap + ); } function _claimPairNFT( - mapping(uint256 => IParaApeStaking.PoolState) storage poolStates, + IParaApeStaking.PoolState storage poolState, IParaApeStaking.ApeStakingVaultCacheVars memory vars, bool isBAYC, uint32[] calldata apeTokenIds, uint32[] calldata bakcTokenIds ) internal { - IParaApeStaking.PoolState storage poolState; - { - uint256 poolId = isBAYC - ? BAYC_BAKC_PAIR_POOL_ID - : MAYC_BAKC_PAIR_POOL_ID; - poolState = poolStates[poolId]; - } vars.apeStakingPoolId = isBAYC ? BAYC_POOL_ID : MAYC_POOL_ID; vars.accumulatedRewardsPerNft = poolState.accumulatedRewardsPerNft; uint256 rewardShares; @@ -498,4 +474,64 @@ library ApeStakingVaultLogic { IERC20(vars.cApe).safeTransfer(claimFor, rewardShares); } } + + function _reayAndCompound( + IParaApeStaking.PoolState storage poolState, + IParaApeStaking.ApeStakingVaultCacheVars memory vars, + uint256 totalAmount, + uint256 positionCap + ) internal { + console.log("_reayAndCompound---------------------------0"); + uint256 cApeExchangeRate = ICApe(vars.cApe).getPooledApeByShares( + WadRayMath.RAY + ); + uint256 latestBorrowIndex = IPool(vars.pool) + .getReserveNormalizedVariableDebt(vars.cApe); + uint256 cApeDebtShare = poolState.cApeDebtShare; + uint256 debtInterest = _calculateCurrentPositionDebtInterest( + cApeDebtShare, + poolState.totalPosition, + positionCap, + cApeExchangeRate, + latestBorrowIndex + ); + console.log("_reayAndCompound---------------------------totalAmount:", totalAmount); + console.log("_reayAndCompound---------------------------debtInterest:", debtInterest); + if (debtInterest >= totalAmount) { + console.log("_reayAndCompound---------------------------1"); + IERC20(vars.cApe).safeApprove(vars.pool, totalAmount); + IPool(vars.pool).repay(vars.cApe, totalAmount, address(this)); + cApeDebtShare -= totalAmount.rayDiv(latestBorrowIndex).rayDiv( + cApeExchangeRate + ); + } else { + //update reward index + console.log("_reayAndCompound---------------------------2"); + IERC20(vars.cApe).safeApprove(vars.pool, debtInterest); + IPool(vars.pool).repay(vars.cApe, debtInterest, address(this)); + uint256 remainingReward = totalAmount - debtInterest; + uint256 shareAmount = remainingReward.rayDiv(cApeExchangeRate); + poolState.accumulatedRewardsPerNft += + shareAmount / + poolState.totalPosition; + cApeDebtShare -= debtInterest.rayDiv(latestBorrowIndex).rayDiv( + cApeExchangeRate + ); + } + console.log("_reayAndCompound---------------------------3"); + poolState.cApeDebtShare = cApeDebtShare; + } + + function _calculateCurrentPositionDebtInterest( + uint256 cApeDebtShare, + uint256 totalPosition, + uint256 perPositionCap, + uint256 cApeExchangeRate, + uint256 latestBorrowIndex + ) internal pure returns (uint256) { + uint256 currentDebt = cApeDebtShare.rayMul(cApeExchangeRate).rayMul( + latestBorrowIndex + ); + return (currentDebt - perPositionCap * totalPosition); + } } diff --git a/contracts/interfaces/IApeStakingVault.sol b/contracts/interfaces/IApeStakingVault.sol index 43c130393..34ae812f1 100644 --- a/contracts/interfaces/IApeStakingVault.sol +++ b/contracts/interfaces/IApeStakingVault.sol @@ -16,5 +16,6 @@ interface IApeStakingVault { mapping(uint256 => uint256) rewardsDebt; //apeTokenId => PairingStatus mapping(uint256 => PairingStatus) pairStatus; + uint256 cApeDebtShare; } } diff --git a/contracts/interfaces/IPoolApeStaking.sol b/contracts/interfaces/IPoolApeStaking.sol index 9b558b272..3d7fe4925 100644 --- a/contracts/interfaces/IPoolApeStaking.sol +++ b/contracts/interfaces/IPoolApeStaking.sol @@ -20,6 +20,8 @@ interface IPoolApeStaking { uint256 cashAmount; } + function paraApeStaking() external view returns (address); + function borrowPoolCApe(uint256 amount) external returns (uint256); /** diff --git a/contracts/protocol/libraries/logic/BorrowLogic.sol b/contracts/protocol/libraries/logic/BorrowLogic.sol index cad627ac1..a528c633f 100644 --- a/contracts/protocol/libraries/logic/BorrowLogic.sol +++ b/contracts/protocol/libraries/logic/BorrowLogic.sol @@ -14,6 +14,7 @@ import {DataTypes} from "../types/DataTypes.sol"; import {ValidationLogic} from "./ValidationLogic.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {GenericLogic} from "./GenericLogic.sol"; +import "hardhat/console.sol"; /** * @title BorrowLogic library @@ -143,6 +144,7 @@ library BorrowLogic { address asset, uint256 amount ) external returns (uint256) { + console.log("---------------------2"); DataTypes.ReserveData storage reserve = reservesData[asset]; DataTypes.ReserveCache memory reserveCache = reserve.cache(); @@ -150,6 +152,9 @@ library BorrowLogic { ValidationLogic.validateBorrowWithoutCollateral(reserveCache, amount); + console.log("variableDebtTokenAddress:", reserveCache.variableDebtTokenAddress); + console.log("borrowFor:", borrowFor); + console.log("borrow asset:", asset); (, reserveCache.nextScaledVariableDebt) = IVariableDebtToken( reserveCache.variableDebtTokenAddress ).mint( diff --git a/contracts/protocol/pool/PoolApeStaking.sol b/contracts/protocol/pool/PoolApeStaking.sol index f2ecf67ed..2d331077b 100644 --- a/contracts/protocol/pool/PoolApeStaking.sol +++ b/contracts/protocol/pool/PoolApeStaking.sol @@ -27,6 +27,7 @@ import {Math} from "../../dependencies/openzeppelin/contracts/Math.sol"; import {ISwapRouter} from "../../dependencies/univ3/interfaces/ISwapRouter.sol"; import {IPriceOracleGetter} from "../../interfaces/IPriceOracleGetter.sol"; import {Helpers} from "../libraries/helpers/Helpers.sol"; +import "hardhat/console.sol"; contract PoolApeStaking is ParaVersionedInitializable, @@ -52,7 +53,7 @@ contract PoolApeStaking is uint24 internal immutable APE_WETH_FEE; uint24 internal immutable WETH_USDC_FEE; address internal immutable WETH; - address internal immutable APE_STAKING_VAULT; + address internal immutable PARA_APE_STAKING; event ReserveUsedAsCollateralEnabled( address indexed reserve, @@ -103,24 +104,29 @@ contract PoolApeStaking is WETH = weth; APE_WETH_FEE = apeWethFee; WETH_USDC_FEE = wethUsdcFee; - APE_STAKING_VAULT = apeStakingVault; + PARA_APE_STAKING = apeStakingVault; } function getRevision() internal pure virtual override returns (uint256) { return POOL_REVISION; } + function paraApeStaking() external view returns (address) { + return PARA_APE_STAKING; + } + function borrowPoolCApe(uint256 amount) external nonReentrant returns (uint256) { - require(msg.sender == APE_STAKING_VAULT); + require(msg.sender == PARA_APE_STAKING); DataTypes.PoolStorage storage ps = poolStorage(); + console.log("---------------------1"); uint256 latestBorrowIndex = BorrowLogic.executeBorrowWithoutCollateral( ps._reserves, - APE_STAKING_VAULT, + PARA_APE_STAKING, address(APE_COMPOUND), amount ); diff --git a/contracts/protocol/tokenization/NTokenApeStaking.sol b/contracts/protocol/tokenization/NTokenApeStaking.sol index 4c11c4edf..34a16e6f1 100644 --- a/contracts/protocol/tokenization/NTokenApeStaking.sol +++ b/contracts/protocol/tokenization/NTokenApeStaking.sol @@ -70,6 +70,9 @@ abstract contract NTokenApeStaking is NToken, INTokenApeStaking { } getBAKC().setApprovalForAll(address(POOL), true); + address paraApeStaking = POOL.paraApeStaking(); + IERC721(underlyingAsset).setApprovalForAll(paraApeStaking, true); + super.initialize( initializingPool, underlyingAsset, diff --git a/contracts/protocol/tokenization/NTokenBAKC.sol b/contracts/protocol/tokenization/NTokenBAKC.sol index 3252e519f..68e17a130 100644 --- a/contracts/protocol/tokenization/NTokenBAKC.sol +++ b/contracts/protocol/tokenization/NTokenBAKC.sol @@ -69,6 +69,9 @@ contract NTokenBAKC is NToken { ape.approve(nMAYC, type(uint256).max); } IERC721(underlyingAsset).setApprovalForAll(address(POOL), true); + + address paraApeStaking = POOL.paraApeStaking(); + IERC721(underlyingAsset).setApprovalForAll(paraApeStaking, true); } function _transfer( diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index 871052af3..c49312a78 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -291,6 +291,12 @@ import { NTokenChromieSquiggle__factory, CLFixedPriceSynchronicityPriceAdapter, CLFixedPriceSynchronicityPriceAdapter__factory, + ParaApeStaking, + ParaApeStaking__factory, + ApeStakingP2PLogic__factory, + ApeStakingP2PLogic, + ApeStakingVaultLogic, + ApeStakingVaultLogic__factory, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -309,6 +315,7 @@ import { getP2PPairStaking, getAutoYieldApe, getHelperContract, + getParaApeStaking, } from "./contracts-getters"; import { convertToCurrencyDecimals, @@ -337,6 +344,8 @@ import {pick, upperFirst} from "lodash"; import {ZERO_ADDRESS} from "./constants"; import {GLOBAL_OVERRIDES} from "./hardhat-constants"; import {parseEther} from "ethers/lib/utils"; +import {zeroAddress} from "ethereumjs-util"; +import {ParaApeStakingLibraryAddresses} from "../types/factories/contracts/apestaking/ParaApeStaking__factory"; export const deployPoolAddressesProvider = async ( marketId: string, @@ -891,6 +900,7 @@ export const deployPoolComponents = async ( allTokens.WETH.address, APE_WETH_FEE, WETH_USDC_FEE, + (await getParaApeStaking()).address, ], verify, false, @@ -2346,7 +2356,7 @@ export const deployApeCoinStaking = async (verify?: boolean) => { amount, "1666771200", "1761465600", - parseEther("100000"), + parseEther("50000"), GLOBAL_OVERRIDES ); return apeCoinStaking; @@ -2703,6 +2713,147 @@ export const deployP2PPairStaking = async (verify?: boolean) => { return await getP2PPairStaking(proxyInstance.address); }; +export const deployApeStakingP2PLogic = async (verify?: boolean) => + withSaveAndVerify( + new ApeStakingP2PLogic__factory(await getFirstSigner()), + eContractid.ApeStakingP2PLogic, + [], + verify + ) as Promise; + +export const deployApeStakingVaultLogic = async (verify?: boolean) => + withSaveAndVerify( + new ApeStakingVaultLogic__factory(await getFirstSigner()), + eContractid.ApeStakingVaultLogic, + [], + verify + ) as Promise; + +export const deployParaApeStakingLibraries = async ( + verify?: boolean +): Promise => { + const p2pLogic = await deployApeStakingP2PLogic(verify); + const vaultLogic = await deployApeStakingVaultLogic(verify); + + return { + ["contracts/apestaking/logic/ApeStakingP2PLogic.sol:ApeStakingP2PLogic"]: + p2pLogic.address, + ["contracts/apestaking/logic/ApeStakingVaultLogic.sol:ApeStakingVaultLogic"]: + vaultLogic.address, + }; +}; + +export const deployFakeParaApeStakingImpl = async (verify?: boolean) => { + const allTokens = await getAllTokens(); + const apeCoinStaking = + (await getContractAddressInDb(eContractid.ApeCoinStaking)) || + (await deployApeCoinStaking(verify)).address; + + const args = [ + zeroAddress(), + zeroAddress(), + zeroAddress(), + zeroAddress(), + zeroAddress(), + zeroAddress(), + zeroAddress(), + allTokens.APE.address, + allTokens.cAPE.address, + apeCoinStaking, + zeroAddress(), + 0, + ]; + + const libraries = await deployParaApeStakingLibraries(); + + return withSaveAndVerify( + new ParaApeStaking__factory(libraries, await getFirstSigner()), + eContractid.ParaApeStakingImpl, + [...args], + verify + ) as Promise; +}; + +export const deployParaApeStakingImpl = async ( + compoundFee: number, + verify?: boolean +) => { + const poolProxy = await getPoolProxy(); + const allTokens = await getAllTokens(); + const protocolDataProvider = await getProtocolDataProvider(); + const nBAYC = ( + await protocolDataProvider.getReserveTokensAddresses(allTokens.BAYC.address) + ).xTokenAddress; + const nMAYC = ( + await protocolDataProvider.getReserveTokensAddresses(allTokens.MAYC.address) + ).xTokenAddress; + const nBAKC = ( + await protocolDataProvider.getReserveTokensAddresses(allTokens.BAKC.address) + ).xTokenAddress; + const apeCoinStaking = + (await getContractAddressInDb(eContractid.ApeCoinStaking)) || + (await deployApeCoinStaking(verify)).address; + const aclManager = await getACLManager(); + const args = [ + poolProxy.address, + allTokens.BAYC.address, + allTokens.MAYC.address, + allTokens.BAKC.address, + nBAYC, + nMAYC, + nBAKC, + allTokens.APE.address, + allTokens.cAPE.address, + apeCoinStaking, + aclManager.address, + compoundFee, + ]; + + const libraries = await deployParaApeStakingLibraries(); + + return withSaveAndVerify( + new ParaApeStaking__factory(libraries, await getFirstSigner()), + eContractid.ParaApeStakingImpl, + [...args], + verify + ) as Promise; +}; + +export const deployParaApeStaking = async ( + fakeImplementation: boolean, + verify?: boolean +) => { + let stakingImplementation; + if (fakeImplementation) { + stakingImplementation = await deployFakeParaApeStakingImpl(verify); + } else { + stakingImplementation = await deployParaApeStakingImpl(0, verify); + } + const deployer = await getFirstSigner(); + const deployerAddress = await deployer.getAddress(); + const initData = + stakingImplementation.interface.encodeFunctionData("initialize"); + + const proxyInstance = await withSaveAndVerify( + new InitializableAdminUpgradeabilityProxy__factory(await getFirstSigner()), + eContractid.ParaApeStaking, + [], + verify + ); + await waitForTx( + await (proxyInstance as InitializableAdminUpgradeabilityProxy)[ + "initialize(address,address,bytes)" + ]( + stakingImplementation.address, + deployerAddress, + initData, + GLOBAL_OVERRIDES + ) + ); + + return await getParaApeStaking(proxyInstance.address); +}; + export const deployAutoYieldApeImpl = async (verify?: boolean) => { const allTokens = await getAllTokens(); const apeCoinStaking = diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 60d60be45..e3e03c5c2 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -97,6 +97,7 @@ import { NTokenStakefish__factory, MockLendPool__factory, NTokenChromieSquiggle__factory, + ParaApeStaking__factory, } from "../types"; import { getEthersSigners, @@ -995,6 +996,17 @@ export const getP2PPairStaking = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getParaApeStaking = async (address?: tEthereumAddress) => + await ParaApeStaking__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.ParaApeStaking}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + export const getHelperContract = async (address?: tEthereumAddress) => await HelperContract__factory.connect( address || diff --git a/helpers/types.ts b/helpers/types.ts index 20d97c1ae..f4fe1879d 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -260,6 +260,10 @@ export enum eContractid { HelperContractImpl = "HelperContractImpl", HelperContract = "HelperContract", P2PPairStakingImpl = "P2PPairStakingImpl", + ApeStakingP2PLogic = "ApeStakingP2PLogic", + ApeStakingVaultLogic = "ApeStakingVaultLogic", + ParaApeStakingImpl = "ParaApeStakingImpl", + ParaApeStaking = "ParaApeStaking", yAPE = "yAPE", yAPEImpl = "yAPEImpl", ParaProxyInterfacesImpl = "ParaProxyInterfacesImpl", diff --git a/package.json b/package.json index d1ac692b9..cf21f5564 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "coverage": "hardhat coverage --testfiles 'test/*.ts'", "format": "prettier --write 'contracts/**/*.sol' 'scripts/**/*.ts' 'helpers/**/*.ts' 'tasks/**/*.ts' 'test/**/*.ts' 'hardhat.config.ts' 'helper-hardhat-config.ts' 'market-config/**/*.ts'", "doc": "hardhat docgen", - "test": "hardhat test ./test/*.ts", + "test": "hardhat test ./test/para_ape_staking.spec.ts", "clean": "hardhat clean" }, "devDependencies": { diff --git a/scripts/deployments/steps/06_pool.ts b/scripts/deployments/steps/06_pool.ts index efdff9ff1..34742232f 100644 --- a/scripts/deployments/steps/06_pool.ts +++ b/scripts/deployments/steps/06_pool.ts @@ -1,6 +1,7 @@ import {ZERO_ADDRESS} from "../../../helpers/constants"; import { deployMockBendDaoLendPool, + deployParaApeStaking, deployPoolComponents, deployPoolParaProxyInterfaces, deployPoolPositionMover, @@ -30,6 +31,8 @@ export const step_06 = async (verify = false) => { const paraSpaceConfig = getParaSpaceConfig(); try { + await deployParaApeStaking(true); + const { poolCore, poolParameters, diff --git a/scripts/deployments/steps/23_renounceOwnership.ts b/scripts/deployments/steps/23_renounceOwnership.ts index 5c5d7e937..7c492116a 100644 --- a/scripts/deployments/steps/23_renounceOwnership.ts +++ b/scripts/deployments/steps/23_renounceOwnership.ts @@ -8,6 +8,7 @@ import { getInitializableAdminUpgradeabilityProxy, getNFTFloorOracle, getP2PPairStaking, + getParaApeStaking, getPausableZoneController, getPoolAddressesProvider, getPoolAddressesProviderRegistry, @@ -396,6 +397,31 @@ export const step_23 = async ( console.log(); } + //////////////////////////////////////////////////////////////////////////////// + // ParaApeStaking + //////////////////////////////////////////////////////////////////////////////// + if (await getContractAddressInDb(eContractid.ParaApeStaking)) { + console.time("transferring ParaApeStaking ownership..."); + const paraApeStaking = await getParaApeStaking(); + const paraApeStakingProxy = + await getInitializableAdminUpgradeabilityProxy(paraApeStaking.address); + const signers = await getEthersSigners(); + const adminAddress = signers[5].getAddress(); + if (DRY_RUN) { + const encodedData1 = paraApeStakingProxy.interface.encodeFunctionData( + "changeAdmin", + [adminAddress] + ); + await dryRunEncodedData(paraApeStakingProxy.address, encodedData1); + } else { + await waitForTx( + await paraApeStakingProxy.changeAdmin(adminAddress, GLOBAL_OVERRIDES) + ); + } + console.timeEnd("transferring ParaApeStaking ownership..."); + console.log(); + } + //////////////////////////////////////////////////////////////////////////////// // HelperContract //////////////////////////////////////////////////////////////////////////////// diff --git a/test/para_ape_staking.spec.ts b/test/para_ape_staking.spec.ts new file mode 100644 index 000000000..6a1f1fb45 --- /dev/null +++ b/test/para_ape_staking.spec.ts @@ -0,0 +1,299 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import { + AutoCompoundApe, + P2PPairStaking, + ParaApeStaking, + VariableDebtToken, +} from "../types"; +import {TestEnv} from "./helpers/make-suite"; +import {testEnvFixture} from "./helpers/setup-env"; +import {mintAndValidate, supplyAndValidate} from "./helpers/validated-steps"; +import { + getAutoCompoundApe, + getInitializableAdminUpgradeabilityProxy, + getP2PPairStaking, + getParaApeStaking, + getVariableDebtToken, +} from "../helpers/contracts-getters"; +import {MAX_UINT_AMOUNT} from "../helpers/constants"; +import {advanceTimeAndBlock, waitForTx} from "../helpers/misc-utils"; +import {getSignedListingOrder} from "./helpers/p2ppairstaking-helper"; +import {parseEther} from "ethers/lib/utils"; +import {almostEqual} from "./helpers/uniswapv3-helper"; +import { + deployP2PPairStakingImpl, + deployParaApeStakingImpl, +} from "../helpers/contracts-deployments"; +import {GLOBAL_OVERRIDES} from "../helpers/hardhat-constants"; +import {ProtocolErrors} from "../helpers/types"; + +describe("Para Ape Staking Test", () => { + let testEnv: TestEnv; + let variableDebtCApeCoin: VariableDebtToken; + let paraApeStaking: ParaApeStaking; + let cApe: AutoCompoundApe; + let MINIMUM_LIQUIDITY; + + const fixture = async () => { + testEnv = await loadFixture(testEnvFixture); + const { + ape, + users: [user1, , , user4, user5, user6], + apeCoinStaking, + pool, + protocolDataProvider, + } = testEnv; + + //upgrade to non-fake implementation + const paraApeStakingImpl = await deployParaApeStakingImpl(0); + paraApeStaking = await getParaApeStaking(); + const paraApeStakingProxy = await getInitializableAdminUpgradeabilityProxy( + paraApeStaking.address + ); + await waitForTx( + await paraApeStakingProxy + .connect(user5.signer) + .upgradeTo(paraApeStakingImpl.address, GLOBAL_OVERRIDES) + ); + + cApe = await getAutoCompoundApe(); + MINIMUM_LIQUIDITY = await cApe.MINIMUM_LIQUIDITY(); + + const {variableDebtTokenAddress: variableDebtCApeCoinAddress} = + await protocolDataProvider.getReserveTokensAddresses(cApe.address); + variableDebtCApeCoin = await getVariableDebtToken( + variableDebtCApeCoinAddress + ); + console.log("paraApeStaking address:", paraApeStaking.address); + console.log("variableDebtCApeCoinAddress:", variableDebtCApeCoinAddress); + + // send extra tokens to the apestaking contract for rewards + await waitForTx( + await ape + .connect(user1.signer) + ["mint(address,uint256)"]( + apeCoinStaking.address, + parseEther("100000000000") + ) + ); + + // user4 deposit MINIMUM_LIQUIDITY to make test case easy + await mintAndValidate(ape, "1", user6); + await waitForTx( + await ape.connect(user6.signer).approve(cApe.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await cApe.connect(user6.signer).deposit(user6.address, MINIMUM_LIQUIDITY) + ); + + // user3 deposit and supply cApe to MM + await mintAndValidate(ape, "10000000", user4); + await waitForTx( + await ape.connect(user4.signer).approve(cApe.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await cApe + .connect(user4.signer) + .deposit(user4.address, parseEther("10000000")) + ); + await waitForTx( + await cApe.connect(user4.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await pool + .connect(user4.signer) + .supply(cApe.address, parseEther("10000000"), user4.address, 0) + ); + + return testEnv; + }; + + it("test BAYC + BAKC pool logic", async () => { + const { + users: [user1, user2, user3], + bayc, + bakc, + nBAYC, + nBAKC, + apeCoinStaking, + } = await loadFixture(fixture); + + await supplyAndValidate(bayc, "3", user1, true); + await supplyAndValidate(bakc, "3", user1, true); + + await waitForTx( + await nBAYC + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 2) + ); + await waitForTx( + await nBAKC + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 2) + ); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .depositPairNFT(true, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking.connect(user2.signer).depositPairNFT(true, [2], [2]) + ); + expect (await bayc.ownerOf(0)).to.be.equal(paraApeStaking.address); + expect (await bayc.ownerOf(1)).to.be.equal(paraApeStaking.address); + expect (await bayc.ownerOf(2)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(0)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(1)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(2)).to.be.equal(paraApeStaking.address); + + await waitForTx( + await paraApeStaking + .connect(user3.signer) + .stakingPairNFT(true, [0, 1, 2], [0, 1, 2]) + ); + expect((await apeCoinStaking.nftPosition(1, 0)).stakedAmount).to.be.eq(parseEther("200000")); + expect((await apeCoinStaking.nftPosition(1, 1)).stakedAmount).to.be.eq(parseEther("200000")); + expect((await apeCoinStaking.nftPosition(1, 2)).stakedAmount).to.be.eq(parseEther("200000")); + expect((await apeCoinStaking.nftPosition(3, 0)).stakedAmount).to.be.eq(parseEther("50000")); + expect((await apeCoinStaking.nftPosition(3, 1)).stakedAmount).to.be.eq(parseEther("50000")); + expect((await apeCoinStaking.nftPosition(3, 2)).stakedAmount).to.be.eq(parseEther("50000")); + expect( + await variableDebtCApeCoin.balanceOf(paraApeStaking.address) + ).to.be.closeTo(parseEther("750000"), parseEther("10")); + + await advanceTimeAndBlock(parseInt("3600")); + + await waitForTx( + await paraApeStaking + .connect(user3.signer) + .compoundPairNFT(true, [0, 1, 2], [0, 1, 2]) + ); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .claimPairNFT(true, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking + .connect(user2.signer) + .claimPairNFT(true, [2], [2]) + ); + const user1Balance = await cApe.balanceOf(user1.address); + const user2Balance = await cApe.balanceOf(user2.address); + expect(user1Balance).to.be.closeTo(user2Balance.mul(2), parseEther("10")); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .withdrawPairNFT(true, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking + .connect(user2.signer) + .withdrawPairNFT(true, [2], [2]) + ); + expect (await bayc.ownerOf(0)).to.be.equal(nBAYC.address); + expect (await bayc.ownerOf(1)).to.be.equal(nBAYC.address); + expect (await bayc.ownerOf(2)).to.be.equal(nBAYC.address); + expect (await bakc.ownerOf(0)).to.be.equal(nBAKC.address); + expect (await bakc.ownerOf(1)).to.be.equal(nBAKC.address); + expect (await bakc.ownerOf(2)).to.be.equal(nBAKC.address); + }); + + it("test MAYC + BAKC pool logic", async () => { + const { + users: [user1, user2, user3], + mayc, + bakc, + nMAYC, + nBAKC, + apeCoinStaking, + } = await loadFixture(fixture); + + await supplyAndValidate(mayc, "3", user1, true); + await supplyAndValidate(bakc, "3", user1, true); + + await waitForTx( + await nMAYC + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 2) + ); + await waitForTx( + await nBAKC + .connect(user1.signer) + .transferFrom(user1.address, user2.address, 2) + ); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .depositPairNFT(false, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking.connect(user2.signer).depositPairNFT(false, [2], [2]) + ); + expect (await mayc.ownerOf(0)).to.be.equal(paraApeStaking.address); + expect (await mayc.ownerOf(1)).to.be.equal(paraApeStaking.address); + expect (await mayc.ownerOf(2)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(0)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(1)).to.be.equal(paraApeStaking.address); + expect (await bakc.ownerOf(2)).to.be.equal(paraApeStaking.address); + + await waitForTx( + await paraApeStaking + .connect(user3.signer) + .stakingPairNFT(false, [0, 1, 2], [0, 1, 2]) + ); + expect((await apeCoinStaking.nftPosition(2, 0)).stakedAmount).to.be.eq(parseEther("100000")); + expect((await apeCoinStaking.nftPosition(2, 1)).stakedAmount).to.be.eq(parseEther("100000")); + expect((await apeCoinStaking.nftPosition(2, 2)).stakedAmount).to.be.eq(parseEther("100000")); + expect((await apeCoinStaking.nftPosition(3, 0)).stakedAmount).to.be.eq(parseEther("50000")); + expect((await apeCoinStaking.nftPosition(3, 1)).stakedAmount).to.be.eq(parseEther("50000")); + expect((await apeCoinStaking.nftPosition(3, 2)).stakedAmount).to.be.eq(parseEther("50000")); + expect( + await variableDebtCApeCoin.balanceOf(paraApeStaking.address) + ).to.be.closeTo(parseEther("450000"), parseEther("10")); + + await advanceTimeAndBlock(parseInt("3600")); + + await waitForTx( + await paraApeStaking + .connect(user3.signer) + .compoundPairNFT(false, [0, 1, 2], [0, 1, 2]) + ); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .claimPairNFT(false, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking + .connect(user2.signer) + .claimPairNFT(false, [2], [2]) + ); + const user1Balance = await cApe.balanceOf(user1.address); + const user2Balance = await cApe.balanceOf(user2.address); + expect(user1Balance).to.be.closeTo(user2Balance.mul(2), parseEther("10")); + + await waitForTx( + await paraApeStaking + .connect(user1.signer) + .withdrawPairNFT(false, [0, 1], [0, 1]) + ); + await waitForTx( + await paraApeStaking + .connect(user2.signer) + .withdrawPairNFT(false, [2], [2]) + ); + expect (await mayc.ownerOf(0)).to.be.equal(nMAYC.address); + expect (await mayc.ownerOf(1)).to.be.equal(nMAYC.address); + expect (await mayc.ownerOf(2)).to.be.equal(nMAYC.address); + expect (await bakc.ownerOf(0)).to.be.equal(nBAKC.address); + expect (await bakc.ownerOf(1)).to.be.equal(nBAKC.address); + expect (await bakc.ownerOf(2)).to.be.equal(nBAKC.address); + }); +});