From 2fef92cd310d8ba4944d514af71a21980ac3cac9 Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Wed, 19 Apr 2023 17:36:36 -0700 Subject: [PATCH 1/6] feat: otherdeed flashclaim initial impl --- contracts/interfaces/IPoolPositionMover.sol | 7 + contracts/interfaces/IVesselClaim.sol | 13 ++ contracts/mocks/MockVessel.sol | 48 +++++++ contracts/mocks/tokens/MintableERC721.sol | 6 + .../libraries/logic/PositionMoverLogic.sol | 132 ++++++++++++++++++ .../protocol/libraries/types/DataTypes.sol | 13 ++ contracts/protocol/pool/PoolPositionMover.sol | 43 +++++- helpers/contracts-deployments.ts | 49 ++++++- helpers/types.ts | 13 ++ market-config/index.ts | 3 + market-config/mocks.ts | 5 +- test/_xtoken_ntoken_otherdeed.spec.ts | 117 +++++++++++++++- test/helpers/make-suite.ts | 53 ++++++- test/helpers/validated-steps.ts | 2 +- 14 files changed, 491 insertions(+), 13 deletions(-) create mode 100644 contracts/interfaces/IVesselClaim.sol create mode 100644 contracts/mocks/MockVessel.sol diff --git a/contracts/interfaces/IPoolPositionMover.sol b/contracts/interfaces/IPoolPositionMover.sol index ba84b4443..7e0a49be4 100644 --- a/contracts/interfaces/IPoolPositionMover.sol +++ b/contracts/interfaces/IPoolPositionMover.sol @@ -8,4 +8,11 @@ pragma solidity 0.8.10; **/ interface IPoolPositionMover { function movePositionFromBendDAO(uint256[] calldata loanIds) external; + + function claimOtherExpandedAndSupply( + uint256[] calldata otherdeedIds, + uint256[] calldata kodaIds, + uint256[] calldata kodaOtherdeedIds, + bytes32[][] calldata merkleProofs + ) external; } diff --git a/contracts/interfaces/IVesselClaim.sol b/contracts/interfaces/IVesselClaim.sol new file mode 100644 index 000000000..f2cd02e10 --- /dev/null +++ b/contracts/interfaces/IVesselClaim.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +interface IVesselClaim { + function claimVesselsAndKodas( + uint256[] calldata otherdeedIds, + uint256[] calldata kodaIds, + uint256[] calldata kodaOtherdeedIds, + bytes32[][] calldata merkleProofs + ) external; + + function claimVessels(uint256[] calldata otherdeedIds) external; +} diff --git a/contracts/mocks/MockVessel.sol b/contracts/mocks/MockVessel.sol new file mode 100644 index 000000000..589fa340f --- /dev/null +++ b/contracts/mocks/MockVessel.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity 0.8.10; + +import {MintableERC721} from "./tokens/MintableERC721.sol"; + +contract MockVessel is MintableERC721 { + + MintableERC721 immutable KODA; + MintableERC721 immutable OTHRE; + MintableERC721 immutable OTHR; + + constructor(MintableERC721 otherdeed, MintableERC721 kodaAddress, MintableERC721 otherdeedExpAddress) MintableERC721("VESSEL", "VSL", "") { + OTHR = otherdeed; + KODA = kodaAddress; + OTHRE = otherdeedExpAddress; + } + + function claimVesselsAndKodas( + uint256[] calldata otherdeedIds, + uint256[] calldata kodaIds, + uint256[] calldata kodaOtherdeedIds, + bytes32[][] calldata merkleProofs + ) + external { + + for (uint256 index = 0; index < otherdeedIds.length; index++) { + mintTokenId(msg.sender, otherdeedIds[index]); + OTHRE.mintTokenId(msg.sender, otherdeedIds[index]); + + OTHR.transferFrom(msg.sender, 0x000000000000000000000000000000000000dEaD, otherdeedIds[index]); + } + + for (uint256 index = 0; index < kodaIds.length; index++) { + KODA.mintTokenId(msg.sender, kodaIds[index]); + } + } + + + function claimVessels(uint256[] calldata otherdeedIds) + external { + for (uint256 index = 0; index < otherdeedIds.length; index++) { + mintTokenId(msg.sender, otherdeedIds[index]); + OTHRE.mintTokenId(msg.sender, otherdeedIds[index]); + + OTHR.transferFrom(msg.sender, 0x000000000000000000000000000000000000dEaD, otherdeedIds[index]); + } + } +} \ No newline at end of file diff --git a/contracts/mocks/tokens/MintableERC721.sol b/contracts/mocks/tokens/MintableERC721.sol index 62b297f14..9784e4f92 100644 --- a/contracts/mocks/tokens/MintableERC721.sol +++ b/contracts/mocks/tokens/MintableERC721.sol @@ -88,6 +88,12 @@ contract MintableERC721 is Context, ERC721Enumerable { } } + function mintTokenId(address to, uint256 _tokenId) public virtual { + // We cannot just use balanceOf to create the new tokenId because tokens + // can be burned (destroyed), so we need a separate counter. + _mint(to, _tokenId); + } + function setBaseURI(string memory baseTokenURI) external { _baseTokenURI = baseTokenURI; } diff --git a/contracts/protocol/libraries/logic/PositionMoverLogic.sol b/contracts/protocol/libraries/logic/PositionMoverLogic.sol index 3afb428f8..05efb9350 100644 --- a/contracts/protocol/libraries/logic/PositionMoverLogic.sol +++ b/contracts/protocol/libraries/logic/PositionMoverLogic.sol @@ -2,16 +2,20 @@ pragma solidity 0.8.10; import {INToken} from "../../../interfaces/INToken.sol"; +import {IVesselClaim} from "../../../interfaces/IVesselClaim.sol"; import {IPoolAddressesProvider} from "../../../interfaces/IPoolAddressesProvider.sol"; import {DataTypes} from "../types/DataTypes.sol"; import {IPToken} from "../../../interfaces/IPToken.sol"; import {IERC20} from "../../../dependencies/openzeppelin/contracts/IERC20.sol"; +import {IERC721} from "../../../dependencies/openzeppelin/contracts/IERC721.sol"; + import {Errors} from "../helpers/Errors.sol"; import {ValidationLogic} from "./ValidationLogic.sol"; import {SupplyLogic} from "./SupplyLogic.sol"; import {BorrowLogic} from "./BorrowLogic.sol"; import {ReserveLogic} from "./ReserveLogic.sol"; import {ReserveConfiguration} from "../configuration/ReserveConfiguration.sol"; +import {UserConfiguration} from "../configuration/UserConfiguration.sol"; import {Address} from "../../../dependencies/openzeppelin/contracts/Address.sol"; import {ILendPoolLoan} from "../../../dependencies/benddao/contracts/interfaces/ILendPoolLoan.sol"; import {ILendPool} from "../../../dependencies/benddao/contracts/interfaces/ILendPool.sol"; @@ -25,6 +29,7 @@ import {BDaoDataTypes} from "../../../dependencies/benddao/contracts/libraries/t library PositionMoverLogic { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; using ReserveLogic for DataTypes.ReserveData; + using UserConfiguration for DataTypes.UserConfigurationMap; struct PositionMoverVars { address weth; @@ -35,6 +40,10 @@ library PositionMoverLogic { } event PositionMoved(address asset, uint256 tokenId, address user); + event ReserveUsedAsCollateralDisabled( + address indexed reserve, + address indexed user + ); function executeMovePositionFromBendDAO( DataTypes.PoolStorage storage ps, @@ -148,4 +157,127 @@ library PositionMoverLogic { }) ); } + + function executeClaimOtherdeedAndSupply( + mapping(address => DataTypes.ReserveData) storage reservesData, + mapping(uint256 => address) storage reservesList, + DataTypes.UserConfigurationMap storage userConfig, + DataTypes.OtherdeedClaimParams memory params + ) external returns (uint256) { + DataTypes.ReserveData storage reserve = reservesData[params.otherdeed]; + DataTypes.ReserveCache memory reserveCache = reserve.cache(); + + ValidationLogic.validateWithdrawERC721( + reservesData, + reserveCache, + params.otherdeed, + params.otherdeedIds + ); + + DataTypes.TimeLockParams memory timeLockParams; + + ( + uint64 oldCollateralizedBalance, + uint64 newCollateralizedBalance + ) = INToken(reserveCache.xTokenAddress).burn( + msg.sender, + address(this), + params.otherdeedIds, + timeLockParams + ); + + bool isWithdrawCollateral = (newCollateralizedBalance < + oldCollateralizedBalance); + + if (isWithdrawCollateral) { + if (newCollateralizedBalance == 0) { + userConfig.setUsingAsCollateral(reserve.id, false); + emit ReserveUsedAsCollateralDisabled( + params.otherdeed, + msg.sender + ); + } + } + + IERC721(params.otherdeed).setApprovalForAll(params.vessel, true); + + if (params.kodaIds.length == 0) { + IVesselClaim(params.vessel).claimVessels(params.otherdeedIds); + } else { + IVesselClaim(params.vessel).claimVesselsAndKodas( + params.otherdeedIds, + params.kodaIds, + params.kodaOtherdeedIds, + params.merkleProofs + ); + } + + IERC721(params.otherdeed).setApprovalForAll(params.vessel, false); + + SupplyLogic.executeSupplyERC721( + reservesData, + userConfig, + DataTypes.ExecuteSupplyERC721Params({ + asset: params.otherdeedExpanded, + tokenData: _getTokenData(params.otherdeedIds), + onBehalfOf: msg.sender, + payer: address(this), + referralCode: 0x0 + }) + ); + + SupplyLogic.executeSupplyERC721( + reservesData, + userConfig, + DataTypes.ExecuteSupplyERC721Params({ + asset: params.vessel, + tokenData: _getTokenData(params.otherdeedIds), + onBehalfOf: msg.sender, + payer: address(this), + referralCode: 0x0 + }) + ); + + if (params.kodaIds.length != 0) { + SupplyLogic.executeSupplyERC721( + reservesData, + userConfig, + DataTypes.ExecuteSupplyERC721Params({ + asset: params.koda, + tokenData: _getTokenData(params.kodaIds), + onBehalfOf: msg.sender, + payer: address(this), + referralCode: 0x0 + }) + ); + } + + if (isWithdrawCollateral) { + if (userConfig.isBorrowingAny()) { + ValidationLogic.validateHFAndLtvERC721( + reservesData, + reservesList, + userConfig, + params.otherdeed, + params.otherdeedIds, + msg.sender, + params.reservesCount, + params.oracle + ); + } + } + } + + function _getTokenData(uint256[] memory tokenIds) + internal + returns (DataTypes.ERC721SupplyParams[] memory tokenData) + { + tokenData = new DataTypes.ERC721SupplyParams[](tokenIds.length); + for (uint256 index = 0; index < tokenIds.length; index++) { + tokenData[index] = DataTypes.ERC721SupplyParams({ + tokenId: tokenIds[index], + useAsCollateral: true + }); + } + } } diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index a527d07e5..a234587b7 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -193,6 +193,19 @@ library DataTypes { address oracle; } + struct OtherdeedClaimParams { + address otherdeed; + address otherdeedExpanded; + address koda; + address vessel; + uint256[] otherdeedIds; + uint256[] kodaIds; + uint256[] kodaOtherdeedIds; + bytes32[][] merkleProofs; + uint256 reservesCount; + address oracle; + } + struct ExecuteDecreaseUniswapV3LiquidityParams { address user; address asset; diff --git a/contracts/protocol/pool/PoolPositionMover.sol b/contracts/protocol/pool/PoolPositionMover.sol index 3b53132fe..701b8ca4e 100644 --- a/contracts/protocol/pool/PoolPositionMover.sol +++ b/contracts/protocol/pool/PoolPositionMover.sol @@ -26,14 +26,28 @@ contract PoolPositionMover is ILendPool internal immutable BENDDAO_LEND_POOL; uint256 internal constant POOL_REVISION = 149; + address immutable OTHERDEED; + address immutable OTHERDEED_EXPANDED; + address immutable KODA; + address immutable VESSEL; + constructor( IPoolAddressesProvider addressProvider, ILendPoolLoan benddaoLendPoolLoan, - ILendPool benddaoLendPool + ILendPool benddaoLendPool, + address otherdeed, + address otherdeedExpanded, + address koda, + address vessel ) { ADDRESSES_PROVIDER = addressProvider; BENDDAO_LEND_POOL_LOAN = benddaoLendPoolLoan; BENDDAO_LEND_POOL = benddaoLendPool; + + OTHERDEED = otherdeed; + OTHERDEED_EXPANDED = otherdeedExpanded; + KODA = koda; + VESSEL = vessel; } function movePositionFromBendDAO(uint256[] calldata loanIds) @@ -51,6 +65,33 @@ contract PoolPositionMover is ); } + function claimOtherExpandedAndSupply( + uint256[] calldata otherdeedIds, + uint256[] calldata kodaIds, + uint256[] calldata kodaOtherdeedIds, + bytes32[][] calldata merkleProofs + ) external nonReentrant { + DataTypes.PoolStorage storage ps = poolStorage(); + + PositionMoverLogic.executeClaimOtherdeedAndSupply( + ps._reserves, + ps._reservesList, + ps._usersConfig[msg.sender], + DataTypes.OtherdeedClaimParams({ + otherdeed: OTHERDEED, + otherdeedExpanded: OTHERDEED_EXPANDED, + koda: KODA, + vessel: VESSEL, + otherdeedIds: otherdeedIds, + kodaIds: kodaIds, + kodaOtherdeedIds: kodaOtherdeedIds, + merkleProofs: merkleProofs, + reservesCount: ps._reservesCount, + oracle: ADDRESSES_PROVIDER.getPriceOracle() + }) + ); + } + function getRevision() internal pure virtual override returns (uint256) { return POOL_REVISION; } diff --git a/helpers/contracts-deployments.ts b/helpers/contracts-deployments.ts index f61a8dee9..5b3306e1c 100644 --- a/helpers/contracts-deployments.ts +++ b/helpers/contracts-deployments.ts @@ -290,6 +290,8 @@ import { PoolPositionMover, PositionMoverLogic, PositionMoverLogic__factory, + MockVessel__factory, + MockVessel, } from "../types"; import {MockContract} from "ethereum-waffle"; import { @@ -877,7 +879,15 @@ export const deployPoolComponents = async ( await getFirstSigner() ), eContractid.PoolPositionMoverImpl, - [provider, bendDaoLendPool.address, bendDaoLendPool.address], + [ + provider, + bendDaoLendPool.address, + bendDaoLendPool.address, + allTokens.OTHR.address, + allTokens.OTHREXP.address, + allTokens.KODA.address, + allTokens.VSL.address, + ], verify, false, positionMoverLibrary, @@ -1424,6 +1434,22 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { ], verify ); + + tokens[ERC721TokenContractId.OTHREXP] = await deployMintableERC721( + [ERC721TokenContractId.OTHREXP, ERC721TokenContractId.OTHREXP, ""], + verify + ); + tokens[ERC721TokenContractId.KODA] = await deployMintableERC721( + [ERC721TokenContractId.KODA, ERC721TokenContractId.KODA, ""], + verify + ); + + tokens[ERC721TokenContractId.VSL] = await deployMockVessel( + tokens[tokenSymbol].address, + tokens[ERC721TokenContractId.KODA].address, + tokens[ERC721TokenContractId.OTHREXP].address + ); + continue; } @@ -1491,6 +1517,14 @@ export const deployAllERC721Tokens = async (verify?: boolean) => { continue; } + if ( + tokenSymbol === ERC721TokenContractId.OTHREXP || + tokenSymbol === ERC721TokenContractId.KODA || + tokenSymbol === ERC721TokenContractId.VSL + ) { + continue; + } + tokens[tokenSymbol] = await deployMintableERC721( [tokenSymbol, tokenSymbol, ""], verify @@ -3326,6 +3360,19 @@ export const deployMockBendDaoLendPool = async (weth, verify?: boolean) => [weth], verify ); + +export const deployMockVessel = async ( + otherdeed: tEthereumAddress, + kodaAddress: tEthereumAddress, + otherdeedExpAddress: tEthereumAddress, + verify?: boolean +) => + withSaveAndVerify( + new MockVessel__factory(await getFirstSigner()), + "VSL", + [otherdeed, kodaAddress, otherdeedExpAddress], + verify + ) as Promise; //////////////////////////////////////////////////////////////////////////////// // PLS ONLY APPEND MOCK CONTRACTS HERE! //////////////////////////////////////////////////////////////////////////////// diff --git a/helpers/types.ts b/helpers/types.ts index 9cfaf9c0a..9de699b94 100644 --- a/helpers/types.ts +++ b/helpers/types.ts @@ -278,6 +278,7 @@ export enum eContractid { MockBendDaoLendPool = "MockBendDaoLendPool", PositionMoverLogic = "PositionMoverLogic", PoolPositionMoverImpl = "PoolPositionMoverImpl", + MockVessel = "MockVessel", } /* @@ -465,6 +466,9 @@ export interface iAssetBase { OTHR: T; CLONEX: T; BAKC: T; + OTHREXP: T; + KODA: T; + VSL: T; } export type iAssetsWithoutETH = Omit, "ETH">; @@ -500,6 +504,9 @@ export type iParaSpacePoolAssets = Pick< | "MOONBIRD" | "MEEBITS" | "OTHR" + | "OTHREXP" + | "VSL" + | "KODA" | "BAKC" >; @@ -548,6 +555,9 @@ export enum ERC721TokenContractId { SEWER = "SEWER", PPG = "PPG", SFVLDR = "SFVLDR", + VSL = "VSL", + KODA = "KODA", + OTHREXP = "OTHREXP", } export enum NTokenContractId { @@ -562,6 +572,9 @@ export enum NTokenContractId { nPPG = "nPPG", nOTHR = "nOTHR", nSFVLDR = "nSFVLDR", + nOTHREXP = "nOTHREXP", + nVSL = "nVSL", + nKODA = "nKODA", } export enum PTokenContractId { diff --git a/market-config/index.ts b/market-config/index.ts index bd4ab8414..93ad5ba18 100644 --- a/market-config/index.ts +++ b/market-config/index.ts @@ -132,6 +132,9 @@ export const HardhatParaSpaceConfig: IParaSpaceConfiguration = { MEEBITS: strategyMeebits, AZUKI: strategyAzuki, OTHR: strategyOthr, + OTHREXP: strategyOthr, + VSL: strategyOthr, + KODA: strategyOthr, CLONEX: strategyClonex, UniswapV3: strategyUniswapV3, sAPE: strategySAPE, diff --git a/market-config/mocks.ts b/market-config/mocks.ts index 49fbfd76d..3ee9ba4ad 100644 --- a/market-config/mocks.ts +++ b/market-config/mocks.ts @@ -36,7 +36,10 @@ export const MOCK_CHAINLINK_AGGREGATORS_PRICES = { MOONBIRD: parseEther("0.02").toString(), MEEBITS: parseEther("22").toString(), AZUKI: parseEther("21").toString(), - OTHR: parseEther("25").toString(), + OTHR: parseEther("2").toString(), + OTHREXP: parseEther("1").toString(), + VSL: parseEther("0.5").toString(), + KODA: parseEther("10").toString(), CLONEX: parseEther("27").toString(), BAKC: parseEther("6").toString(), SEWER: parseEther("2").toString(), diff --git a/test/_xtoken_ntoken_otherdeed.spec.ts b/test/_xtoken_ntoken_otherdeed.spec.ts index 97036a5df..9a21a093b 100644 --- a/test/_xtoken_ntoken_otherdeed.spec.ts +++ b/test/_xtoken_ntoken_otherdeed.spec.ts @@ -4,32 +4,47 @@ import {TestEnv} from "./helpers/make-suite"; import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {testEnvFixture} from "./helpers/setup-env"; import {HotWalletProxy} from "../types"; -import {getHotWalletProxy} from "../helpers/contracts-getters"; +import { + getERC721, + getHotWalletProxy, + getMintableERC721, +} from "../helpers/contracts-getters"; import {ZERO_ADDRESS} from "../helpers/constants"; +import {supplyAndValidate} from "./helpers/validated-steps"; describe("Otherdeed nToken warmwallet delegation", () => { let testEnv: TestEnv; let hotWallet: HotWalletProxy; - before(async () => { + before(async () => {}); + + beforeEach(async () => { testEnv = await loadFixture(testEnvFixture); - hotWallet = await getHotWalletProxy(); - }); - it("Admin can set hot wallet from NToken", async () => { const { users: [user1], - nOTHR, + OTHR, poolAdmin, + nOTHR, } = testEnv; - const currentTime = await timeLatest(); + hotWallet = await getHotWalletProxy(); + await supplyAndValidate(OTHR, "5", user1, true); + const currentTime = await timeLatest(); await expect( await nOTHR .connect(poolAdmin.signer) .setHotWallet(user1.address, currentTime.add(3600), true) ); + }); + + it("Admin can set hot wallet from NToken", async () => { + const { + users: [user1], + nOTHR, + poolAdmin, + } = testEnv; await expect(await hotWallet.getHotWallet(nOTHR.address)).to.be.eq( user1.address @@ -88,4 +103,92 @@ describe("Otherdeed nToken warmwallet delegation", () => { ZERO_ADDRESS ); }); + + it("OTHR owner can flashclaim OTHREXP, VSL", async () => { + const { + users: [user1], + nOTHR, + pool, + nVSL, + nOTHREXP, + } = testEnv; + + const nOTHRBalanceBefore = await nOTHR.balanceOf(user1.address); + + await expect( + await pool + .connect(user1.signer) + .claimOtherExpandedAndSupply(["0"], [], [], [[]]) + ); + + const nOTHRBalanceAfter = await nOTHR.balanceOf(user1.address); + await expect(nOTHRBalanceAfter).to.be.eq(nOTHRBalanceBefore.sub(1)); + + const nOTHREXPBalanceAfter = await nOTHREXP.balanceOf(user1.address); + const nVSLBalanceAfter = await nVSL.balanceOf(user1.address); + + await expect(nOTHREXPBalanceAfter).to.be.eq(1); + await expect(nVSLBalanceAfter).to.be.eq(1); + }); + + it("OTHR non-owner can't flashclaim OTHREXP, VSL, KODA", async () => { + const { + users: [, user2], + pool, + } = testEnv; + + await expect( + pool + .connect(user2.signer) + .claimOtherExpandedAndSupply(["0"], [], [], [[]]) + ).to.be.reverted; + }); + + it("OTHR owner can flashclaim multiple OTHREXP, VSL", async () => { + const { + users: [user1], + pool, + nVSL, + nOTHREXP, + } = testEnv; + + await expect( + await pool + .connect(user1.signer) + .claimOtherExpandedAndSupply(["0", "1", "2"], [], [], [[]]) + ); + + const nOTHREXPBalanceAfter = await nOTHREXP.balanceOf(user1.address); + const nVSLBalanceAfter = await nVSL.balanceOf(user1.address); + + await expect(nOTHREXPBalanceAfter).to.be.eq(3); + await expect(nVSLBalanceAfter).to.be.eq(3); + }); + + it("OTHR owner can flashclaim OTHREXP, VSL and KODA", async () => { + const { + users: [user1], + pool, + nVSL, + nOTHREXP, + nKODA, + } = testEnv; + + await expect( + await pool.connect(user1.signer).claimOtherExpandedAndSupply( + ["0", "1", "2"], + ["12", "23"], + ["1", "2"], + [[]] //merkle proof + ) + ); + + const nOTHREXPBalanceAfter = await nOTHREXP.balanceOf(user1.address); + const nVSLBalanceAfter = await nVSL.balanceOf(user1.address); + const nKODAPBalanceAfter = await nKODA.balanceOf(user1.address); + + await expect(nOTHREXPBalanceAfter).to.be.eq(3); + await expect(nVSLBalanceAfter).to.be.eq(3); + await expect(nKODAPBalanceAfter).to.be.eq(2); + }); }); diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index 6f6aa9f65..b4ee0072d 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -171,6 +171,7 @@ export interface TestEnv { usdt: MintableERC20; nBAYC: NTokenBAYC; nOTHR: NTokenOtherdeed; + OTHR: MintableERC721; bayc: MintableERC721; sfvldr: StakefishNFTManager; nSfvldr: NTokenStakefish; @@ -191,6 +192,12 @@ export interface TestEnv { doodles: MintableERC721; bakc: MintableERC721; nBAKC: NTokenBAKC; + nVSL: NToken; + nKODA: NToken; + nOTHREXP: NToken; + OTHREXP: MintableERC721; + VSL: MintableERC721; + KODA: MintableERC721; mockTokenFaucet: MockTokenFaucet; wPunkGateway: WPunkGateway; wETHGateway: WETHGateway; @@ -268,6 +275,9 @@ export async function initializeMakeSuite() { mayc: {} as MintableERC721, doodles: {} as MintableERC721, bakc: {} as MintableERC721, + VSL: {} as MintableERC721, + KODA: {} as MintableERC721, + OTHREXP: {} as MintableERC721, mockTokenFaucet: {} as MockTokenFaucet, wPunkGateway: {} as WPunkGateway, wETHGateway: {} as WETHGateway, @@ -331,8 +341,6 @@ export async function initializeMakeSuite() { testEnv.poolAdmin.signer ); - testEnv.nOTHR = await getNTokenOtherdeed(); - testEnv.protocolDataProvider = await getProtocolDataProvider(); testEnv.mockTokenFaucet = await getMockTokenFaucet(); @@ -398,6 +406,22 @@ export async function initializeMakeSuite() { (xToken) => xToken.symbol === NTokenContractId.nBAKC )?.tokenAddress; + const nOTHRAddress = allTokens.find( + (xToken) => xToken.symbol === NTokenContractId.nOTHR + )?.tokenAddress; + + const nOTHREXPAddress = allTokens.find( + (xToken) => xToken.symbol === NTokenContractId.nOTHREXP + )?.tokenAddress; + + const nVSLAddress = allTokens.find( + (xToken) => xToken.symbol === NTokenContractId.nVSL + )?.tokenAddress; + + const nKODAAddress = allTokens.find( + (xToken) => xToken.symbol === NTokenContractId.nKODA + )?.tokenAddress; + const nSfvldrAddress = allTokens.find( (xToken) => xToken.symbol === NTokenContractId.nSFVLDR )?.tokenAddress; @@ -495,6 +519,22 @@ export async function initializeMakeSuite() { (token) => token.symbol === ERC721TokenContractId.MOONBIRD )?.tokenAddress; + const OTHRAddess = reservesTokens.find( + (token) => token.symbol === ERC721TokenContractId.OTHR + )?.tokenAddress; + + const VSLAddess = reservesTokens.find( + (token) => token.symbol === ERC721TokenContractId.VSL + )?.tokenAddress; + + const OTHREXPAddess = reservesTokens.find( + (token) => token.symbol === ERC721TokenContractId.OTHREXP + )?.tokenAddress; + + const KODAddress = reservesTokens.find( + (token) => token.symbol === ERC721TokenContractId.KODA + )?.tokenAddress; + if (!pDaiAddress || !pWEthAddress || !nBAYCAddress) { console.error("no pDAI|pWETH|nBAYC address"); process.exit(1); @@ -526,6 +566,10 @@ export async function initializeMakeSuite() { testEnv.nDOODLE = await getNToken(nDOODLEAddress); testEnv.nBAKC = await getNTokenBAKC(nBAKCAddress); testEnv.nSfvldr = await getNTokenStakefish(nSfvldrAddress); + testEnv.nOTHR = await getNTokenOtherdeed(nOTHRAddress); + testEnv.nOTHREXP = await getNToken(nOTHREXPAddress); + testEnv.nKODA = await getNToken(nKODAAddress); + testEnv.nVSL = await getNToken(nVSLAddress); testEnv.nMOONBIRD = await getNTokenMoonBirds(nMOONBIRDAddress); @@ -554,6 +598,11 @@ export async function initializeMakeSuite() { testEnv.mayc = await getMintableERC721(maycAddress); testEnv.doodles = await getMintableERC721(doodlesAddress); testEnv.bakc = await getMintableERC721(bakcAddress); + testEnv.OTHR = await getMintableERC721(OTHRAddess); + testEnv.OTHREXP = await getMintableERC721(OTHREXPAddess); + testEnv.KODA = await getMintableERC721(KODAddress); + testEnv.VSL = await getMintableERC721(VSLAddess); + testEnv.moonbirds = await getMoonBirds(moonbirdsAddress); testEnv.uniswapV3Factory = await getUniswapV3Factory(); testEnv.nftPositionManager = await getNonfungiblePositionManager(); diff --git a/test/helpers/validated-steps.ts b/test/helpers/validated-steps.ts index b4f5923bf..21707fc8c 100644 --- a/test/helpers/validated-steps.ts +++ b/test/helpers/validated-steps.ts @@ -70,7 +70,7 @@ export const mintAndValidate = async ( : await convertToCurrencyDecimals(token.address, amount); if (isNFT) { for (const i in nftIdsToUse) { - await waitForTx( + const tx = await waitForTx( await token.connect(user.signer)["mint(address)"](user.address) ); expect(await (token as ERC721).ownerOf(i)).to.be.equal(user.address); From dd9390d62635ad49e9c3e55d34a0df1a060c5489 Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Wed, 19 Apr 2023 17:38:25 -0700 Subject: [PATCH 2/6] chore: modify OTHR minting logic --- contracts/mocks/tokens/Land.sol | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/contracts/mocks/tokens/Land.sol b/contracts/mocks/tokens/Land.sol index 94b94acdc..8ac6c5567 100644 --- a/contracts/mocks/tokens/Land.sol +++ b/contracts/mocks/tokens/Land.sol @@ -1182,7 +1182,7 @@ contract Land is ERC721Enumerable, Ownable { MAX_FUTURE_LANDS = amount.future; betaNftIdCurrent = amount.alpha; //beta starts after alpha - mintIndexPublicSaleAndContributors = amount.alpha + amount.beta; //public sale starts after beta + mintIndexPublicSaleAndContributors = 0; //public sale starts after beta RESERVED_CONTRIBUTORS_AMOUNT = 10000; @@ -1214,7 +1214,8 @@ contract Land is ERC721Enumerable, Ownable { totalSupply() + 1 <= collectionSize, "exceed collectionSize" ); - _safeMint(_to, mintIndexPublicSaleAndContributors++); + _safeMint(_to, mintIndexPublicSaleAndContributors); + mintIndexPublicSaleAndContributors++; } function mint(uint256 count, address to) public virtual { @@ -1223,8 +1224,9 @@ contract Land is ERC721Enumerable, Ownable { "exceed collectionSize" ); for (uint256 i = 0; i < count; i++) { - mintIndexPublicSaleAndContributors++; + _safeMint(to, mintIndexPublicSaleAndContributors); + mintIndexPublicSaleAndContributors++; } } From 8c260295ed88bc433a47043113e45a3509c77c93 Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Wed, 19 Apr 2023 17:42:54 -0700 Subject: [PATCH 3/6] chore: fix typo --- test/helpers/make-suite.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/helpers/make-suite.ts b/test/helpers/make-suite.ts index b4ee0072d..ca225eb5b 100644 --- a/test/helpers/make-suite.ts +++ b/test/helpers/make-suite.ts @@ -519,15 +519,15 @@ export async function initializeMakeSuite() { (token) => token.symbol === ERC721TokenContractId.MOONBIRD )?.tokenAddress; - const OTHRAddess = reservesTokens.find( + const OTHRAddress = reservesTokens.find( (token) => token.symbol === ERC721TokenContractId.OTHR )?.tokenAddress; - const VSLAddess = reservesTokens.find( + const VSLAddress = reservesTokens.find( (token) => token.symbol === ERC721TokenContractId.VSL )?.tokenAddress; - const OTHREXPAddess = reservesTokens.find( + const OTHREXPAddress = reservesTokens.find( (token) => token.symbol === ERC721TokenContractId.OTHREXP )?.tokenAddress; @@ -598,10 +598,10 @@ export async function initializeMakeSuite() { testEnv.mayc = await getMintableERC721(maycAddress); testEnv.doodles = await getMintableERC721(doodlesAddress); testEnv.bakc = await getMintableERC721(bakcAddress); - testEnv.OTHR = await getMintableERC721(OTHRAddess); - testEnv.OTHREXP = await getMintableERC721(OTHREXPAddess); + testEnv.OTHR = await getMintableERC721(OTHRAddress); + testEnv.OTHREXP = await getMintableERC721(OTHREXPAddress); testEnv.KODA = await getMintableERC721(KODAddress); - testEnv.VSL = await getMintableERC721(VSLAddess); + testEnv.VSL = await getMintableERC721(VSLAddress); testEnv.moonbirds = await getMoonBirds(moonbirdsAddress); testEnv.uniswapV3Factory = await getUniswapV3Factory(); From 0574e64a9c90f665995e2a1bccd7b1ff6dbe65b2 Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Wed, 19 Apr 2023 18:37:33 -0700 Subject: [PATCH 4/6] chore: adds more tests --- test/_xtoken_ntoken_otherdeed.spec.ts | 42 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/test/_xtoken_ntoken_otherdeed.spec.ts b/test/_xtoken_ntoken_otherdeed.spec.ts index 9a21a093b..093d1d75b 100644 --- a/test/_xtoken_ntoken_otherdeed.spec.ts +++ b/test/_xtoken_ntoken_otherdeed.spec.ts @@ -4,20 +4,19 @@ import {TestEnv} from "./helpers/make-suite"; import {loadFixture} from "@nomicfoundation/hardhat-network-helpers"; import {testEnvFixture} from "./helpers/setup-env"; import {HotWalletProxy} from "../types"; -import { - getERC721, - getHotWalletProxy, - getMintableERC721, -} from "../helpers/contracts-getters"; +import {getHotWalletProxy} from "../helpers/contracts-getters"; import {ZERO_ADDRESS} from "../helpers/constants"; -import {supplyAndValidate} from "./helpers/validated-steps"; +import { + borrowAndValidate, + changePriceAndValidate, + supplyAndValidate, +} from "./helpers/validated-steps"; +import {ProtocolErrors} from "../helpers/types"; describe("Otherdeed nToken warmwallet delegation", () => { let testEnv: TestEnv; let hotWallet: HotWalletProxy; - before(async () => {}); - beforeEach(async () => { testEnv = await loadFixture(testEnvFixture); @@ -43,7 +42,6 @@ describe("Otherdeed nToken warmwallet delegation", () => { const { users: [user1], nOTHR, - poolAdmin, } = testEnv; await expect(await hotWallet.getHotWallet(nOTHR.address)).to.be.eq( @@ -191,4 +189,30 @@ describe("Otherdeed nToken warmwallet delegation", () => { await expect(nVSLBalanceAfter).to.be.eq(3); await expect(nKODAPBalanceAfter).to.be.eq(2); }); + + it("OTHR owner can't flashclaim if HF goes below 1'", async () => { + const { + users: [user1, user2], + pool, + VSL, + OTHREXP, + weth, + } = testEnv; + await supplyAndValidate(weth, "100000", user2, true); + await borrowAndValidate(weth, "3", user1); // can borrow 40% of total value (10) + + await changePriceAndValidate(VSL, "0.000001"); + await changePriceAndValidate(OTHREXP, "0.000001"); + + await expect( + pool.connect(user1.signer).claimOtherExpandedAndSupply( + ["0", "1", "2"], + [], + [], + [[]] //merkle proof + ) + ).to.be.revertedWith( + ProtocolErrors.HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD + ); + }); }); From 926ffcc0dd78b5151591beaa7f227810f72025ae Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Wed, 19 Apr 2023 19:11:15 -0700 Subject: [PATCH 5/6] chore: remove debug code --- test/helpers/validated-steps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/helpers/validated-steps.ts b/test/helpers/validated-steps.ts index 21707fc8c..b4f5923bf 100644 --- a/test/helpers/validated-steps.ts +++ b/test/helpers/validated-steps.ts @@ -70,7 +70,7 @@ export const mintAndValidate = async ( : await convertToCurrencyDecimals(token.address, amount); if (isNFT) { for (const i in nftIdsToUse) { - const tx = await waitForTx( + await waitForTx( await token.connect(user.signer)["mint(address)"](user.address) ); expect(await (token as ERC721).ownerOf(i)).to.be.equal(user.address); From 008b6788ec8e622b4f2ad1a3003255e628776d55 Mon Sep 17 00:00:00 2001 From: 0xwalid Date: Thu, 20 Apr 2023 23:44:48 -0700 Subject: [PATCH 6/6] chore: adds above 1 HF test --- test/_xtoken_ntoken_otherdeed.spec.ts | 31 ++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/test/_xtoken_ntoken_otherdeed.spec.ts b/test/_xtoken_ntoken_otherdeed.spec.ts index 093d1d75b..5c941189d 100644 --- a/test/_xtoken_ntoken_otherdeed.spec.ts +++ b/test/_xtoken_ntoken_otherdeed.spec.ts @@ -12,6 +12,8 @@ import { supplyAndValidate, } from "./helpers/validated-steps"; import {ProtocolErrors} from "../helpers/types"; +import {ethers} from "hardhat"; +import {BigNumber} from "ethers"; describe("Otherdeed nToken warmwallet delegation", () => { let testEnv: TestEnv; @@ -190,6 +192,33 @@ describe("Otherdeed nToken warmwallet delegation", () => { await expect(nKODAPBalanceAfter).to.be.eq(2); }); + it("OTHR owner can flashclaim if HF is above 1'", async () => { + const { + users: [user1, user2], + pool, + VSL, + OTHREXP, + weth, + } = testEnv; + await supplyAndValidate(weth, "100000", user2, true); + await borrowAndValidate(weth, "3", user1); // can borrow 30% of total value (10) + + await pool.connect(user1.signer).claimOtherExpandedAndSupply( + ["0", "1", "2"], + [], + [], + [[]] //merkle proof + ); + + await expect( + BigNumber.from( + ( + await pool.getUserAccountData(user1.address) + ).healthFactor + ) + ).to.be.gt(BigNumber.from(10).pow(18)); + }); + it("OTHR owner can't flashclaim if HF goes below 1'", async () => { const { users: [user1, user2], @@ -199,7 +228,7 @@ describe("Otherdeed nToken warmwallet delegation", () => { weth, } = testEnv; await supplyAndValidate(weth, "100000", user2, true); - await borrowAndValidate(weth, "3", user1); // can borrow 40% of total value (10) + await borrowAndValidate(weth, "3", user1); // can borrow 30% of total value (10) await changePriceAndValidate(VSL, "0.000001"); await changePriceAndValidate(OTHREXP, "0.000001");