From dffd693298e90c458b745e766f834bccbd8de059 Mon Sep 17 00:00:00 2001 From: zhoujia6139 Date: Wed, 15 Nov 2023 13:02:48 +0800 Subject: [PATCH] chore: ape coin staking voting --- contracts/misc/ParaXApeCoinStakingVoting.sol | 80 +++++ helpers/contracts-deployments.ts | 18 + helpers/contracts-getters.ts | 12 + helpers/hardhat-constants.ts | 3 +- helpers/types.ts | 1 + scripts/upgrade/ntoken.ts | 6 +- test/_apecoin_staking_voting.spec.ts | 342 +++++++++++++++++++ 7 files changed, 458 insertions(+), 4 deletions(-) create mode 100644 contracts/misc/ParaXApeCoinStakingVoting.sol create mode 100644 test/_apecoin_staking_voting.spec.ts diff --git a/contracts/misc/ParaXApeCoinStakingVoting.sol b/contracts/misc/ParaXApeCoinStakingVoting.sol new file mode 100644 index 00000000..8cc472df --- /dev/null +++ b/contracts/misc/ParaXApeCoinStakingVoting.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; +import {INToken} from "../interfaces/INToken.sol"; +import "../dependencies/openzeppelin/contracts//IERC20.sol"; +import "../dependencies/yoga-labs/ApeCoinStaking.sol"; + +contract ParaXApeCoinStakingVoting { + ApeCoinStaking immutable apeCoinStaking; + IERC20 immutable cApe; + INToken immutable nBAYC; + INToken immutable nMAYC; + INToken immutable nBAKC; + + uint256 constant BAYC_POOL_ID = 1; + uint256 constant MAYC_POOL_ID = 2; + uint256 constant BAKC_POOL_ID = 3; + + constructor( + address _cApe, + address _apeCoinStaking, + address _nBAYC, + address _nMAYC, + address _nBAKC + ) { + cApe = IERC20(_cApe); + apeCoinStaking = ApeCoinStaking(_apeCoinStaking); + nBAYC = INToken(_nBAYC); + nMAYC = INToken(_nMAYC); + nBAKC = INToken(_nBAKC); + } + + /** + * @notice Returns a vote count across all pools in the ApeCoinStaking contract for a given address + * @param userAddress The address to return votes for + */ + function getVotes(address userAddress) public view returns (uint256 votes) { + votes = getCApeVotes(userAddress); + votes += getVotesInAllNftPool(userAddress); + } + + function getCApeVotes( + address userAddress + ) public view returns (uint256 votes) { + votes = cApe.balanceOf(userAddress); + } + + function getVotesInAllNftPool( + address userAddress + ) public view returns (uint256 votes) { + votes = getVotesForNToken(nBAYC, BAYC_POOL_ID, userAddress); + votes += getVotesForNToken(nMAYC, MAYC_POOL_ID, userAddress); + votes += getVotesForNToken(nBAKC, BAKC_POOL_ID, userAddress); + } + + function getVotesForNToken( + INToken ntoken, + uint256 poolId, + address userAddress + ) public view returns (uint256 votes) { + uint256 balance = ntoken.balanceOf(userAddress); + if (balance == 0) { + return 0; + } + + for (uint256 i = 0; i < balance; i++) { + uint256 tokenId = ntoken.tokenOfOwnerByIndex(userAddress, i); + + (uint256 stakedAmount, ) = apeCoinStaking.nftPosition( + poolId, + tokenId + ); + uint256 pendingReward = apeCoinStaking.pendingRewards( + poolId, + address(ntoken), + tokenId + ); + votes += (pendingReward + stakedAmount); + } + } +} diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index b825dbb2..8df742e5 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -156,6 +156,7 @@ import { PoolAAPositionMover__factory, PoolBorrowAndStake__factory, PoolBorrowAndStake, + ParaXApeCoinStakingVoting, } from "../types"; import { getACLManager, @@ -3346,6 +3347,23 @@ export const deployDelegationAwarePToken = async ( return instance; }; +export const deployApeCoinStakingVoting = async ( + apeCoinStaking: tEthereumAddress, + cApe: tEthereumAddress, + nBAYC: tEthereumAddress, + nMAYC: tEthereumAddress, + nBAKC: tEthereumAddress, + verify?: boolean +) => { + return withSaveAndVerify( + await getContractFactory("ParaXApeCoinStakingVoting"), + eContractid.ParaXApeCoinStakingVoting, + [apeCoinStaking, cApe, nBAYC, nMAYC, nBAKC], + verify, + false + ) as Promise; +}; + export const deployMockVariableDebtToken = async ( args: [ tEthereumAddress, diff --git a/helpers/contracts-getters.ts b/helpers/contracts-getters.ts index 693b9916..9b722132 100644 --- a/helpers/contracts-getters.ts +++ b/helpers/contracts-getters.ts @@ -100,6 +100,7 @@ import { Account__factory, AccountFactory__factory, AccountRegistry__factory, + ParaXApeCoinStakingVoting__factory, } from "../types"; import { getEthersSigners, @@ -1348,6 +1349,17 @@ export const getAccountFactory = async (address?: tEthereumAddress) => await getFirstSigner() ); +export const getApeCoinStakingVoting = async (address?: tEthereumAddress) => + await ParaXApeCoinStakingVoting__factory.connect( + address || + ( + await getDb() + .get(`${eContractid.ParaXApeCoinStakingVoting}.${DRE.network.name}`) + .value() + ).address, + await getFirstSigner() + ); + //////////////////////////////////////////////////////////////////////////////// // MOCK //////////////////////////////////////////////////////////////////////////////// diff --git a/helpers/hardhat-constants.ts b/helpers/hardhat-constants.ts index ef66a0b0..bb3c018c 100644 --- a/helpers/hardhat-constants.ts +++ b/helpers/hardhat-constants.ts @@ -498,5 +498,4 @@ export const XTOKEN_TYPE_UPGRADE_WHITELIST = .split(/\s?,\s?/) .map((x) => +x); export const XTOKEN_SYMBOL_UPGRADE_WHITELIST = - process.env.XTOKEN_SYMBOL_UPGRADE_WHITELIST?.trim() - .split(/\s?,\s?/); + process.env.XTOKEN_SYMBOL_UPGRADE_WHITELIST?.trim().split(/\s?,\s?/); diff --git a/helpers/types.ts b/helpers/types.ts index 5e6c1842..62530dbe 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -292,6 +292,7 @@ export enum eContractid { MockBendDaoLendPool = "MockBendDaoLendPool", PositionMoverLogic = "PositionMoverLogic", PoolPositionMoverImpl = "PoolPositionMoverImpl", + ParaXApeCoinStakingVoting = "ParaXApeCoinStakingVoting", Account = "Account", AccountFactory = "AccountFactory", AccountProxy = "AccountProxy", diff --git a/scripts/upgrade/ntoken.ts b/scripts/upgrade/ntoken.ts index ba3ca36f..21713a50 100644 --- a/scripts/upgrade/ntoken.ts +++ b/scripts/upgrade/ntoken.ts @@ -76,12 +76,14 @@ export const upgradeNToken = async (verify = false) => { continue; } - if (XTOKEN_SYMBOL_UPGRADE_WHITELIST && !XTOKEN_SYMBOL_UPGRADE_WHITELIST.includes(symbol)) { + if ( + XTOKEN_SYMBOL_UPGRADE_WHITELIST && + !XTOKEN_SYMBOL_UPGRADE_WHITELIST.includes(symbol) + ) { console.log(symbol + "not in XTOKEN_SYMBOL_UPGRADE_WHITELIST, skip..."); continue; } - if (xTokenType == XTokenType.NTokenBAYC) { if (!nTokenBAYCImplementationAddress) { console.log("deploy NTokenBAYC implementation"); diff --git a/test/_apecoin_staking_voting.spec.ts b/test/_apecoin_staking_voting.spec.ts new file mode 100644 index 00000000..d6b1b24c --- /dev/null +++ b/test/_apecoin_staking_voting.spec.ts @@ -0,0 +1,342 @@ +import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; +import {expect} from "chai"; +import {MAX_UINT_AMOUNT, ONE_ADDRESS} from "../helpers/constants"; +import { + getApeCoinStakingVoting, + getAutoCompoundApe, + getPToken, + getPTokenSApe, + getVariableDebtToken, +} from "../helpers/contracts-getters"; +import {convertToCurrencyDecimals} from "../helpers/contracts-helpers"; +import {waitForTx} from "../helpers/misc-utils"; +import {VariableDebtToken, PTokenSApe, PToken, AutoCompoundApe} from "../types"; +import {TestEnv} from "./helpers/make-suite"; +import {testEnvFixture} from "./helpers/setup-env"; + +import { + changePriceAndValidate, + changeSApePriceAndValidate, + mintAndValidate, + supplyAndValidate, +} from "./helpers/validated-steps"; +import {parseEther} from "ethers/lib/utils"; +import {deployApeCoinStakingVoting} from "../helpers/contracts-deployments"; +import {BigNumberish} from "ethers"; + +describe("APE Coin Staking Test", () => { + let testEnv: TestEnv; + let cApe: AutoCompoundApe; + const sApeAddress = ONE_ADDRESS; + const InitialNTokenApeBalance = parseEther("100"); + + const fixture = async () => { + testEnv = await loadFixture(testEnvFixture); + const { + ape, + mayc, + bayc, + users, + bakc, + pool, + apeCoinStaking, + nMAYC, + nBAYC, + nBAKC, + } = testEnv; + const user1 = users[0]; + const depositor = users[1]; + const user4 = users[5]; + + cApe = await getAutoCompoundApe(); + + await supplyAndValidate(ape, "20000", depositor, true); + await changePriceAndValidate(ape, "0.001"); + await changePriceAndValidate(cApe, "0.001"); + await changeSApePriceAndValidate(sApeAddress, "0.001"); + + await changePriceAndValidate(mayc, "50"); + await changePriceAndValidate(bayc, "50"); + + await waitForTx(await bakc["mint(uint256,address)"]("2", user1.address)); + + await waitForTx( + await ape.connect(user1.signer).approve(pool.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await bakc.connect(user1.signer).setApprovalForAll(pool.address, true) + ); + + // send extra tokens to the apestaking contract for rewards + await waitForTx( + await ape + .connect(user1.signer) + ["mint(address,uint256)"]( + apeCoinStaking.address, + parseEther("100000000000") + ) + ); + + // send extra tokens to the nToken contract for testing ape balance check + await waitForTx( + await ape + .connect(user1.signer) + ["mint(address,uint256)"](nMAYC.address, InitialNTokenApeBalance) + ); + await waitForTx( + await ape + .connect(user1.signer) + ["mint(address,uint256)"](nBAYC.address, InitialNTokenApeBalance) + ); + + await mintAndValidate(ape, "1", user4); + await waitForTx( + await ape.connect(user4.signer).approve(cApe.address, MAX_UINT_AMOUNT) + ); + // user4 deposit MINIMUM_LIQUIDITY to make test case easy + const MINIMUM_LIQUIDITY = await cApe.MINIMUM_LIQUIDITY(); + await waitForTx( + await cApe.connect(user4.signer).deposit(user4.address, MINIMUM_LIQUIDITY) + ); + + await deployApeCoinStakingVoting( + cApe.address, + apeCoinStaking.address, + nBAYC.address, + nMAYC.address, + nBAKC.address + ); + + return testEnv; + }; + + it("test with cape position", async () => { + const { + users: [user1], + ape, + } = await loadFixture(fixture); + + await mintAndValidate(ape, "15000", user1); + await waitForTx( + await ape.connect(user1.signer).approve(cApe.address, MAX_UINT_AMOUNT) + ); + await waitForTx( + await cApe + .connect(user1.signer) + .deposit(user1.address, parseEther("15000")) + ); + + const voting = await getApeCoinStakingVoting(); + expect(parseEther("15000")).equal(await voting.getVotes(user1.address)); + expect(parseEther("15000")).equal(await voting.getCApeVotes(user1.address)); + expect(0).equal(await voting.getVotesInAllNftPool(user1.address)); + }); + + it("test with bayc and bakc position0", async () => { + const { + users: [user1], + ape, + bayc, + bakc, + pool, + } = await loadFixture(fixture); + + await supplyAndValidate(bayc, "1", user1, true); + await mintAndValidate(ape, "15000", user1); + + const amount1 = await convertToCurrencyDecimals(ape.address, "7000"); + const amount2 = await convertToCurrencyDecimals(ape.address, "8000"); + const amount = await convertToCurrencyDecimals(ape.address, "15000"); + expect( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: 0, + cashAmount: amount, + }, + [{tokenId: 0, amount: amount1}], + [{mainTokenId: 0, bakcTokenId: 0, amount: amount2}] + ) + ); + + const voting = await getApeCoinStakingVoting(); + expect(amount1).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount1).equal(await voting.getVotesInAllNftPool(user1.address)); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bakc.address, + [ + { + tokenId: 0, + useAsCollateral: false, + }, + ], + user1.address, + 0 + ) + ); + expect(amount).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount).equal(await voting.getVotesInAllNftPool(user1.address)); + }); + + it("test with bayc and bakc position1", async () => { + const { + users: [user1], + ape, + bayc, + bakc, + pool, + } = await loadFixture(fixture); + + await supplyAndValidate(bayc, "1", user1, true); + await mintAndValidate(ape, "7000", user1); + + const amount1 = await convertToCurrencyDecimals(ape.address, "7000"); + // 50 * 0.3250 + 7000 * 0.001 * 0.2 = 17.65 + // 17.65 / 0.001 = 17650 + const amount2 = await convertToCurrencyDecimals(ape.address, "8000"); + const amount = await convertToCurrencyDecimals(ape.address, "15000"); + expect( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: bayc.address, + borrowAsset: ape.address, + borrowAmount: amount2, + cashAmount: amount1, + }, + [{tokenId: 0, amount: amount1}], + [{mainTokenId: 0, bakcTokenId: 0, amount: amount2}] + ) + ); + + const voting = await getApeCoinStakingVoting(); + expect(amount1).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount1).equal(await voting.getVotesInAllNftPool(user1.address)); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bakc.address, + [ + { + tokenId: 0, + useAsCollateral: false, + }, + ], + user1.address, + 0 + ) + ); + expect(amount).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount).equal(await voting.getVotesInAllNftPool(user1.address)); + }); + + it("test with mayc and bakc position0", async () => { + const { + users: [user1], + ape, + mayc, + bakc, + pool, + } = await loadFixture(fixture); + + await supplyAndValidate(mayc, "1", user1, true); + await mintAndValidate(ape, "15000", user1); + + const amount1 = await convertToCurrencyDecimals(ape.address, "7000"); + const amount2 = await convertToCurrencyDecimals(ape.address, "8000"); + const amount = await convertToCurrencyDecimals(ape.address, "15000"); + expect( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: mayc.address, + borrowAsset: ape.address, + borrowAmount: 0, + cashAmount: amount, + }, + [{tokenId: 0, amount: amount1}], + [{mainTokenId: 0, bakcTokenId: 0, amount: amount2}] + ) + ); + + const voting = await getApeCoinStakingVoting(); + expect(amount1).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount1).equal(await voting.getVotesInAllNftPool(user1.address)); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bakc.address, + [ + { + tokenId: 0, + useAsCollateral: false, + }, + ], + user1.address, + 0 + ) + ); + expect(amount).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount).equal(await voting.getVotesInAllNftPool(user1.address)); + }); + + it("test with mayc and bakc position1", async () => { + const { + users: [user1], + ape, + mayc, + bakc, + pool, + } = await loadFixture(fixture); + + await supplyAndValidate(mayc, "1", user1, true); + await mintAndValidate(ape, "7000", user1); + + const amount1 = await convertToCurrencyDecimals(ape.address, "7000"); + // 50 * 0.3250 + 7000 * 0.001 * 0.2 = 17.65 + // 17.65 / 0.001 = 17650 + const amount2 = await convertToCurrencyDecimals(ape.address, "8000"); + const amount = await convertToCurrencyDecimals(ape.address, "15000"); + expect( + await pool.connect(user1.signer).borrowApeAndStake( + { + nftAsset: mayc.address, + borrowAsset: ape.address, + borrowAmount: amount2, + cashAmount: amount1, + }, + [{tokenId: 0, amount: amount1}], + [{mainTokenId: 0, bakcTokenId: 0, amount: amount2}] + ) + ); + + const voting = await getApeCoinStakingVoting(); + expect(amount1).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount1).equal(await voting.getVotesInAllNftPool(user1.address)); + + await waitForTx( + await pool.connect(user1.signer).supplyERC721( + bakc.address, + [ + { + tokenId: 0, + useAsCollateral: false, + }, + ], + user1.address, + 0 + ) + ); + expect(amount).equal(await voting.getVotes(user1.address)); + expect(0).equal(await voting.getCApeVotes(user1.address)); + expect(amount).equal(await voting.getVotesInAllNftPool(user1.address)); + }); +});