├── .prettierrc ├── .solhintignore ├── slither.config.json ├── .env.template ├── .eslintignore ├── .prettierignore ├── deployments ├── deployed-contracts-mainnet.json ├── deployed-contracts-sepolia.json ├── deployed-contracts-apechain.json └── deployed-contracts-curtis.json ├── contracts ├── misc │ ├── interfaces │ │ ├── IStakeManagerV1.sol │ │ ├── IBendApeCoinV1.sol │ │ ├── ILendPoolLoan.sol │ │ ├── IAaveLendPool.sol │ │ ├── IAaveLendPoolAddressesProvider.sol │ │ ├── ILendPoolAddressesProvider.sol │ │ ├── IAaveFlashLoanReceiver.sol │ │ └── ILendPool.sol │ └── BendApeCoinStakedVoting.sol ├── interfaces │ ├── IWAPE.sol │ ├── IRewardsStrategy.sol │ ├── IBNFTRegistry.sol │ ├── IWithdrawStrategy.sol │ ├── IBendV2Interfaces.sol │ ├── ICoinPool.sol │ ├── IStakedNft.sol │ ├── INftPool.sol │ ├── IDelegateRegistryV2.sol │ ├── IApeCoinStaking.sol │ ├── IStakeManager.sol │ ├── INftVault.sol │ └── IDelegationRegistry.sol ├── test │ ├── ApeCoinStaking │ │ └── IShadowCallbackReceiver.sol │ ├── MockAaveLendPoolAddressesProvider.sol │ ├── MockStakeManagerV1.sol │ ├── MockBNFTRegistry.sol │ ├── MockBendApeCoinV1.sol │ ├── MockBeacon.sol │ ├── MockBendLendPoolAddressesProvider.sol │ ├── MintableERC20.sol │ ├── BendStakeManagerTester.sol │ ├── MockBNFT.sol │ ├── MockAaveLendPool.sol │ ├── MockBendLendPoolLoan.sol │ ├── MockDelegationRegistryV2.sol │ ├── MockAddressProviderV2.sol │ ├── MintableERC721.sol │ └── MockBendLendPool.sol ├── stakednft │ ├── StBAKC.sol │ ├── StBAYC.sol │ └── StMAYC.sol ├── strategy │ ├── DefaultRewardsStrategy.sol │ └── DefaultWithdrawStrategy.sol └── libraries │ └── ApeStakingLib.sol ├── remappings.txt ├── .gitignore ├── test ├── hardhat │ ├── stakednft │ │ ├── StBAKC.test.ts │ │ ├── StBAYC.test.ts │ │ ├── StMAYC.test.ts │ │ └── StNft.test.ts │ ├── helpers │ │ ├── gas-helper.ts │ │ ├── transaction-helper.ts │ │ ├── hardhat-keys.ts │ │ ├── signature-helper.ts │ │ └── block-traveller.ts │ ├── DefaultRewardsStrategy.test.ts │ ├── PoolViewer.test.ts │ ├── utils.ts │ └── BendNftLockup.test.ts └── foundry │ ├── UtilitiesHelper.sol │ ├── BendStakeManagerAsync.t.sol │ ├── BendNftPool.t.sol │ ├── BendCoinPool.t.sol │ └── BendStakeManager.t.sol ├── .npmignore ├── .solcover.js ├── tasks ├── keys.ts.template ├── set-bre.ts ├── utils │ ├── DRE.ts │ ├── verification.ts │ └── helpers.ts └── config.ts ├── foundry.toml ├── abis ├── IAaveLendPoolAddressesProvider.json ├── IRewardsStrategy.json ├── IStakeManagerV1.json ├── IWAPE.json ├── IBNFTRegistry.json ├── ILendPoolAddressesProvider.json ├── IWithdrawStrategy.json ├── IAddressProviderV2.json ├── IAaveFlashLoanReceiver.json ├── ILendPoolLoan.json ├── IAaveLendPool.json ├── IPoolLensV2.json ├── DefaultRewardsStrategy.json ├── BendApeCoinStakedVoting.json ├── IDelegateRegistryV2.json ├── ILendPool.json ├── DefaultWithdrawStrategy.json └── INftPool.json ├── tsconfig.json ├── .solhint.json ├── .commitlintrc ├── .vscode └── settings.json ├── .release-it.js ├── .eslintrc ├── README.md ├── package.json ├── LICENSE └── hardhat.config.ts /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test 3 | demo.sol 4 | *.t.sol 5 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter_paths": "(mocks/|test/|lib/|node_modules/)" 3 | } 4 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | ETHERSCAN_KEY="" 2 | INFURA_KEY="" 3 | ALCHEMY_KEY="" 4 | MNEMONIC="" 5 | PRIVATE_KEY="" 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | typechain 7 | .history 8 | tasks/x2y2 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | artifacts 3 | cache 4 | coverage* 5 | gasReporterOutput.json 6 | typechain-types 7 | .history 8 | deployments 9 | tasks/x2y2 10 | cache_forge 11 | out 12 | lib -------------------------------------------------------------------------------- /deployments/deployed-contracts-mainnet.json: -------------------------------------------------------------------------------- 1 | { 2 | "BendNftLockup": { 3 | "address": "0x29A1D37d7c5fbB68d1ACF825E04d3FF448D1088e", 4 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 5 | } 6 | } -------------------------------------------------------------------------------- /deployments/deployed-contracts-sepolia.json: -------------------------------------------------------------------------------- 1 | { 2 | "BendNftLockup": { 3 | "address": "0x1756C5B489cBE264C10AbfFf019C370d3416A498", 4 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 5 | } 6 | } -------------------------------------------------------------------------------- /contracts/misc/interfaces/IStakeManagerV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IStakeManagerV1 { 5 | function claimFor(address proxy, address staker) external; 6 | } 7 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | @openzeppelin/=node_modules/@openzeppelin/ 2 | ds-test/=lib/forge-std/lib/ds-test/src/ 3 | eth-gas-reporter/=node_modules/eth-gas-reporter/ 4 | forge-std/=lib/forge-std/src/ 5 | hardhat/=node_modules/hardhat/ 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | # Hardhat files 5 | /cache 6 | /artifacts 7 | 8 | # TypeChain files 9 | /typechain 10 | /typechain-types 11 | 12 | # solidity-coverage files 13 | /coverage 14 | /coverage.json 15 | -------------------------------------------------------------------------------- /contracts/interfaces/IWAPE.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IWAPE { 5 | function deposit() external payable; 6 | 7 | function withdraw(uint256 wad) external; 8 | } 9 | -------------------------------------------------------------------------------- /contracts/interfaces/IRewardsStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IRewardsStrategy { 5 | function getNftRewardsShare() external view returns (uint256 nftShare); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/test/ApeCoinStaking/IShadowCallbackReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.18; 4 | 5 | interface IShadowCallbackReceiver { 6 | function executeCallback(bytes32 guid) external; 7 | } 8 | -------------------------------------------------------------------------------- /test/hardhat/stakednft/StBAKC.test.ts: -------------------------------------------------------------------------------- 1 | import { Contracts } from "../setup"; 2 | import { makeStNftTest } from "./StNft.test"; 3 | 4 | makeStNftTest("StBAKC", (contracts: Contracts) => { 5 | return [contracts.stBakc, contracts.bakc]; 6 | }); 7 | -------------------------------------------------------------------------------- /test/hardhat/stakednft/StBAYC.test.ts: -------------------------------------------------------------------------------- 1 | import { Contracts } from "../setup"; 2 | import { makeStNftTest } from "./StNft.test"; 3 | 4 | makeStNftTest("StBAYC", (contracts: Contracts) => { 5 | return [contracts.stBayc, contracts.bayc]; 6 | }); 7 | -------------------------------------------------------------------------------- /test/hardhat/stakednft/StMAYC.test.ts: -------------------------------------------------------------------------------- 1 | import { Contracts } from "../setup"; 2 | import { makeStNftTest } from "./StNft.test"; 3 | 4 | makeStNftTest("StMAYC", (contracts: Contracts) => { 5 | return [contracts.stMayc, contracts.mayc]; 6 | }); 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .env* 4 | coverage 5 | coverage.json 6 | typechain 7 | 8 | # Hardhat files 9 | cache 10 | artifacts 11 | 12 | hardhat.config.ts 13 | contracts/test 14 | test/ 15 | 16 | # Others 17 | lib 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | silent: true, 3 | measureStatementCoverage: true, 4 | measureFunctionCoverage: true, 5 | skipFiles: ["interfaces", "test", "misc/interfaces", "misc/PoolViewer.sol"], 6 | configureYulOptimizer: true, 7 | }; 8 | -------------------------------------------------------------------------------- /contracts/interfaces/IBNFTRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IBNFTRegistry { 5 | function getBNFTAddresses(address nftAsset) external view returns (address bNftProxy, address bNftImpl); 6 | } 7 | -------------------------------------------------------------------------------- /tasks/keys.ts.template: -------------------------------------------------------------------------------- 1 | export function findPrivateKey(publicKey: string): string { 2 | switch (publicKey.toLowerCase()) { 3 | case "": 4 | return ""; 5 | 6 | default: 7 | throw new Error("No private key found"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/IWithdrawStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IWithdrawStrategy { 5 | function withdrawApeCoin(uint256 required) external returns (uint256 withdrawn); 6 | 7 | function initGlobalState() external; 8 | } 9 | -------------------------------------------------------------------------------- /test/hardhat/helpers/gas-helper.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, ContractTransaction } from "ethers"; 2 | 3 | export async function gasCost(tx: ContractTransaction): Promise { 4 | const receipt = await tx.wait(); 5 | return receipt.cumulativeGasUsed.mul(receipt.effectiveGasPrice); 6 | } 7 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'contracts' 3 | out = 'out' 4 | libs = ['node_modules', 'lib'] 5 | test = 'test' 6 | cache_path = 'cache_forge' 7 | gas_reports = ["BendCoinPool", "BendNftPool", "BendStakeManager", "NftVault", "StBAYC"] 8 | 9 | [fmt] 10 | line_length = 120 11 | 12 | [fuzz] 13 | runs = 32 -------------------------------------------------------------------------------- /contracts/misc/interfaces/IBendApeCoinV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; 5 | 6 | interface IBendApeCoinV1 is IERC4626 { 7 | function assetBalanceOf(address account) external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/ILendPoolLoan.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface ILendPoolLoan { 5 | function getCollateralLoanId(address nftAsset, uint256 nftTokenId) external view returns (uint256); 6 | 7 | function borrowerOf(uint256 loanId) external view returns (address); 8 | } 9 | -------------------------------------------------------------------------------- /abis/IAaveLendPoolAddressesProvider.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "getLendingPool", 5 | "outputs": [ 6 | { 7 | "internalType": "address", 8 | "name": "", 9 | "type": "address" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /abis/IRewardsStrategy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "getNftRewardsShare", 5 | "outputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "nftShare", 9 | "type": "uint256" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | "declaration": true 9 | }, 10 | "include": ["./scripts", "./test", "./typechain-types", "./tasks"], 11 | "files": ["./hardhat.config.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "0.8.18"], 5 | "func-visibility": ["error", { "ignoreConstructors": true }], 6 | "func-name-mixedcase": "off", 7 | "not-rely-on-time": "off", 8 | "reason-string": "off", 9 | "no-empty-blocks": "off", 10 | "max-states-count": ["warn", 20] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tasks/set-bre.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { DRE, initDB, setDRE } from "./utils/DRE"; 4 | 5 | task(`set-DRE`, `Inits the DRE, to have access to all the plugins' objects`).setAction(async (_, _DRE) => { 6 | if (DRE) { 7 | return; 8 | } 9 | console.log("- Enviroment"); 10 | console.log(" - Network :", _DRE.network.name); 11 | setDRE(_DRE); 12 | initDB(_DRE.network.name); 13 | return _DRE; 14 | }); 15 | -------------------------------------------------------------------------------- /abis/IStakeManagerV1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "proxy", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "staker", 12 | "type": "address" 13 | } 14 | ], 15 | "name": "claimFor", 16 | "outputs": [], 17 | "stateMutability": "nonpayable", 18 | "type": "function" 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /contracts/stakednft/StBAKC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {INftVault} from "../interfaces/INftVault.sol"; 5 | 6 | import {StNft, IERC721MetadataUpgradeable} from "./StNft.sol"; 7 | 8 | contract StBAKC is StNft { 9 | function initialize(IERC721MetadataUpgradeable bakc_, INftVault nftVault_) public initializer { 10 | __StNft_init(bakc_, nftVault_, "Staked BAKC", "stBAKC"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/stakednft/StBAYC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {INftVault} from "../interfaces/INftVault.sol"; 5 | 6 | import {StNft, IERC721MetadataUpgradeable} from "./StNft.sol"; 7 | 8 | contract StBAYC is StNft { 9 | function initialize(IERC721MetadataUpgradeable bayc_, INftVault nftVault_) public initializer { 10 | __StNft_init(bayc_, nftVault_, "Staked BAYC", "stBAYC"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/stakednft/StMAYC.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {INftVault} from "../interfaces/INftVault.sol"; 5 | 6 | import {StNft, IERC721MetadataUpgradeable} from "./StNft.sol"; 7 | 8 | contract StMAYC is StNft { 9 | function initialize(IERC721MetadataUpgradeable mayc_, INftVault nftVault_) public initializer { 10 | __StNft_init(mayc_, nftVault_, "Staked MAYC", "stMAYC"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /abis/IWAPE.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "deposit", 5 | "outputs": [], 6 | "stateMutability": "payable", 7 | "type": "function" 8 | }, 9 | { 10 | "inputs": [ 11 | { 12 | "internalType": "uint256", 13 | "name": "wad", 14 | "type": "uint256" 15 | } 16 | ], 17 | "name": "withdraw", 18 | "outputs": [], 19 | "stateMutability": "nonpayable", 20 | "type": "function" 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/IAaveLendPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IAaveLendPool { 5 | function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint256); 6 | 7 | function flashLoan( 8 | address receiverAddress, 9 | address[] calldata assets, 10 | uint256[] calldata amounts, 11 | uint256[] calldata modes, 12 | address onBehalfOf, 13 | bytes calldata params, 14 | uint16 referralCode 15 | ) external; 16 | } 17 | -------------------------------------------------------------------------------- /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@commitlint/config-conventional" 4 | ], 5 | "rules": { 6 | "subject-case": [ 7 | 2, 8 | "always", 9 | "sentence-case" 10 | ], 11 | "type-enum": [ 12 | 2, 13 | "always", 14 | [ 15 | "build", 16 | "ci", 17 | "chore", 18 | "docs", 19 | "feat", 20 | "fix", 21 | "perf", 22 | "refactor", 23 | "revert", 24 | "style", 25 | "test" 26 | ] 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": false, 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/.DS_Store": true, 6 | "**/node_modules": true, 7 | "**/dist": true, 8 | ".history/**": true, 9 | "artifacts": true, 10 | "cache": true 11 | }, 12 | "files.watcherExclude": { 13 | "**/.git/objects/**": true, 14 | "**/.git/subtree-cache/**": true, 15 | "**/node_modules/**": true, 16 | "**/dist/**": true 17 | }, 18 | "solidity.compileUsingRemoteVersion": "v0.8.18+commit.87f61d96" 19 | } 20 | -------------------------------------------------------------------------------- /tasks/utils/DRE.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import low from "lowdb"; 3 | import FileSync from "lowdb/adapters/FileSync"; 4 | import { HardhatRuntimeEnvironment } from "hardhat/types"; 5 | 6 | export let DB: low.LowdbSync; 7 | 8 | export const initDB = (network: string): void => { 9 | DB = low(new FileSync(`./deployments/deployed-contracts-${network}.json`)); 10 | }; 11 | 12 | export let DRE: HardhatRuntimeEnvironment; 13 | 14 | export const setDRE = (_DRE: HardhatRuntimeEnvironment): void => { 15 | DRE = _DRE; 16 | }; 17 | -------------------------------------------------------------------------------- /contracts/test/MockAaveLendPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IAaveLendPoolAddressesProvider} from "../misc/interfaces/IAaveLendPoolAddressesProvider.sol"; 5 | 6 | contract MockAaveLendPoolAddressesProvider is IAaveLendPoolAddressesProvider { 7 | address public lendingPool; 8 | 9 | function setLendingPool(address lendingPool_) public { 10 | lendingPool = lendingPool_; 11 | } 12 | 13 | function getLendingPool() public view returns (address) { 14 | return lendingPool; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /abis/IBNFTRegistry.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "nftAsset", 7 | "type": "address" 8 | } 9 | ], 10 | "name": "getBNFTAddresses", 11 | "outputs": [ 12 | { 13 | "internalType": "address", 14 | "name": "bNftProxy", 15 | "type": "address" 16 | }, 17 | { 18 | "internalType": "address", 19 | "name": "bNftImpl", 20 | "type": "address" 21 | } 22 | ], 23 | "stateMutability": "view", 24 | "type": "function" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/IAaveLendPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | /** 5 | * @title IAaveLendPoolAddressesProvider contract 6 | * @dev Main registry of addresses part of or connected to the aave protocol, including permissioned roles 7 | * - Acting also as factory of proxies and admin of those, so with right to change its implementations 8 | * - Owned by the Aave Governance 9 | * @author Bend 10 | **/ 11 | interface IAaveLendPoolAddressesProvider { 12 | function getLendingPool() external view returns (address); 13 | } 14 | -------------------------------------------------------------------------------- /abis/ILendPoolAddressesProvider.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "getLendPool", 5 | "outputs": [ 6 | { 7 | "internalType": "address", 8 | "name": "", 9 | "type": "address" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [], 17 | "name": "getLendPoolLoan", 18 | "outputs": [ 19 | { 20 | "internalType": "address", 21 | "name": "", 22 | "type": "address" 23 | } 24 | ], 25 | "stateMutability": "view", 26 | "type": "function" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /abis/IWithdrawStrategy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "initGlobalState", 5 | "outputs": [], 6 | "stateMutability": "nonpayable", 7 | "type": "function" 8 | }, 9 | { 10 | "inputs": [ 11 | { 12 | "internalType": "uint256", 13 | "name": "required", 14 | "type": "uint256" 15 | } 16 | ], 17 | "name": "withdrawApeCoin", 18 | "outputs": [ 19 | { 20 | "internalType": "uint256", 21 | "name": "withdrawn", 22 | "type": "uint256" 23 | } 24 | ], 25 | "stateMutability": "nonpayable", 26 | "type": "function" 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/ILendPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | /** 5 | * @title LendPoolAddressesProvider contract 6 | * @dev Main registry of addresses part of or connected to the protocol, including permissioned roles 7 | * - Acting also as factory of proxies and admin of those, so with right to change its implementations 8 | * - Owned by the Bend Governance 9 | * @author Bend 10 | **/ 11 | interface ILendPoolAddressesProvider { 12 | function getLendPool() external view returns (address); 13 | 14 | function getLendPoolLoan() external view returns (address); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/IAaveFlashLoanReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | /** 5 | * @title IAaveFlashLoanReceiver interface 6 | * @notice Interface for the Aave fee IFlashLoanReceiver. 7 | * @author Bend 8 | * @dev implement this interface to develop a flashloan-compatible flashLoanReceiver contract 9 | **/ 10 | interface IAaveFlashLoanReceiver { 11 | function executeOperation( 12 | address[] calldata assets, 13 | uint256[] calldata amounts, 14 | uint256[] calldata premiums, 15 | address initiator, 16 | bytes calldata params 17 | ) external returns (bool); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/test/MockStakeManagerV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import {IStakeManagerV1} from "../misc/interfaces/IStakeManagerV1.sol"; 7 | 8 | contract MockStakeManagerV1 is IStakeManagerV1 { 9 | uint256 public constant REWARDS_AMOUNT = 100 * 1e18; 10 | 11 | IERC20 public apeCoin; 12 | 13 | constructor(address apeCoin_) { 14 | apeCoin = IERC20(apeCoin_); 15 | } 16 | 17 | function claimFor(address proxy, address staker) external override { 18 | proxy; 19 | 20 | apeCoin.transfer(staker, REWARDS_AMOUNT); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | // Doc: https://github.com/release-it/release-it 4 | module.exports = { 5 | git: { 6 | commitMessage: "build: Release v${version}", 7 | requireUpstream: false, 8 | pushRepo: "upstream", // Push tags and commit to the remote `upstream` (fails if doesn't exist) 9 | requireBranch: "main", // Push commit to the branch `master` (fail if on other branch) 10 | requireCommits: true, // Require new commits since latest tag 11 | }, 12 | github: { 13 | release: true, 14 | }, 15 | hooks: { 16 | "after:bump": "yarn compile:force", 17 | }, 18 | 19 | npm: { 20 | publish: false, 21 | skipChecks: true, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es2021": true, 5 | "mocha": true, 6 | "node": true 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "extends": [ 10 | "standard", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:node/recommended" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 12 18 | }, 19 | "rules": { 20 | "node/no-unsupported-features/es-syntax": ["error", { "ignores": ["modules"] }], 21 | "prefer-const": "off" 22 | }, 23 | "settings": { 24 | "node": { 25 | "tryExtensions": [".js", ".json", ".ts", ".d.ts"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/test/MockBNFTRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; 5 | 6 | import {IBNFTRegistry} from "../interfaces/IBNFTRegistry.sol"; 7 | 8 | contract MockBNFTRegistry is IBNFTRegistry { 9 | mapping(address => address) public bnftContracts; 10 | 11 | function getBNFTAddresses(address nftAsset) public view returns (address bNftProxy, address bNftImpl) { 12 | bNftProxy = bnftContracts[nftAsset]; 13 | bNftImpl = bNftProxy; 14 | } 15 | 16 | function setBNFTContract(address nftAsset, address bnftAsset) public { 17 | bnftContracts[nftAsset] = bnftAsset; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /abis/IAddressProviderV2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "getPoolManager", 5 | "outputs": [ 6 | { 7 | "internalType": "address", 8 | "name": "", 9 | "type": "address" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "uint256", 19 | "name": "moduleId", 20 | "type": "uint256" 21 | } 22 | ], 23 | "name": "getPoolModuleProxy", 24 | "outputs": [ 25 | { 26 | "internalType": "address", 27 | "name": "", 28 | "type": "address" 29 | } 30 | ], 31 | "stateMutability": "view", 32 | "type": "function" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /contracts/test/MockBendApeCoinV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 7 | import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol"; 8 | 9 | import {IBendApeCoinV1} from "../misc/interfaces/IBendApeCoinV1.sol"; 10 | 11 | contract MockBendApeCoinV1 is IBendApeCoinV1, ERC4626 { 12 | uint256 public constant REWARDS_AMOUNT = 100 * 1e18; 13 | 14 | constructor(IERC20 asset_) ERC20("BendDAO Staked APE", "bstAPE") ERC4626(asset_) {} 15 | 16 | function assetBalanceOf(address account) external view override returns (uint256) { 17 | return convertToAssets(balanceOf(account)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/test/MockBeacon.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | contract MockBeacon { 5 | uint256 internal _nativeFee; 6 | uint256 internal _lzTokenFee; 7 | 8 | function setFees(uint256 nativeFee_, uint256 lzTokenFee_) public { 9 | _nativeFee = nativeFee_; 10 | _lzTokenFee = lzTokenFee_; 11 | } 12 | 13 | function quoteRead( 14 | address baseCollectionAddress, 15 | uint256[] calldata tokenIds, 16 | uint32[] calldata dstEids, 17 | uint128 supplementalGasLimit 18 | ) public view returns (uint256 nativeFee, uint256 lzTokenFee) { 19 | baseCollectionAddress; 20 | tokenIds; 21 | dstEids; 22 | supplementalGasLimit; 23 | 24 | return (_nativeFee, _lzTokenFee); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/test/MockBendLendPoolAddressesProvider.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {ILendPoolAddressesProvider} from "../misc/interfaces/ILendPoolAddressesProvider.sol"; 5 | 6 | contract MockBendLendPoolAddressesProvider is ILendPoolAddressesProvider { 7 | address public lendPool; 8 | address public lendPoolLoan; 9 | 10 | function setLendPool(address lendPool_) public { 11 | lendPool = lendPool_; 12 | } 13 | 14 | function setLendPoolLoan(address lendPoolLoan_) public { 15 | lendPoolLoan = lendPoolLoan_; 16 | } 17 | 18 | function getLendPool() public view returns (address) { 19 | return lendPool; 20 | } 21 | 22 | function getLendPoolLoan() public view returns (address) { 23 | return lendPoolLoan; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/hardhat/helpers/transaction-helper.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-extraneous-import */ 2 | import { TransactionReceipt } from "@ethersproject/abstract-provider"; 3 | import { utils } from "ethers"; 4 | import { Result } from "ethers/lib/utils"; 5 | 6 | export const parseEvents = ( 7 | receipt: TransactionReceipt, 8 | abiInterface: utils.Interface 9 | ): { [eventName: string]: Result } => { 10 | const txEvents: { [eventName: string]: Result } = {}; 11 | for (const log of receipt.logs) { 12 | for (const event of Object.values(abiInterface.events)) { 13 | const topichash = log.topics[0].toLowerCase(); 14 | if (topichash === abiInterface.getEventTopic(event.name)) { 15 | txEvents[event.name] = abiInterface.decodeEventLog(event, log.data, log.topics); 16 | } 17 | } 18 | } 19 | return txEvents; 20 | }; 21 | -------------------------------------------------------------------------------- /contracts/strategy/DefaultRewardsStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 | 6 | import {IRewardsStrategy} from "../interfaces/IRewardsStrategy.sol"; 7 | 8 | contract DefaultRewardsStrategy is IRewardsStrategy, Ownable { 9 | uint256 public constant PERCENTAGE_FACTOR = 1e4; 10 | 11 | uint256 internal _nftShare; 12 | 13 | constructor(uint256 nftShare_) Ownable() { 14 | _nftShare = nftShare_; 15 | } 16 | 17 | function setNftRewardsShare(uint256 nftShare_) public onlyOwner { 18 | require(nftShare_ < PERCENTAGE_FACTOR, "DRS: nft share is too high"); 19 | _nftShare = nftShare_; 20 | } 21 | 22 | function getNftRewardsShare() public view override returns (uint256 nftShare) { 23 | return _nftShare; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /contracts/interfaces/IBendV2Interfaces.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IAddressProviderV2 { 5 | function getPoolManager() external view returns (address); 6 | 7 | function getPoolModuleProxy(uint moduleId) external view returns (address); 8 | } 9 | 10 | interface IPoolLensV2 { 11 | function getUserAssetData( 12 | address user, 13 | uint32 poolId, 14 | address asset 15 | ) 16 | external 17 | view 18 | returns ( 19 | uint256 totalCrossSupply, 20 | uint256 totalIsolateSupply, 21 | uint256 totalCrossBorrow, 22 | uint256 totalIsolateBorrow 23 | ); 24 | 25 | function getERC721TokenData( 26 | uint32 poolId, 27 | address asset, 28 | uint256 tokenId 29 | ) external view returns (address, uint8, address); 30 | } 31 | -------------------------------------------------------------------------------- /abis/IAaveFlashLoanReceiver.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address[]", 6 | "name": "assets", 7 | "type": "address[]" 8 | }, 9 | { 10 | "internalType": "uint256[]", 11 | "name": "amounts", 12 | "type": "uint256[]" 13 | }, 14 | { 15 | "internalType": "uint256[]", 16 | "name": "premiums", 17 | "type": "uint256[]" 18 | }, 19 | { 20 | "internalType": "address", 21 | "name": "initiator", 22 | "type": "address" 23 | }, 24 | { 25 | "internalType": "bytes", 26 | "name": "params", 27 | "type": "bytes" 28 | } 29 | ], 30 | "name": "executeOperation", 31 | "outputs": [ 32 | { 33 | "internalType": "bool", 34 | "name": "", 35 | "type": "bool" 36 | } 37 | ], 38 | "stateMutability": "nonpayable", 39 | "type": "function" 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /abis/ILendPoolLoan.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "loanId", 7 | "type": "uint256" 8 | } 9 | ], 10 | "name": "borrowerOf", 11 | "outputs": [ 12 | { 13 | "internalType": "address", 14 | "name": "", 15 | "type": "address" 16 | } 17 | ], 18 | "stateMutability": "view", 19 | "type": "function" 20 | }, 21 | { 22 | "inputs": [ 23 | { 24 | "internalType": "address", 25 | "name": "nftAsset", 26 | "type": "address" 27 | }, 28 | { 29 | "internalType": "uint256", 30 | "name": "nftTokenId", 31 | "type": "uint256" 32 | } 33 | ], 34 | "name": "getCollateralLoanId", 35 | "outputs": [ 36 | { 37 | "internalType": "uint256", 38 | "name": "", 39 | "type": "uint256" 40 | } 41 | ], 42 | "stateMutability": "view", 43 | "type": "function" 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /contracts/test/MintableERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | /** 7 | * @title ERC20Mintable 8 | * @dev ERC20 minting logic 9 | */ 10 | contract MintableERC20 is ERC20 { 11 | uint8 private _decimals; 12 | 13 | constructor(string memory name, string memory symbol, uint8 decimals_) ERC20(name, symbol) { 14 | _setupDecimals(decimals_); 15 | } 16 | 17 | function _setupDecimals(uint8 decimals_) internal { 18 | _decimals = decimals_; 19 | } 20 | 21 | function decimals() public view virtual override returns (uint8) { 22 | return _decimals; 23 | } 24 | 25 | /** 26 | * @dev Function to mint tokens 27 | * @param value The amount of tokens to mint. 28 | * @return A boolean that indicates if the operation was successful. 29 | */ 30 | function mint(uint256 value) public returns (bool) { 31 | _mint(_msgSender(), value); 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApeCoin Staking V2 2 | 3 | ## Description 4 | 5 | This project contains all smart contracts used for the current BendDAO ApeCoin Staking V2 features. This includes: 6 | 7 | - Pool to Pool Service Model; 8 | - Coin holders deposit ApeCoin; 9 | - NFT holders deposit BAYC/MAYC/BAKC; 10 | - Holders can withdraw assets at any time; 11 | - Holder can borrow ETH after staking; 12 | - The rewards share ratio are determined by strategy contract; 13 | - Bot will automatically do the pairing for the Coin and NFT; 14 | - Bot will automatically do the claim rewards and compounding; 15 | 16 | ## Documentation 17 | 18 | [Docs](https://docs.benddao.xyz/portal/) 19 | 20 | ## Audits 21 | 22 | - [Verilog Solution](https://www.verilog.solutions/audits/benddao_ape_staking_v2/) 23 | 24 | ### Run tests 25 | 26 | - TypeScript tests are included in the `test` folder at the root of this repo. 27 | - Solidity tests are included in the `test` folder in the `contracts` folder. 28 | 29 | ```shell 30 | yarn install 31 | yarn test 32 | ``` 33 | 34 | ### Run static analysis 35 | 36 | ```shell 37 | # install only once 38 | pip3 install slither-analyzer 39 | 40 | slither . 41 | ``` 42 | -------------------------------------------------------------------------------- /contracts/test/BendStakeManagerTester.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {BendStakeManager, IApeCoinStaking} from "../BendStakeManager.sol"; 5 | import {ApeStakingLib} from "../libraries/ApeStakingLib.sol"; 6 | 7 | contract BendStakeManagerTester is BendStakeManager { 8 | function collectFee(uint256 rewardsAmount_) external returns (uint256 feeAmount) { 9 | return _collectFee(rewardsAmount_); 10 | } 11 | 12 | function prepareApeCoin(uint256 amount_) external { 13 | _prepareApeCoin(amount_); 14 | } 15 | 16 | function distributeRewards(address nft_, uint256 rewardsAmount_) external { 17 | _distributePrincipalAndRewards(nft_, 0, rewardsAmount_); 18 | } 19 | 20 | function totalPendingRewardsIncludeFee() external view returns (uint256 amount) { 21 | amount += _pendingRewards(ApeStakingLib.APE_COIN_POOL_ID); 22 | amount += _pendingRewards(ApeStakingLib.BAYC_POOL_ID); 23 | amount += _pendingRewards(ApeStakingLib.MAYC_POOL_ID); 24 | amount += _pendingRewards(ApeStakingLib.BAKC_POOL_ID); 25 | } 26 | 27 | function pendingRewardsIncludeFee(uint256 poolId_) external view returns (uint256 amount) { 28 | amount = _pendingRewards(poolId_); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/misc/interfaces/ILendPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface ILendPool { 5 | function borrow( 6 | address reserveAsset, 7 | uint256 amount, 8 | address nftAsset, 9 | uint256 nftTokenId, 10 | address onBehalfOf, 11 | uint16 referralCode 12 | ) external; 13 | 14 | function repay(address nftAsset, uint256 nftTokenId, uint256 amount) external returns (uint256, bool); 15 | 16 | function redeem(address nftAsset, uint256 nftTokenId, uint256 amount, uint256 bidFine) external returns (uint256); 17 | 18 | function getNftDebtData( 19 | address nftAsset, 20 | uint256 nftTokenId 21 | ) 22 | external 23 | view 24 | returns ( 25 | uint256 loanId, 26 | address reserveAsset, 27 | uint256 totalCollateral, 28 | uint256 totalDebt, 29 | uint256 availableBorrows, 30 | uint256 healthFactor 31 | ); 32 | 33 | function getNftAuctionData( 34 | address nftAsset, 35 | uint256 nftTokenId 36 | ) 37 | external 38 | view 39 | returns (uint256 loanId, address bidderAddress, uint256 bidPrice, uint256 bidBorrowAmount, uint256 bidFine); 40 | } 41 | -------------------------------------------------------------------------------- /test/foundry/UtilitiesHelper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.8.0; 3 | 4 | import "forge-std/Test.sol"; 5 | 6 | // common utilities for forge tests 7 | contract UtilitiesHelper is Test { 8 | bytes32 internal nextUser = keccak256(abi.encodePacked("user address")); 9 | 10 | function getNextUserAddress() external returns (address payable) { 11 | // bytes32 to address conversion 12 | address payable user = payable(address(uint160(uint256(nextUser)))); 13 | nextUser = keccak256(abi.encodePacked(nextUser)); 14 | return user; 15 | } 16 | 17 | // create users with 100 ether balance 18 | function createUsers(uint256 userNum) external returns (address payable[] memory) { 19 | address payable[] memory users = new address payable[](userNum); 20 | for (uint256 i = 0; i < userNum; ++i) { 21 | address payable user = this.getNextUserAddress(); 22 | vm.deal(user, 100 ether); 23 | users[i] = user; 24 | } 25 | return users; 26 | } 27 | 28 | // move block.number forward by a given number of blocks 29 | function mineBlocks(uint256 numBlocks) external { 30 | uint256 targetBlock = block.number + numBlocks; 31 | vm.roll(targetBlock); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/hardhat/DefaultRewardsStrategy.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contracts, Env, makeSuite, Snapshots } from "./setup"; 3 | import { constants } from "ethers"; 4 | import { DefaultRewardsStrategy } from "../../typechain-types"; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 6 | 7 | makeSuite("DefaultRewardsStrategy", (contracts: Contracts, env: Env, snapshots: Snapshots) => { 8 | let hacker: SignerWithAddress; 9 | let lastRevert: string; 10 | let baycStrategy: DefaultRewardsStrategy; 11 | 12 | before(async () => { 13 | hacker = env.accounts[2]; 14 | 15 | lastRevert = "init"; 16 | 17 | baycStrategy = contracts.baycStrategy as DefaultRewardsStrategy; 18 | 19 | await snapshots.capture(lastRevert); 20 | }); 21 | 22 | afterEach(async () => { 23 | if (lastRevert) { 24 | await snapshots.revert(lastRevert); 25 | } 26 | }); 27 | 28 | it("onlyOwner: reverts", async () => { 29 | await expect(baycStrategy.connect(hacker).setNftRewardsShare(constants.AddressZero)).revertedWith( 30 | "Ownable: caller is not the owner" 31 | ); 32 | }); 33 | 34 | it("setNftRewardsShare", async () => { 35 | const newNftShare = 5432; 36 | 37 | await baycStrategy.setNftRewardsShare(newNftShare); 38 | 39 | expect(await contracts.baycStrategy.getNftRewardsShare()).eq(newNftShare); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /abis/IAaveLendPool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "name": "FLASHLOAN_PREMIUM_TOTAL", 5 | "outputs": [ 6 | { 7 | "internalType": "uint256", 8 | "name": "", 9 | "type": "uint256" 10 | } 11 | ], 12 | "stateMutability": "view", 13 | "type": "function" 14 | }, 15 | { 16 | "inputs": [ 17 | { 18 | "internalType": "address", 19 | "name": "receiverAddress", 20 | "type": "address" 21 | }, 22 | { 23 | "internalType": "address[]", 24 | "name": "assets", 25 | "type": "address[]" 26 | }, 27 | { 28 | "internalType": "uint256[]", 29 | "name": "amounts", 30 | "type": "uint256[]" 31 | }, 32 | { 33 | "internalType": "uint256[]", 34 | "name": "modes", 35 | "type": "uint256[]" 36 | }, 37 | { 38 | "internalType": "address", 39 | "name": "onBehalfOf", 40 | "type": "address" 41 | }, 42 | { 43 | "internalType": "bytes", 44 | "name": "params", 45 | "type": "bytes" 46 | }, 47 | { 48 | "internalType": "uint16", 49 | "name": "referralCode", 50 | "type": "uint16" 51 | } 52 | ], 53 | "name": "flashLoan", 54 | "outputs": [], 55 | "stateMutability": "nonpayable", 56 | "type": "function" 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /contracts/test/MockBNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 5 | 6 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 7 | import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 8 | import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; 9 | 10 | /** 11 | * @title MintableERC721 12 | * @dev ERC721 minting logic 13 | */ 14 | contract MockBNFT is ERC721Enumerable, ERC721Holder { 15 | string public baseURI; 16 | address public underlyingAsset; 17 | 18 | constructor(string memory name, string memory symbol, address underlyingAsset_) ERC721(name, symbol) { 19 | baseURI = "https://MockBNFT/"; 20 | underlyingAsset = underlyingAsset_; 21 | } 22 | 23 | function mint(address to, uint256 tokenId) external { 24 | IERC721Enumerable(underlyingAsset).safeTransferFrom(_msgSender(), address(this), tokenId); 25 | 26 | _mint(to, tokenId); 27 | } 28 | 29 | function burn(uint256 tokenId) external { 30 | require(_msgSender() == ownerOf(tokenId), "MockBNFT: not owner"); 31 | 32 | _burn(tokenId); 33 | 34 | IERC721Enumerable(underlyingAsset).safeTransferFrom(address(this), _msgSender(), tokenId); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/interfaces/ICoinPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC4626Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC4626Upgradeable.sol"; 5 | 6 | interface ICoinPool is IERC4626Upgradeable { 7 | event RewardDistributed(uint256 rewardAmount); 8 | 9 | function getWrapApeCoin() external view returns (address); 10 | 11 | function depositNativeSelf() external payable returns (uint256); 12 | 13 | function withdrawNativeSelf(uint256 assets) external returns (uint256); 14 | 15 | function mintSelf(uint256 shares) external returns (uint256); 16 | 17 | function depositSelf(uint256 assets) external returns (uint256); 18 | 19 | function withdrawSelf(uint256 assets) external returns (uint256); 20 | 21 | function redeemSelf(uint256 shares) external returns (uint256); 22 | 23 | function redeemNative(uint256 shares, address receiver, address owner) external returns (uint256); 24 | 25 | function pendingApeCoin() external view returns (uint256); 26 | 27 | function assetBalanceOf(address account_) external view returns (uint256); 28 | 29 | function assetWithdrawableOf(address account) external view returns (uint256); 30 | 31 | function pullApeCoin(uint256 amount_) external; 32 | 33 | function receiveApeCoin(uint256 principalAmount, uint256 rewardsAmount_) external; 34 | 35 | function compoundApeCoin() external; 36 | } 37 | -------------------------------------------------------------------------------- /test/hardhat/helpers/hardhat-keys.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-unsupported-features/es-syntax */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | /* eslint-disable node/no-missing-import */ 4 | 5 | /** 6 | * WARNING!! DO NOT USE IN PRODUCTION OR WITH ANY FUNDS. 7 | * THESE PUBLIC/PRIVATE KEYS COME FROM HARDHAT AND ARE PUBLICLY KNOWN. 8 | */ 9 | export async function findPrivateKey(publicKey: string): Promise { 10 | switch (publicKey.toLowerCase()) { 11 | case "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": 12 | return "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; 13 | 14 | case "0x70997970c51812dc3a010c7d01b50e0d17dc79c8": 15 | return "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; 16 | 17 | case "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc": 18 | return "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; 19 | 20 | case "0x90f79bf6eb2c4f870365e785982e1f101e93b906": 21 | return "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6"; 22 | 23 | case "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65": 24 | return "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a"; 25 | 26 | case "0x71be63f3384f5fb98995898a86b02fb2426c5788": 27 | return "0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82"; 28 | 29 | case "0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc": 30 | return "0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba"; 31 | 32 | default: 33 | throw new Error(`No private key found for ${publicKey}`); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/hardhat/helpers/signature-helper.ts: -------------------------------------------------------------------------------- 1 | import { BigNumberish, utils, Wallet } from "ethers"; 2 | /* eslint-disable node/no-extraneous-import */ 3 | import { TypedDataDomain } from "@ethersproject/abstract-signer"; 4 | /* eslint-disable node/no-extraneous-import */ 5 | import { Signature } from "@ethersproject/bytes"; 6 | /* eslint-disable node/no-extraneous-import */ 7 | import { _TypedDataEncoder } from "@ethersproject/hash"; 8 | 9 | const { defaultAbiCoder, keccak256, solidityPack } = utils; 10 | 11 | /* eslint-disable @typescript-eslint/no-unused-vars */ 12 | /** 13 | * Generate a signature used to generate v, r, s parameters 14 | * @param privateKey privateKey 15 | * @param types solidity types of the value param 16 | * @param values params to be sent to the Solidity function 17 | * @param domain typed data domain 18 | * @returns splitted signature 19 | * @see https://docs.ethers.io/v5/api/signer/#Signer-signTypedData 20 | */ 21 | export const signTypedData = async ( 22 | privateKey: string, 23 | types: string[], 24 | values: (string | boolean | BigNumberish)[], 25 | domain: TypedDataDomain 26 | ): Promise => { 27 | const domainSeparator = _TypedDataEncoder.hashDomain(domain); 28 | 29 | // https://docs.ethers.io/v5/api/utils/abi/coder/#AbiCoder--methods 30 | const hash = keccak256(defaultAbiCoder.encode(types, values)); 31 | 32 | // Compute the digest 33 | const digest = keccak256( 34 | solidityPack(["bytes1", "bytes1", "bytes32", "bytes32"], ["0x19", "0x01", domainSeparator, hash]) 35 | ); 36 | 37 | const adjustedSigner = new Wallet(privateKey); 38 | return { ...adjustedSigner._signingKey().signDigest(digest) }; 39 | }; 40 | 41 | export const computeDomainSeparator = (domain: TypedDataDomain): string => { 42 | return _TypedDataEncoder.hashDomain(domain); 43 | }; 44 | -------------------------------------------------------------------------------- /abis/IPoolLensV2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint32", 6 | "name": "poolId", 7 | "type": "uint32" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "asset", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "tokenId", 17 | "type": "uint256" 18 | } 19 | ], 20 | "name": "getERC721TokenData", 21 | "outputs": [ 22 | { 23 | "internalType": "address", 24 | "name": "", 25 | "type": "address" 26 | }, 27 | { 28 | "internalType": "uint8", 29 | "name": "", 30 | "type": "uint8" 31 | }, 32 | { 33 | "internalType": "address", 34 | "name": "", 35 | "type": "address" 36 | } 37 | ], 38 | "stateMutability": "view", 39 | "type": "function" 40 | }, 41 | { 42 | "inputs": [ 43 | { 44 | "internalType": "address", 45 | "name": "user", 46 | "type": "address" 47 | }, 48 | { 49 | "internalType": "uint32", 50 | "name": "poolId", 51 | "type": "uint32" 52 | }, 53 | { 54 | "internalType": "address", 55 | "name": "asset", 56 | "type": "address" 57 | } 58 | ], 59 | "name": "getUserAssetData", 60 | "outputs": [ 61 | { 62 | "internalType": "uint256", 63 | "name": "totalCrossSupply", 64 | "type": "uint256" 65 | }, 66 | { 67 | "internalType": "uint256", 68 | "name": "totalIsolateSupply", 69 | "type": "uint256" 70 | }, 71 | { 72 | "internalType": "uint256", 73 | "name": "totalCrossBorrow", 74 | "type": "uint256" 75 | }, 76 | { 77 | "internalType": "uint256", 78 | "name": "totalIsolateBorrow", 79 | "type": "uint256" 80 | } 81 | ], 82 | "stateMutability": "view", 83 | "type": "function" 84 | } 85 | ] 86 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakedNft.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC721MetadataUpgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC721MetadataUpgradeable.sol"; 5 | import {IERC721EnumerableUpgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC721EnumerableUpgradeable.sol"; 6 | 7 | interface IStakedNft is IERC721MetadataUpgradeable, IERC721EnumerableUpgradeable { 8 | event Minted(address indexed to, uint256[] tokenId); 9 | event Burned(address indexed from, uint256[] tokenId); 10 | 11 | function authorise(address addr_, bool authorized_) external; 12 | 13 | function mint(address to, uint256[] calldata tokenIds) external; 14 | 15 | function burn(address from, uint256[] calldata tokenIds) external; 16 | 17 | /** 18 | * @dev Returns the staker of the `tokenId` token. 19 | */ 20 | function stakerOf(uint256 tokenId) external view returns (address); 21 | 22 | /** 23 | * @dev Returns a token ID owned by `staker` at a given `index` of its token list. 24 | * Use along with {totalStaked} to enumerate all of ``staker``'s tokens. 25 | */ 26 | 27 | function tokenOfStakerByIndex(address staker, uint256 index) external view returns (uint256); 28 | 29 | /** 30 | * @dev Returns the total staked amount of tokens for staker. 31 | */ 32 | function totalStaked(address staker) external view returns (uint256); 33 | 34 | function underlyingAsset() external view returns (address); 35 | 36 | function setBnftRegistry(address bnftRegistry_) external; 37 | 38 | function setDelegateCash(address delegate, uint256[] calldata tokenIds, bool value) external; 39 | 40 | function getDelegateCashForToken(uint256[] calldata tokenIds_) external view returns (address[][] memory); 41 | 42 | function setDelegateCashV2(address delegate, uint256[] calldata tokenIds, bool value) external; 43 | 44 | function getDelegateCashForTokenV2(uint256[] calldata tokenIds_) external view returns (address[][] memory); 45 | } 46 | -------------------------------------------------------------------------------- /contracts/interfaces/INftPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | import {IApeCoinStaking} from "./IApeCoinStaking.sol"; 4 | import {IStakeManager} from "./IStakeManager.sol"; 5 | import {IStakedNft} from "./IStakedNft.sol"; 6 | 7 | interface INftPool { 8 | event NftRewardDistributed(address indexed nft, uint256 rewardAmount); 9 | 10 | event NftRewardClaimed( 11 | address indexed nft, 12 | uint256[] tokenIds, 13 | address indexed receiver, 14 | uint256 amount, 15 | uint256 rewardsDebt 16 | ); 17 | 18 | event NftDeposited(address indexed nft, uint256[] tokenIds, address indexed owner); 19 | 20 | event NftWithdrawn(address indexed nft, uint256[] tokenIds, address indexed owner); 21 | 22 | struct PoolState { 23 | IStakedNft stakedNft; 24 | uint256 accumulatedRewardsPerNft; 25 | mapping(uint256 => uint256) rewardsDebt; 26 | uint256 pendingApeCoin; 27 | } 28 | 29 | struct PoolUI { 30 | uint256 totalStakedNft; 31 | uint256 accumulatedRewardsPerNft; 32 | uint256 pendingApeCoin; 33 | } 34 | 35 | function claimable(address[] calldata nfts_, uint256[][] calldata tokenIds_) external view returns (uint256); 36 | 37 | function staker() external view returns (IStakeManager); 38 | 39 | function deposit(address[] calldata nfts_, uint256[][] calldata tokenIds_, address owner_) external; 40 | 41 | function withdraw(address[] calldata nfts_, uint256[][] calldata tokenIds_, address owner_) external; 42 | 43 | function claim(address[] calldata nfts_, uint256[][] calldata tokenIds_) external; 44 | 45 | function receiveApeCoin(address nft_, uint256 rewardsAmount_) external; 46 | 47 | function compoundApeCoin(address nft_) external; 48 | 49 | function pendingApeCoin(address nft_) external view returns (uint256); 50 | 51 | function getPoolStateUI(address nft_) external view returns (PoolUI memory); 52 | 53 | function getNftStateUI(address nft_, uint256 tokenId) external view returns (uint256 rewardsDebt); 54 | } 55 | -------------------------------------------------------------------------------- /deployments/deployed-contracts-apechain.json: -------------------------------------------------------------------------------- 1 | { 2 | "NftVault": { 3 | "address": "0x79d922DD382E42A156bC0A354861cDBC4F09110d", 4 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 5 | }, 6 | "StBAYC": { 7 | "address": "0xBbB884c0A67d44CB2380FC3F1Df6Ee820e3286c5", 8 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 9 | }, 10 | "StMAYC": { 11 | "address": "0x501c991E0D31D408c25bCf00da27BdF2759A394a", 12 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 13 | }, 14 | "StBAKC": { 15 | "address": "0x8b4117E91a6f6A810651aFae788EC044f13a06a5", 16 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 17 | }, 18 | "BendCoinPool": { 19 | "address": "0x24451F47CaF13B24f4b5034e1dF6c0E401ec0e46", 20 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 21 | }, 22 | "BendNftPool": { 23 | "address": "0x66662bC916BbCEc60F7E88cB849F13A2dbf51BE2", 24 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 25 | }, 26 | "BendStakeManager": { 27 | "address": "0x40E7Df7189Ef33711a4B0BFc3B4FDc7678B40d55", 28 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 29 | }, 30 | "BaycRewardsStrategy": { 31 | "address": "0x39C3EFB6075a26d62eBCe532c87147b5d1d8D68D", 32 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 33 | }, 34 | "MaycRewardsStrategy": { 35 | "address": "0x59826860CFe9E1ebE6163e4eb9F549A2404bfa83", 36 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 37 | }, 38 | "BakcRewardsStrategy": { 39 | "address": "0xfd5D311Ec71B9c441E1eF822081209a29a5D032F", 40 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 41 | }, 42 | "DefaultWithdrawStrategy": { 43 | "address": "0x1967B19d1424C3350eE8eC9d5f62cc40Af2182E9", 44 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 45 | }, 46 | "PoolViewer": { 47 | "address": "0xa25f86d18818830A0586769f69CB2b61f76dcEbd", 48 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 49 | }, 50 | "BendApeCoinStakedVoting": { 51 | "address": "0xC0db6A46B34060C73A17552487bFeBB1FDA85191", 52 | "deployer": "0x868964fa49a6fd6e116FE82c8f4165904406f479" 53 | } 54 | } -------------------------------------------------------------------------------- /test/hardhat/PoolViewer.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contracts, Env, makeSuite, Snapshots } from "./setup"; 3 | import { makeBNWithDecimals } from "./utils"; 4 | import { constants } from "ethers"; 5 | 6 | makeSuite("PoolViewer", (contracts: Contracts, env: Env, snapshots: Snapshots) => { 7 | let lastRevert: string; 8 | 9 | before(async () => { 10 | const apeAmountForStakingV1 = makeBNWithDecimals(100000, 18); 11 | await contracts.wrapApeCoin.connect(env.admin).deposit({ value: apeAmountForStakingV1 }); 12 | await contracts.wrapApeCoin 13 | .connect(env.admin) 14 | .transfer(contracts.mockStakeManagerV1.address, apeAmountForStakingV1); 15 | 16 | lastRevert = "init"; 17 | 18 | await snapshots.capture(lastRevert); 19 | }); 20 | 21 | afterEach(async () => { 22 | if (lastRevert) { 23 | await snapshots.revert(lastRevert); 24 | } 25 | }); 26 | 27 | it("deposit: preparing the first deposit", async () => { 28 | await contracts.wrapApeCoin.connect(env.feeRecipient).approve(contracts.bendCoinPool.address, constants.MaxUint256); 29 | await contracts.bendCoinPool.connect(env.feeRecipient).depositSelf(makeBNWithDecimals(1, 18)); 30 | expect(await contracts.bendCoinPool.totalSupply()).gt(0); 31 | 32 | lastRevert = "init"; 33 | await snapshots.capture(lastRevert); 34 | }); 35 | 36 | it("pending rewards", async () => { 37 | const viewerStatsPool = await contracts.poolViewer.viewPoolPendingRewards(); 38 | expect(viewerStatsPool.baycPoolRewards).eq(0); 39 | 40 | const viewerStatsUser = await contracts.poolViewer.viewUserPendingRewards(env.feeRecipient.address); 41 | expect(viewerStatsUser.baycPoolRewards).eq(0); 42 | }); 43 | 44 | it("pending rewards for bend v2", async () => { 45 | const viewerStatsUser1 = await contracts.poolViewer.viewUserPendingRewardsForBendV2(env.feeRecipient.address, []); 46 | expect(viewerStatsUser1.baycPoolRewards).eq(0); 47 | 48 | const viewerStatsUser2 = await contracts.poolViewer.viewUserPendingRewardsForBendV2( 49 | env.feeRecipient.address, 50 | [1, 2, 3] 51 | ); 52 | expect(viewerStatsUser2.baycPoolRewards).eq(0); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /contracts/test/MockAaveLendPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | 6 | import {IAaveLendPool} from "../misc/interfaces/IAaveLendPool.sol"; 7 | import {IAaveFlashLoanReceiver} from "../misc/interfaces/IAaveFlashLoanReceiver.sol"; 8 | 9 | contract MockAaveLendPool is IAaveLendPool { 10 | uint256 private _flashLoanPremiumTotal; 11 | 12 | constructor() { 13 | _flashLoanPremiumTotal = 9; 14 | } 15 | 16 | function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint256) { 17 | return _flashLoanPremiumTotal; 18 | } 19 | 20 | struct FlashLoanLocalVars { 21 | IAaveFlashLoanReceiver receiver; 22 | uint256 i; 23 | address currentAsset; 24 | uint256 currentAmount; 25 | uint256 currentPremium; 26 | uint256 currentAmountPlusPremium; 27 | } 28 | 29 | function flashLoan( 30 | address receiverAddress, 31 | address[] calldata assets, 32 | uint256[] calldata amounts, 33 | uint256[] calldata /*modes*/, 34 | address /*onBehalfOf*/, 35 | bytes calldata params, 36 | uint16 /*referralCode*/ 37 | ) external { 38 | FlashLoanLocalVars memory vars; 39 | vars.receiver = IAaveFlashLoanReceiver(receiverAddress); 40 | 41 | uint256[] memory premiums = new uint256[](assets.length); 42 | 43 | for (vars.i = 0; vars.i < assets.length; vars.i++) { 44 | premiums[vars.i] = (amounts[vars.i] * _flashLoanPremiumTotal) / 10000; 45 | 46 | IERC20(assets[vars.i]).transfer(receiverAddress, amounts[vars.i]); 47 | } 48 | 49 | require( 50 | vars.receiver.executeOperation(assets, amounts, premiums, msg.sender, params), 51 | "AaveLendPool: Flashloan execution failed" 52 | ); 53 | 54 | for (vars.i = 0; vars.i < assets.length; vars.i++) { 55 | vars.currentAsset = assets[vars.i]; 56 | vars.currentAmount = amounts[vars.i]; 57 | vars.currentPremium = premiums[vars.i]; 58 | vars.currentAmountPlusPremium = vars.currentAmount + vars.currentPremium; 59 | 60 | IERC20(vars.currentAsset).transferFrom(receiverAddress, vars.currentAsset, vars.currentAmountPlusPremium); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /contracts/test/MockBendLendPoolLoan.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {ILendPoolLoan} from "../misc/interfaces/ILendPoolLoan.sol"; 5 | 6 | contract MockBendLendPoolLoan is ILendPoolLoan { 7 | struct LoanData { 8 | address borrower; 9 | address reserveAsset; 10 | uint256 totalDebt; 11 | uint256 bidFine; 12 | } 13 | 14 | mapping(address => mapping(uint256 => uint256)) private _loanIds; 15 | mapping(uint256 => LoanData) private _loanDatas; 16 | uint256 private _loanId; 17 | 18 | function setLoanData( 19 | address nftAsset, 20 | uint256 nftTokenId, 21 | address borrower, 22 | address reserveAsset, 23 | uint256 totalDebt 24 | ) public { 25 | uint256 loanId = _loanIds[nftAsset][nftTokenId]; 26 | if (loanId == 0) { 27 | loanId = ++_loanId; 28 | _loanIds[nftAsset][nftTokenId] = loanId; 29 | _loanDatas[loanId] = LoanData(borrower, reserveAsset, totalDebt, 0); 30 | } else { 31 | _loanDatas[loanId] = LoanData(borrower, reserveAsset, totalDebt, 0); 32 | } 33 | } 34 | 35 | function setBidFine(uint256 loanId, uint256 bidFine) public { 36 | _loanDatas[loanId].bidFine = bidFine; 37 | } 38 | 39 | function setTotalDebt(uint256 loanId, uint256 totalDebt) public { 40 | _loanDatas[loanId].totalDebt = totalDebt; 41 | } 42 | 43 | function deleteLoanData(address nftAsset, uint256 nftTokenId) public { 44 | uint256 loanId = _loanIds[nftAsset][nftTokenId]; 45 | delete _loanIds[nftAsset][nftTokenId]; 46 | delete _loanDatas[loanId]; 47 | } 48 | 49 | function getLoanData( 50 | uint256 loanId 51 | ) public view returns (address borrower, address reserveAsset, uint256 totalDebt, uint256 bidFine) { 52 | LoanData memory loanData = _loanDatas[loanId]; 53 | borrower = loanData.borrower; 54 | reserveAsset = loanData.reserveAsset; 55 | totalDebt = loanData.totalDebt; 56 | bidFine = loanData.bidFine; 57 | } 58 | 59 | function getCollateralLoanId(address nftAsset, uint256 nftTokenId) public view returns (uint256) { 60 | return _loanIds[nftAsset][nftTokenId]; 61 | } 62 | 63 | function borrowerOf(uint256 loanId) public view returns (address) { 64 | return _loanDatas[loanId].borrower; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/foundry/BendStakeManagerAsync.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | import "./SetupHelper.sol"; 4 | 5 | contract BendStakeManagerAsyncTest is SetupHelper { 6 | function setUp() public override { 7 | super.setUp(); 8 | 9 | // configure apecoin staking to async mode 10 | mockBeacon.setFees(399572542890580499, 0); 11 | vm.deal(address(nftVault), 100 ether); 12 | } 13 | 14 | function test_async_StakeBAYC() public { 15 | address testUser = testUsers[0]; 16 | uint256 depositCoinAmount = 1_000_000 * 1e18; 17 | uint256[] memory testBaycTokenIds = new uint256[](1); 18 | 19 | // deposit some coins 20 | vm.startPrank(testUser); 21 | mockWAPE.deposit{value: depositCoinAmount}(); 22 | mockWAPE.approve(address(coinPool), depositCoinAmount); 23 | coinPool.deposit(depositCoinAmount, testUser); 24 | vm.stopPrank(); 25 | 26 | // deposit some nfts 27 | vm.startPrank(testUser); 28 | mockBAYC.setApprovalForAll(address(nftPool), true); 29 | 30 | testBaycTokenIds[0] = 100; 31 | mockBAYC.mint(testBaycTokenIds[0]); 32 | 33 | address[] memory nfts = new address[](1); 34 | uint256[][] memory tokenIds = new uint256[][](1); 35 | nfts[0] = address(mockBAYC); 36 | tokenIds[0] = testBaycTokenIds; 37 | 38 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[0]); 39 | 40 | vm.stopPrank(); 41 | 42 | mockBAYC.setLocked(testBaycTokenIds[0], true); 43 | 44 | // bot do some operations 45 | vm.startPrank(botAdmin); 46 | stakeManager.depositNft(nfts, tokenIds, testUser); 47 | 48 | stakeManager.stakeBayc(testBaycTokenIds); 49 | 50 | // make some rewards 51 | advanceTimeAndBlock(2 hours, 100); 52 | 53 | bytes32 guidClaim = mockBAYC.getNextGUID(); 54 | stakeManager.claimBayc(testBaycTokenIds); 55 | 56 | mockBAYC.executeCallback(address(mockApeStaking), guidClaim); 57 | 58 | stakeManager.distributePendingFunds(); 59 | 60 | // make some rewards 61 | advanceTimeAndBlock(2 hours, 100); 62 | 63 | bytes32 guidUnstake = mockBAYC.getNextGUID(); 64 | stakeManager.unstakeBayc(testBaycTokenIds); 65 | 66 | mockBAYC.executeCallback(address(mockApeStaking), guidUnstake); 67 | 68 | stakeManager.distributePendingFunds(); 69 | 70 | vm.stopPrank(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /abis/DefaultRewardsStrategy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "uint256", 6 | "name": "nftShare_", 7 | "type": "uint256" 8 | } 9 | ], 10 | "stateMutability": "nonpayable", 11 | "type": "constructor" 12 | }, 13 | { 14 | "anonymous": false, 15 | "inputs": [ 16 | { 17 | "indexed": true, 18 | "internalType": "address", 19 | "name": "previousOwner", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": true, 24 | "internalType": "address", 25 | "name": "newOwner", 26 | "type": "address" 27 | } 28 | ], 29 | "name": "OwnershipTransferred", 30 | "type": "event" 31 | }, 32 | { 33 | "inputs": [], 34 | "name": "PERCENTAGE_FACTOR", 35 | "outputs": [ 36 | { 37 | "internalType": "uint256", 38 | "name": "", 39 | "type": "uint256" 40 | } 41 | ], 42 | "stateMutability": "view", 43 | "type": "function" 44 | }, 45 | { 46 | "inputs": [], 47 | "name": "getNftRewardsShare", 48 | "outputs": [ 49 | { 50 | "internalType": "uint256", 51 | "name": "nftShare", 52 | "type": "uint256" 53 | } 54 | ], 55 | "stateMutability": "view", 56 | "type": "function" 57 | }, 58 | { 59 | "inputs": [], 60 | "name": "owner", 61 | "outputs": [ 62 | { 63 | "internalType": "address", 64 | "name": "", 65 | "type": "address" 66 | } 67 | ], 68 | "stateMutability": "view", 69 | "type": "function" 70 | }, 71 | { 72 | "inputs": [], 73 | "name": "renounceOwnership", 74 | "outputs": [], 75 | "stateMutability": "nonpayable", 76 | "type": "function" 77 | }, 78 | { 79 | "inputs": [ 80 | { 81 | "internalType": "uint256", 82 | "name": "nftShare_", 83 | "type": "uint256" 84 | } 85 | ], 86 | "name": "setNftRewardsShare", 87 | "outputs": [], 88 | "stateMutability": "nonpayable", 89 | "type": "function" 90 | }, 91 | { 92 | "inputs": [ 93 | { 94 | "internalType": "address", 95 | "name": "newOwner", 96 | "type": "address" 97 | } 98 | ], 99 | "name": "transferOwnership", 100 | "outputs": [], 101 | "stateMutability": "nonpayable", 102 | "type": "function" 103 | } 104 | ] 105 | -------------------------------------------------------------------------------- /contracts/interfaces/IDelegateRegistryV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | pragma solidity 0.8.18; 3 | 4 | /** 5 | * @title IDelegateRegistryV2 6 | * @custom:version 2.0 7 | * @custom:author foobar (0xfoobar) 8 | * @notice A standalone immutable registry storing delegated permissions from one address to another 9 | */ 10 | interface IDelegateRegistryV2 { 11 | /// @notice Delegation type, NONE is used when a delegation does not exist or is revoked 12 | enum DelegationType { 13 | NONE, 14 | ALL, 15 | CONTRACT, 16 | ERC721, 17 | ERC20, 18 | ERC1155 19 | } 20 | 21 | /// @notice Struct for returning delegations 22 | struct Delegation { 23 | DelegationType type_; 24 | address to; 25 | address from; 26 | bytes32 rights; 27 | address contract_; 28 | uint256 tokenId; 29 | uint256 amount; 30 | } 31 | 32 | /** 33 | * ----------- WRITE ----------- 34 | */ 35 | 36 | /** 37 | * @notice Allow the delegate to act on behalf of `msg.sender` for a specific ERC721 token 38 | * @param to The address to act as delegate 39 | * @param contract_ The contract whose rights are being delegated 40 | * @param tokenId The token id to delegate 41 | * @param rights Specific subdelegation rights granted to the delegate, pass an empty bytestring to encompass all rights 42 | * @param enable Whether to enable or disable this delegation, true delegates and false revokes 43 | * @return delegationHash The unique identifier of the delegation 44 | */ 45 | function delegateERC721( 46 | address to, 47 | address contract_, 48 | uint256 tokenId, 49 | bytes32 rights, 50 | bool enable 51 | ) external payable returns (bytes32 delegationHash); 52 | 53 | /** 54 | * ----------- ENUMERATIONS ----------- 55 | */ 56 | 57 | /** 58 | * @notice Returns all enabled delegations an address has given out 59 | * @param from The address to retrieve delegations for 60 | * @return delegations Array of Delegation structs 61 | */ 62 | function getOutgoingDelegations(address from) external view returns (Delegation[] memory delegations); 63 | 64 | /** 65 | * @notice Returns the delegations for a given array of delegation hashes 66 | * @param delegationHashes is an array of hashes that correspond to delegations 67 | * @return delegations Array of Delegation structs, return empty structs for nonexistent or revoked delegations 68 | */ 69 | function getDelegationsFromHashes( 70 | bytes32[] calldata delegationHashes 71 | ) external view returns (Delegation[] memory delegations); 72 | } 73 | -------------------------------------------------------------------------------- /contracts/interfaces/IApeCoinStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | interface IApeCoinStaking { 5 | struct Pool { 6 | uint48 lastRewardedTimestampHour; 7 | uint16 lastRewardsRangeIndex; 8 | uint96 stakedAmount; 9 | uint96 accumulatedRewardsPerShare; 10 | TimeRange[] timeRanges; 11 | } 12 | 13 | struct TimeRange { 14 | uint48 startTimestampHour; 15 | uint48 endTimestampHour; 16 | uint96 rewardsPerHour; 17 | uint96 capPerPosition; 18 | } 19 | 20 | struct PoolWithoutTimeRange { 21 | uint48 lastRewardedTimestampHour; 22 | uint16 lastRewardsRangeIndex; 23 | uint96 stakedAmount; 24 | uint96 accumulatedRewardsPerShare; 25 | } 26 | 27 | struct PoolUI { 28 | uint256 poolId; 29 | uint256 stakedAmount; 30 | TimeRange currentTimeRange; 31 | } 32 | 33 | struct Position { 34 | uint256 stakedAmount; 35 | int256 rewardsDebt; 36 | } 37 | 38 | // public varaiables 39 | function pools(uint256 poolId_) external view returns (PoolWithoutTimeRange memory); 40 | 41 | function nftContracts(uint256 poolId_) external view returns (address); 42 | 43 | function nftPosition(uint256 poolId_, uint256 tokenId_) external view returns (Position memory); 44 | 45 | // public read methods 46 | function getTimeRangeBy(uint256 _poolId, uint256 _index) external view returns (TimeRange memory); 47 | 48 | function rewardsBy(uint256 _poolId, uint256 _from, uint256 _to) external view returns (uint256, uint256); 49 | 50 | function getPoolsUI() external view returns (PoolUI memory, PoolUI memory, PoolUI memory); 51 | 52 | function stakedTotal( 53 | uint256[] memory baycTokenIds, 54 | uint256[] memory maycTokenIds, 55 | uint256[] memory bakcTokenIds 56 | ) external view returns (uint256 total); 57 | 58 | function pendingRewards(uint256 _poolId, uint256 _tokenId) external view returns (uint256); 59 | 60 | function pendingClaims( 61 | bytes32 guid 62 | ) external view returns (uint8 poolId, uint8 requestType, address caller, address recipient, uint96 numNfts); 63 | 64 | function quoteRequest(uint256 poolId, uint256[] calldata tokenIds) external view returns (uint256 fee); 65 | 66 | // public write methods 67 | function deposit(uint256 poolId, uint256[] calldata tokenIds, uint256[] calldata amounts) external payable; 68 | 69 | function claim(uint256 poolId, uint256[] calldata tokenIds, address recipient) external payable; 70 | 71 | function withdraw( 72 | uint256 poolId, 73 | uint256[] calldata tokenIds, 74 | uint256[] calldata amounts, 75 | address recipient 76 | ) external payable; 77 | } 78 | -------------------------------------------------------------------------------- /test/foundry/BendNftPool.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | import "./SetupHelper.sol"; 4 | 5 | contract BendNftPoolTest is SetupHelper { 6 | function setUp() public override { 7 | super.setUp(); 8 | } 9 | 10 | function testSingleUserDepositWithdrawBAYCNoRewards() public { 11 | address testUser = testUsers[0]; 12 | uint256[] memory testBaycTokenIds = new uint256[](1); 13 | 14 | vm.startPrank(testUser); 15 | 16 | mockBAYC.setApprovalForAll(address(nftPool), true); 17 | 18 | testBaycTokenIds[0] = 100; 19 | mockBAYC.mint(testBaycTokenIds[0]); 20 | address[] memory nfts = new address[](1); 21 | uint256[][] memory tokenIds = new uint256[][](1); 22 | 23 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[0]); 24 | 25 | vm.stopPrank(); 26 | 27 | nfts[0] = address(mockBAYC); 28 | tokenIds[0] = testBaycTokenIds; 29 | vm.prank(address(stakeManager)); 30 | nftPool.deposit(nfts, tokenIds, address(testUser)); 31 | 32 | // make some rewards 33 | advanceTimeAndBlock(12 hours, 100); 34 | 35 | vm.prank(address(testUser)); 36 | nftPool.claim(nfts, tokenIds); 37 | 38 | vm.prank(address(stakeManager)); 39 | nftPool.withdraw(nfts, tokenIds, address(testUser)); 40 | } 41 | 42 | function testSingleUserBatchDepositWithdrawBAYCNoRewards() public { 43 | address testUser = testUsers[0]; 44 | uint256[] memory testBaycTokenIds = new uint256[](3); 45 | 46 | vm.startPrank(testUser); 47 | 48 | mockBAYC.setApprovalForAll(address(nftPool), true); 49 | 50 | testBaycTokenIds[0] = 100; 51 | mockBAYC.mint(testBaycTokenIds[0]); 52 | 53 | testBaycTokenIds[1] = 200; 54 | mockBAYC.mint(testBaycTokenIds[1]); 55 | 56 | testBaycTokenIds[2] = 300; 57 | mockBAYC.mint(testBaycTokenIds[2]); 58 | 59 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[0]); 60 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[1]); 61 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[2]); 62 | 63 | vm.stopPrank(); 64 | 65 | address[] memory nfts = new address[](1); 66 | uint256[][] memory tokenIds = new uint256[][](1); 67 | 68 | nfts[0] = address(mockBAYC); 69 | tokenIds[0] = testBaycTokenIds; 70 | 71 | vm.prank(address(stakeManager)); 72 | nftPool.deposit(nfts, tokenIds, address(testUser)); 73 | 74 | // make some rewards 75 | advanceTimeAndBlock(12 hours, 100); 76 | 77 | vm.prank(address(testUser)); 78 | nftPool.claim(nfts, tokenIds); 79 | 80 | vm.prank(address(stakeManager)); 81 | nftPool.withdraw(nfts, tokenIds, address(testUser)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /test/hardhat/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable node/no-extraneous-import */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | 5 | import fc, { ShuffledSubarrayConstraints } from "fast-check"; 6 | import { ethers } from "hardhat"; 7 | import { BigNumber, Contract } from "ethers"; 8 | import { MintableERC721 } from "../../typechain-types"; 9 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 10 | import { advanceBlock, latest, increaseTo, increaseBy } from "./helpers/block-traveller"; 11 | 12 | export function makeBN18(num: any): BigNumber { 13 | return ethers.utils.parseUnits(num.toString(), 18); 14 | } 15 | 16 | export function makeBNWithDecimals(num: any, decimals: any): BigNumber { 17 | return ethers.utils.parseUnits(num.toString(), decimals); 18 | } 19 | 20 | export const getSignerByAddress = async (address: string): Promise => ethers.getSigner(address); 21 | 22 | export const getContract = async ( 23 | contractName: string, 24 | address: string 25 | ): Promise => (await ethers.getContractAt(contractName, address)) as ContractType; 26 | 27 | export const mintNft = async (owner: SignerWithAddress, nft: MintableERC721, tokenIds: number[]): Promise => { 28 | for (let id of tokenIds) { 29 | await nft.connect(owner).mint(id); 30 | } 31 | }; 32 | 33 | const skipHourBlocks = async (tolerance: number) => { 34 | const currentTime = await latest(); 35 | // skip hour blocks 36 | if (currentTime % 3600 >= 3600 - tolerance) { 37 | await increaseTo(Math.round(currentTime / 3600) * 3600 + 1); 38 | await advanceBlock(); 39 | } 40 | }; 41 | 42 | export const advanceHours = async (hours: number) => { 43 | await increaseBy(randomUint(3600, 3600 * hours)); 44 | await advanceBlock(); 45 | await skipHourBlocks(60); 46 | }; 47 | 48 | export const randomUint = (min: number, max: number) => { 49 | return fc.sample(fc.integer({ min, max }), 1)[0]; 50 | }; 51 | 52 | export const shuffledSubarray = (originalArray: number[], constraints?: ShuffledSubarrayConstraints) => { 53 | return fc.sample( 54 | fc.shuffledSubarray(originalArray, constraints || { minLength: 1, maxLength: originalArray.length }), 55 | 1 56 | )[0]; 57 | }; 58 | 59 | export const randomItem = (originalArray: number[]) => { 60 | return fc.sample(fc.constantFrom(...originalArray), 1)[0]; 61 | }; 62 | 63 | export async function deployContract( 64 | contractName: string, 65 | args: any[], 66 | libraries?: { [libraryName: string]: string } 67 | ): Promise { 68 | // console.log("deployContract:", contractName, args, libraries); 69 | const instance = await (await ethers.getContractFactory(contractName, { libraries })).deploy(...args); 70 | return instance as ContractType; 71 | } 72 | -------------------------------------------------------------------------------- /contracts/test/MockDelegationRegistryV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: agpl-3.0 2 | pragma solidity 0.8.18; 3 | 4 | import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; 5 | 6 | import {IDelegateRegistryV2} from "../interfaces/IDelegateRegistryV2.sol"; 7 | 8 | contract MockDelegationRegistryV2 is IDelegateRegistryV2 { 9 | using EnumerableSet for EnumerableSet.Bytes32Set; 10 | 11 | /// @dev Only this mapping should be used to verify delegations; the other mapping arrays are for enumerations 12 | mapping(bytes32 => Delegation) internal allDelegations; 13 | 14 | /// @dev Vault delegation enumeration outbox, for pushing new hashes only 15 | mapping(address => EnumerableSet.Bytes32Set) internal outgoingDelegationHashes; 16 | 17 | /// @dev Delegate enumeration inbox, for pushing new hashes only 18 | mapping(address => EnumerableSet.Bytes32Set) internal incomingDelegationHashes; 19 | 20 | function delegateERC721( 21 | address to, 22 | address contract_, 23 | uint256 tokenId, 24 | bytes32 rights, 25 | bool enable 26 | ) public payable override returns (bytes32 delegationHash) { 27 | delegationHash = keccak256(abi.encodePacked(rights, msg.sender, to, contract_, tokenId)); 28 | if (enable) { 29 | allDelegations[delegationHash] = Delegation( 30 | DelegationType.ERC721, 31 | to, 32 | msg.sender, 33 | rights, 34 | contract_, 35 | tokenId, 36 | 0 37 | ); 38 | 39 | outgoingDelegationHashes[msg.sender].add(delegationHash); 40 | incomingDelegationHashes[to].add(delegationHash); 41 | } else { 42 | delete allDelegations[delegationHash]; 43 | 44 | outgoingDelegationHashes[msg.sender].remove(delegationHash); 45 | incomingDelegationHashes[to].remove(delegationHash); 46 | } 47 | } 48 | 49 | function getOutgoingDelegations(address from) external view override returns (Delegation[] memory delegations) { 50 | bytes32[] memory delegationHashes = outgoingDelegationHashes[from].values(); 51 | return _getDelegationsFromHashes(delegationHashes); 52 | } 53 | 54 | function getDelegationsFromHashes( 55 | bytes32[] calldata delegationHashes 56 | ) public view override returns (Delegation[] memory delegations) { 57 | return _getDelegationsFromHashes(delegationHashes); 58 | } 59 | 60 | function _getDelegationsFromHashes( 61 | bytes32[] memory delegationHashes 62 | ) private view returns (Delegation[] memory delegations) { 63 | delegations = new Delegation[](delegationHashes.length); 64 | for (uint256 i = 0; i < delegationHashes.length; ++i) { 65 | delegations[i] = allDelegations[delegationHashes[i]]; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/hardhat/helpers/block-traveller.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "ethers"; 2 | import { ethers, network } from "hardhat"; 3 | 4 | /** 5 | * Advance the state by one block 6 | */ 7 | export async function advanceBlock(): Promise { 8 | await network.provider.send("evm_mine"); 9 | } 10 | 11 | /** 12 | * Advance the block to the passed target block 13 | * @param targetBlock target block number 14 | * @dev If target block is lower/equal to current block, it throws an error 15 | */ 16 | export async function advanceBlockTo(targetBlock: number): Promise { 17 | const currentBlock = await ethers.provider.getBlockNumber(); 18 | if (targetBlock < currentBlock) { 19 | throw Error(`Target·block·#(${targetBlock})·is·lower·than·current·block·#(${currentBlock})`); 20 | } 21 | 22 | let numberBlocks = targetBlock - currentBlock; 23 | 24 | // hardhat_mine only can move by 256 blocks (256 in hex is 0x100) 25 | while (numberBlocks >= 256) { 26 | await network.provider.send("hardhat_mine", ["0x100"]); 27 | numberBlocks = numberBlocks - 256; 28 | } 29 | 30 | if (numberBlocks === 1) { 31 | await network.provider.send("evm_mine"); 32 | } else if (numberBlocks === 15) { 33 | // Issue with conversion from hexString of 15 (0x0f instead of 0xF) 34 | await network.provider.send("hardhat_mine", ["0xF"]); 35 | } else { 36 | await network.provider.send("hardhat_mine", [BigNumber.from(numberBlocks).toHexString()]); 37 | } 38 | } 39 | 40 | /** 41 | * Advance the block time to target time 42 | * @param targetTime target time (epoch) 43 | * @dev If target time is lower/equal to current time, it throws an error 44 | */ 45 | export async function increaseTo(targetTime: number): Promise { 46 | const currentTime = await latest(); 47 | if (targetTime < currentTime) { 48 | throw Error(`Target time: (${targetTime}) is lower than current time: #(${currentTime})`); 49 | } 50 | 51 | await network.provider.send("evm_setNextBlockTimestamp", [BigNumber.from(targetTime).toHexString()]); 52 | } 53 | 54 | /** 55 | * Advance the block time 56 | * @param targetTime target time (epoch) 57 | * @dev If target time is lower/equal to current time, it throws an error 58 | */ 59 | export async function increaseBy(targetTime: number): Promise { 60 | const currentTime = await latest(); 61 | await network.provider.send("evm_setNextBlockTimestamp", [BigNumber.from(currentTime).add(targetTime).toHexString()]); 62 | } 63 | 64 | /** 65 | * Fetch the current block number 66 | */ 67 | export async function latest(): Promise { 68 | return (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; 69 | } 70 | 71 | /** 72 | * Start automine 73 | */ 74 | export async function pauseAutomine(): Promise { 75 | await network.provider.send("evm_setAutomine", [false]); 76 | } 77 | 78 | /** 79 | * Resume automine 80 | */ 81 | export async function resumeAutomine(): Promise { 82 | await network.provider.send("evm_setAutomine", [true]); 83 | } 84 | -------------------------------------------------------------------------------- /deployments/deployed-contracts-curtis.json: -------------------------------------------------------------------------------- 1 | { 2 | "MockBeacon": { 3 | "address": "0x52912571414Db261E4122B0658037122dAe9718C", 4 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 5 | }, 6 | "MockWAPE": { 7 | "address": "0x647dc527Bd7dFEE4DD468cE6fC62FC50fa42BD8b", 8 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 9 | }, 10 | "MockBAYC": { 11 | "address": "0x52f51701ed1B560108fF71862330f9C8b4a15c8e", 12 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 13 | }, 14 | "MockMAYC": { 15 | "address": "0x35e13d4545Dd3935F87a056495b5Bae4f3aB9049", 16 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 17 | }, 18 | "MockBAKC": { 19 | "address": "0xFe94A8d9260a96e6da8BD6BfA537b49E73791567", 20 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 21 | }, 22 | "ApeCoinStaking": { 23 | "address": "0x5350A3FFEDCb50775f425982a11E31E2Ba43F34B", 24 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 25 | }, 26 | "NftVault": { 27 | "address": "0x5221bb8e9Ec407824a851755a753367F3f4e4D24", 28 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 29 | }, 30 | "StBAYC": { 31 | "address": "0xB1FB6C0973D97d285ab87b7b235BaEA7f96F5c36", 32 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 33 | }, 34 | "StMAYC": { 35 | "address": "0x3285c31a1aDC8E21E6E6b67bF28a95Bbd1622393", 36 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 37 | }, 38 | "StBAKC": { 39 | "address": "0x1c3A772154D640bbc4879cFdB2C8310a9a8D795E", 40 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 41 | }, 42 | "BendCoinPool": { 43 | "address": "0x97681ECaac2593A0Eee4275E2eEEB82F3b31371f", 44 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 45 | }, 46 | "BendNftPool": { 47 | "address": "0x3BDc9DeD2a895Afbf43F7AD99CDb13D6f95E6f83", 48 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 49 | }, 50 | "BendStakeManager": { 51 | "address": "0x1f7915Ed7A71275DF5A99646C1aab4b241d3f693", 52 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 53 | }, 54 | "BaycRewardsStrategy": { 55 | "address": "0x46d607b976aB45fDC41a5e074F87ddc6f8C457dc", 56 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 57 | }, 58 | "MaycRewardsStrategy": { 59 | "address": "0x1bF78B4CD13fD6458c95bB346a6498E118Da37B0", 60 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 61 | }, 62 | "BakcRewardsStrategy": { 63 | "address": "0xF08f40fa0312B23C31C4b027F26fDbc95Bcd7781", 64 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 65 | }, 66 | "DefaultWithdrawStrategy": { 67 | "address": "0x102385317E3e3A0B6d78a2B458D4Edf16dB4f41A", 68 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 69 | }, 70 | "PoolViewer": { 71 | "address": "0xa700939369433405E3Be0EC18D0E057E53f00364", 72 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 73 | }, 74 | "BendApeCoinStakedVoting": { 75 | "address": "0x85867E4778226Cb6A202F76D4C1516a1719E84c2", 76 | "deployer": "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6" 77 | } 78 | } -------------------------------------------------------------------------------- /contracts/test/MockAddressProviderV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | contract MockAddressProviderV2 { 5 | MockPoolManager private _poolManager; 6 | MockPoolLensV2 private _poolLens; 7 | 8 | constructor() { 9 | _poolManager = new MockPoolManager(); 10 | _poolLens = new MockPoolLensV2(); 11 | } 12 | 13 | function getPoolManager() public view returns (address) { 14 | return address(_poolManager); 15 | } 16 | 17 | function getPoolModuleProxy(uint moduleId) public view returns (address) { 18 | if (moduleId == 4) { 19 | return address(_poolLens); 20 | } 21 | 22 | revert("Unkonwn moduleId"); 23 | } 24 | } 25 | 26 | contract MockPoolManager { 27 | uint256 private _dummy; 28 | } 29 | 30 | contract MockPoolLensV2 { 31 | struct MockUserAssetData { 32 | uint256 totalCrossSupply; 33 | uint256 totalIsolateSupply; 34 | uint256 totalCrossBorrow; 35 | uint256 totalIsolateBorrow; 36 | } 37 | struct MockERC721TokenData { 38 | address owner; 39 | uint8 supplyMode; 40 | address lockerAddr; 41 | } 42 | 43 | mapping(address => mapping(uint32 => mapping(address => MockUserAssetData))) private _userAssetDatas; 44 | mapping(uint32 => mapping(address => mapping(uint256 => MockERC721TokenData))) private _erc721TokenDatas; 45 | 46 | function getUserAssetData( 47 | address user, 48 | uint32 poolId, 49 | address asset 50 | ) 51 | public 52 | view 53 | returns ( 54 | uint256 totalCrossSupply, 55 | uint256 totalIsolateSupply, 56 | uint256 totalCrossBorrow, 57 | uint256 totalIsolateBorrow 58 | ) 59 | { 60 | MockUserAssetData memory uad = _userAssetDatas[user][poolId][asset]; 61 | return (uad.totalCrossSupply, uad.totalIsolateSupply, uad.totalCrossBorrow, uad.totalIsolateBorrow); 62 | } 63 | 64 | function setUserAssetData( 65 | address user, 66 | uint32 poolId, 67 | address asset, 68 | uint256 totalCrossSupply, 69 | uint256 totalIsolateSupply, 70 | uint256 totalCrossBorrow, 71 | uint256 totalIsolateBorrow 72 | ) public { 73 | MockUserAssetData storage uad = _userAssetDatas[user][poolId][asset]; 74 | uad.totalCrossSupply = totalCrossSupply; 75 | uad.totalIsolateSupply = totalIsolateSupply; 76 | uad.totalCrossBorrow = totalCrossBorrow; 77 | uad.totalIsolateBorrow = totalIsolateBorrow; 78 | } 79 | 80 | function getERC721TokenData( 81 | uint32 poolId, 82 | address asset, 83 | uint256 tokenId 84 | ) public view returns (address, uint8, address) { 85 | MockERC721TokenData memory etd = _erc721TokenDatas[poolId][asset][tokenId]; 86 | return (etd.owner, etd.supplyMode, etd.lockerAddr); 87 | } 88 | 89 | function setERC721TokenData( 90 | uint32 poolId, 91 | address asset, 92 | uint256 tokenId, 93 | address owner, 94 | uint8 supplyMode, 95 | address lockerAddr 96 | ) public { 97 | MockERC721TokenData storage etd = _erc721TokenDatas[poolId][asset][tokenId]; 98 | etd.owner = owner; 99 | etd.supplyMode = supplyMode; 100 | etd.lockerAddr = lockerAddr; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@benddao/bendApeStaking", 3 | "version": "1.0.0", 4 | "description": "Bend ape staking smart contracts", 5 | "author": "https://x.com/soljesty", 6 | "private": false, 7 | "files": [ 8 | "/abis/*.json", 9 | "/contracts/*.sol", 10 | "/contracts/interfaces/*.sol", 11 | "/contracts/libraries/*.sol" 12 | ], 13 | "keywords": [ 14 | "benddao" 15 | ], 16 | "engines": { 17 | "node": ">=8.3.0" 18 | }, 19 | "homepage": "https://benddao.xyz/", 20 | "publishConfig": { 21 | "access": "public", 22 | "registry": "https://registry.npmjs.org" 23 | }, 24 | "scripts": { 25 | "typechain": "TS_NODE_TRANSPILE_ONLY=true hardhat typechain", 26 | "size": "npm run compile && hardhat size-contracts", 27 | "slither": "slither .", 28 | "clean": "hardhat clean && forge clean", 29 | "compile": "TS_NODE_TRANSPILE_ONLY=true hardhat compile", 30 | "compile:force": "TS_NODE_TRANSPILE_ONLY=true hardhat typechain && hardhat compile --force", 31 | "format:check": "prettier --check '**/*.{js,jsx,ts,tsx,sol,json,yaml,md}'", 32 | "format:write": "prettier --write '**/*.{js,jsx,ts,tsx,sol,json,yaml,md}'", 33 | "lint": "yarn lint:sol && yarn lint:ts && yarn format:check", 34 | "lint:sol": "solhint 'contracts/**/*.sol'", 35 | "lint:ts": "eslint '**/*.{js,jsx,ts,tsx}'", 36 | "prepare": "husky install", 37 | "test": "hardhat test", 38 | "test:file": "npm run compile && hardhat test ./test/hardhat/setup.ts ./test/hardhat/${TEST_FILE}", 39 | "test:gas": "REPORT_GAS=true hardhat test", 40 | "test:coverage": "TS_NODE_TRANSPILE_ONLY=true hardhat coverage && hardhat compile --force", 41 | "test:forge": "forge test -vvv", 42 | "test:all": "npm run test && npm run test:forge", 43 | "release": "release-it" 44 | }, 45 | "devDependencies": { 46 | "@commitlint/cli": "^17.0.2", 47 | "@commitlint/config-conventional": "^16.2.1", 48 | "@nomicfoundation/hardhat-chai-matchers": "^1.0.6", 49 | "@nomicfoundation/hardhat-foundry": "^1.0.1", 50 | "@nomicfoundation/hardhat-network-helpers": "^1.0.8", 51 | "@nomiclabs/hardhat-ethers": "^2.2.3", 52 | "@nomiclabs/hardhat-etherscan": "^3.1.7", 53 | "@openzeppelin/hardhat-upgrades": "^1.25.1", 54 | "@typechain/ethers-v5": "^10.2.0", 55 | "@typechain/hardhat": "^6.1.5", 56 | "@types/chai": "^4.3.5", 57 | "@types/lowdb": "^1.0.9", 58 | "@types/mocha": "^10.0.1", 59 | "@types/node": "^18.16.3", 60 | "@typescript-eslint/eslint-plugin": "^5.59.2", 61 | "@typescript-eslint/parser": "^5.59.2", 62 | "axios-logger": "^2.6.2", 63 | "chai": "^4.3.7", 64 | "dotenv": "^16.0.3", 65 | "eslint": "^8.39.0", 66 | "eslint-config-prettier": "^8.8.0", 67 | "eslint-config-standard": "^17.0.0", 68 | "eslint-plugin-import": "^2.27.5", 69 | "eslint-plugin-node": "^11.1.0", 70 | "eslint-plugin-prettier": "^4.2.1", 71 | "eslint-plugin-promise": "^6.1.1", 72 | "ethereumjs-util": "^7.1.5", 73 | "ethers": "^5.7.2", 74 | "fast-check": "^3.8.1", 75 | "hardhat": "^2.14.0", 76 | "hardhat-abi-exporter": "^2.10.1", 77 | "hardhat-gas-reporter": "^1.0.9", 78 | "husky": "^8.0.3", 79 | "lodash": "^4.17.21", 80 | "lowdb": "^1.0.0", 81 | "prettier": "^2.8.8", 82 | "prettier-plugin-solidity": "^1.1.3", 83 | "release-it": "^15.10.3", 84 | "solhint": "^3.4.1", 85 | "solidity-coverage": "^0.8.2", 86 | "tmp-promise": "^3.0.3", 87 | "ts-node": "^10.9.1", 88 | "typechain": "^8.1.1", 89 | "typescript": "^5.0.4" 90 | }, 91 | "dependencies": { 92 | "@openzeppelin/contracts": "4.8.3", 93 | "@openzeppelin/contracts-upgradeable": "4.8.3", 94 | "eslint-plugin-n": "^15.7.0", 95 | "hardhat-contract-sizer": "^2.8.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /abis/BendApeCoinStakedVoting.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "contract ICoinPool", 6 | "name": "coinPool_", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "contract INftPool", 11 | "name": "nftPool_", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "contract IStakeManager", 16 | "name": "staker_", 17 | "type": "address" 18 | }, 19 | { 20 | "internalType": "contract IBNFTRegistry", 21 | "name": "bnftRegistry_", 22 | "type": "address" 23 | } 24 | ], 25 | "stateMutability": "nonpayable", 26 | "type": "constructor" 27 | }, 28 | { 29 | "inputs": [], 30 | "name": "bnftRegistry", 31 | "outputs": [ 32 | { 33 | "internalType": "contract IBNFTRegistry", 34 | "name": "", 35 | "type": "address" 36 | } 37 | ], 38 | "stateMutability": "view", 39 | "type": "function" 40 | }, 41 | { 42 | "inputs": [], 43 | "name": "coinPool", 44 | "outputs": [ 45 | { 46 | "internalType": "contract ICoinPool", 47 | "name": "", 48 | "type": "address" 49 | } 50 | ], 51 | "stateMutability": "view", 52 | "type": "function" 53 | }, 54 | { 55 | "inputs": [ 56 | { 57 | "internalType": "address", 58 | "name": "userAddress", 59 | "type": "address" 60 | } 61 | ], 62 | "name": "getVotes", 63 | "outputs": [ 64 | { 65 | "internalType": "uint256", 66 | "name": "votes", 67 | "type": "uint256" 68 | } 69 | ], 70 | "stateMutability": "view", 71 | "type": "function" 72 | }, 73 | { 74 | "inputs": [ 75 | { 76 | "internalType": "address", 77 | "name": "userAddress", 78 | "type": "address" 79 | } 80 | ], 81 | "name": "getVotesInAllNftPool", 82 | "outputs": [ 83 | { 84 | "internalType": "uint256", 85 | "name": "votes", 86 | "type": "uint256" 87 | } 88 | ], 89 | "stateMutability": "view", 90 | "type": "function" 91 | }, 92 | { 93 | "inputs": [ 94 | { 95 | "internalType": "address", 96 | "name": "userAddress", 97 | "type": "address" 98 | } 99 | ], 100 | "name": "getVotesInCoinPool", 101 | "outputs": [ 102 | { 103 | "internalType": "uint256", 104 | "name": "votes", 105 | "type": "uint256" 106 | } 107 | ], 108 | "stateMutability": "view", 109 | "type": "function" 110 | }, 111 | { 112 | "inputs": [ 113 | { 114 | "internalType": "contract IStakedNft", 115 | "name": "stnft_", 116 | "type": "address" 117 | }, 118 | { 119 | "internalType": "address", 120 | "name": "userAddress", 121 | "type": "address" 122 | } 123 | ], 124 | "name": "getVotesInOneNftPool", 125 | "outputs": [ 126 | { 127 | "internalType": "uint256", 128 | "name": "votes", 129 | "type": "uint256" 130 | } 131 | ], 132 | "stateMutability": "view", 133 | "type": "function" 134 | }, 135 | { 136 | "inputs": [], 137 | "name": "nftPool", 138 | "outputs": [ 139 | { 140 | "internalType": "contract INftPool", 141 | "name": "", 142 | "type": "address" 143 | } 144 | ], 145 | "stateMutability": "view", 146 | "type": "function" 147 | }, 148 | { 149 | "inputs": [], 150 | "name": "staker", 151 | "outputs": [ 152 | { 153 | "internalType": "contract IStakeManager", 154 | "name": "", 155 | "type": "address" 156 | } 157 | ], 158 | "stateMutability": "view", 159 | "type": "function" 160 | } 161 | ] 162 | -------------------------------------------------------------------------------- /contracts/misc/BendApeCoinStakedVoting.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 5 | 6 | import {IBNFTRegistry} from "../interfaces/IBNFTRegistry.sol"; 7 | 8 | import {ICoinPool} from "../interfaces/ICoinPool.sol"; 9 | import {INftPool, IStakedNft} from "../interfaces/INftPool.sol"; 10 | import {IStakeManager} from "../interfaces/IStakeManager.sol"; 11 | 12 | /** 13 | * @title BendDAO Staked ApeCoin's Voting Contract 14 | * @notice Provides a comprehensive vote count across all pools in the ApeCoinStaking contract 15 | */ 16 | contract BendApeCoinStakedVoting { 17 | ICoinPool public immutable coinPool; 18 | INftPool public immutable nftPool; 19 | IStakeManager public immutable staker; 20 | IBNFTRegistry public immutable bnftRegistry; 21 | 22 | constructor(ICoinPool coinPool_, INftPool nftPool_, IStakeManager staker_, IBNFTRegistry bnftRegistry_) { 23 | coinPool = coinPool_; 24 | nftPool = nftPool_; 25 | staker = staker_; 26 | bnftRegistry = bnftRegistry_; 27 | } 28 | 29 | /** 30 | * @notice Returns a vote count across all pools in the ApeCoinStaking contract for a given address 31 | * @param userAddress The address to return votes for 32 | */ 33 | function getVotes(address userAddress) public view returns (uint256 votes) { 34 | votes += getVotesInCoinPool(userAddress); 35 | votes += getVotesInAllNftPool(userAddress); 36 | } 37 | 38 | function getVotesInCoinPool(address userAddress) public view returns (uint256 votes) { 39 | votes = coinPool.assetBalanceOf(userAddress); 40 | } 41 | 42 | function getVotesInAllNftPool(address userAddress) public view returns (uint256 votes) { 43 | votes += getVotesInOneNftPool(staker.stBayc(), userAddress); 44 | votes += getVotesInOneNftPool(staker.stMayc(), userAddress); 45 | votes += getVotesInOneNftPool(staker.stBakc(), userAddress); 46 | } 47 | 48 | function getVotesInOneNftPool(IStakedNft stnft_, address userAddress) public view returns (uint256 votes) { 49 | // Check user balance 50 | uint256 stnftBalance = stnft_.balanceOf(userAddress); 51 | uint256 bnftBalance; 52 | (address bnftProxy, ) = bnftRegistry.getBNFTAddresses(address(stnft_)); 53 | if (bnftProxy != address(0)) { 54 | bnftBalance += IERC721Enumerable(bnftProxy).balanceOf(userAddress); 55 | } 56 | if (bnftBalance == 0 && stnftBalance == 0) { 57 | return 0; 58 | } 59 | 60 | // Get all tokenIds 61 | uint256[] memory allTokenIds = new uint256[](stnftBalance + bnftBalance); 62 | uint256 allIdSize = 0; 63 | 64 | for (uint256 i = 0; i < stnftBalance; i++) { 65 | uint256 tokenId = stnft_.tokenOfOwnerByIndex(userAddress, i); 66 | if (stnft_.stakerOf(tokenId) == address(staker)) { 67 | allTokenIds[allIdSize] = tokenId; 68 | allIdSize++; 69 | } 70 | } 71 | 72 | if (bnftProxy != address(0)) { 73 | IERC721Enumerable bnft = IERC721Enumerable(bnftProxy); 74 | for (uint256 i = 0; i < bnftBalance; i++) { 75 | uint256 tokenId = bnft.tokenOfOwnerByIndex(userAddress, i); 76 | if (stnft_.stakerOf(tokenId) == address(staker)) { 77 | allTokenIds[allIdSize] = tokenId; 78 | allIdSize++; 79 | } 80 | } 81 | } 82 | 83 | // Get votes from claimable rewards 84 | address[] memory claimNfts = new address[](1); 85 | claimNfts[0] = stnft_.underlyingAsset(); 86 | 87 | uint256[][] memory claimTokenIds = new uint256[][](1); 88 | claimTokenIds[0] = new uint256[](allIdSize); 89 | for (uint256 i = 0; i < allIdSize; i++) { 90 | claimTokenIds[0][i] = allTokenIds[i]; 91 | } 92 | 93 | votes = nftPool.claimable(claimNfts, claimTokenIds); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/interfaces/IStakeManager.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | import {IApeCoinStaking} from "./IApeCoinStaking.sol"; 4 | import {IRewardsStrategy} from "./IRewardsStrategy.sol"; 5 | import {IWithdrawStrategy} from "./IWithdrawStrategy.sol"; 6 | import {IStakedNft} from "./IStakedNft.sol"; 7 | 8 | interface IStakeManager { 9 | event FeeRatioChanged(uint256 newRatio); 10 | event FeeRecipientChanged(address newRecipient); 11 | event BotAdminChanged(address newAdmin); 12 | event RewardsStrategyChanged(address nft, address newStrategy); 13 | event WithdrawStrategyChanged(address newStrategy); 14 | event Compounded(bool isClaimCoinPool, uint256 claimedNfts); 15 | 16 | function stBayc() external view returns (IStakedNft); 17 | 18 | function stMayc() external view returns (IStakedNft); 19 | 20 | function stBakc() external view returns (IStakedNft); 21 | 22 | function totalStakedApeCoin() external view returns (uint256); 23 | 24 | function totalPendingRewards() external view returns (uint256); 25 | 26 | function stakedApeCoin(uint256 poolId_) external view returns (uint256); 27 | 28 | function pendingRewards(uint256 poolId_) external view returns (uint256); 29 | 30 | function pendingFeeAmount() external view returns (uint256); 31 | 32 | function fee() external view returns (uint256); 33 | 34 | function feeRecipient() external view returns (address); 35 | 36 | function updateFee(uint256 fee_) external; 37 | 38 | function updateFeeRecipient(address recipient_) external; 39 | 40 | // bot 41 | function updateBotAdmin(address bot_) external; 42 | 43 | // strategy 44 | function updateRewardsStrategy(address nft_, IRewardsStrategy rewardsStrategy_) external; 45 | 46 | function rewardsStrategies(address nft_) external view returns (IRewardsStrategy); 47 | 48 | function getNftRewardsShare(address nft_) external view returns (uint256 nftShare); 49 | 50 | function updateWithdrawStrategy(IWithdrawStrategy withdrawStrategy_) external; 51 | 52 | function withdrawApeCoin(uint256 required) external returns (uint256); 53 | 54 | function depositNft(address[] calldata nfts_, uint256[][] calldata tokenIds_, address owner_) external; 55 | 56 | function withdrawNft(address[] calldata nfts_, uint256[][] calldata tokenIds_, address owner_) external; 57 | 58 | function mintStNft(IStakedNft stNft_, address to_, uint256[] calldata tokenIds_) external; 59 | 60 | function burnStNft(IStakedNft stNft_, address from_, uint256[] calldata tokenIds_) external; 61 | 62 | // staking 63 | function calculateFee(uint256 rewardsAmount_) external view returns (uint256 feeAmount); 64 | 65 | function stakeBayc(uint256[] calldata tokenIds_) external; 66 | 67 | function unstakeBayc(uint256[] calldata tokenIds_) external; 68 | 69 | function claimBayc(uint256[] calldata tokenIds_) external; 70 | 71 | function stakeMayc(uint256[] calldata tokenIds_) external; 72 | 73 | function unstakeMayc(uint256[] calldata tokenIds_) external; 74 | 75 | function claimMayc(uint256[] calldata tokenIds_) external; 76 | 77 | function stakeBakc(uint256[] calldata tokenIds_) external; 78 | 79 | function unstakeBakc(uint256[] calldata tokenIds_) external; 80 | 81 | function claimBakc(uint256[] calldata tokenIds_) external; 82 | 83 | struct NftArgs { 84 | uint256[] bayc; 85 | uint256[] mayc; 86 | uint256[] bakc; 87 | } 88 | 89 | struct TokenOwner { 90 | uint256[] tokenIds; 91 | address owner; 92 | } 93 | 94 | struct TokenOwnerArgs { 95 | TokenOwner bayc; 96 | TokenOwner mayc; 97 | TokenOwner bakc; 98 | } 99 | 100 | struct CompoundArgs { 101 | bool claimCoinPool; 102 | TokenOwnerArgs deposit; 103 | TokenOwnerArgs withdraw; 104 | NftArgs claim; 105 | NftArgs unstake; 106 | NftArgs stake; 107 | uint256 coinStakeThreshold; 108 | } 109 | 110 | function compound(CompoundArgs calldata args_) external; 111 | } 112 | -------------------------------------------------------------------------------- /abis/IDelegateRegistryV2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "to", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "address", 11 | "name": "contract_", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "uint256", 16 | "name": "tokenId", 17 | "type": "uint256" 18 | }, 19 | { 20 | "internalType": "bytes32", 21 | "name": "rights", 22 | "type": "bytes32" 23 | }, 24 | { 25 | "internalType": "bool", 26 | "name": "enable", 27 | "type": "bool" 28 | } 29 | ], 30 | "name": "delegateERC721", 31 | "outputs": [ 32 | { 33 | "internalType": "bytes32", 34 | "name": "delegationHash", 35 | "type": "bytes32" 36 | } 37 | ], 38 | "stateMutability": "payable", 39 | "type": "function" 40 | }, 41 | { 42 | "inputs": [ 43 | { 44 | "internalType": "bytes32[]", 45 | "name": "delegationHashes", 46 | "type": "bytes32[]" 47 | } 48 | ], 49 | "name": "getDelegationsFromHashes", 50 | "outputs": [ 51 | { 52 | "components": [ 53 | { 54 | "internalType": "enum IDelegateRegistryV2.DelegationType", 55 | "name": "type_", 56 | "type": "uint8" 57 | }, 58 | { 59 | "internalType": "address", 60 | "name": "to", 61 | "type": "address" 62 | }, 63 | { 64 | "internalType": "address", 65 | "name": "from", 66 | "type": "address" 67 | }, 68 | { 69 | "internalType": "bytes32", 70 | "name": "rights", 71 | "type": "bytes32" 72 | }, 73 | { 74 | "internalType": "address", 75 | "name": "contract_", 76 | "type": "address" 77 | }, 78 | { 79 | "internalType": "uint256", 80 | "name": "tokenId", 81 | "type": "uint256" 82 | }, 83 | { 84 | "internalType": "uint256", 85 | "name": "amount", 86 | "type": "uint256" 87 | } 88 | ], 89 | "internalType": "struct IDelegateRegistryV2.Delegation[]", 90 | "name": "delegations", 91 | "type": "tuple[]" 92 | } 93 | ], 94 | "stateMutability": "view", 95 | "type": "function" 96 | }, 97 | { 98 | "inputs": [ 99 | { 100 | "internalType": "address", 101 | "name": "from", 102 | "type": "address" 103 | } 104 | ], 105 | "name": "getOutgoingDelegations", 106 | "outputs": [ 107 | { 108 | "components": [ 109 | { 110 | "internalType": "enum IDelegateRegistryV2.DelegationType", 111 | "name": "type_", 112 | "type": "uint8" 113 | }, 114 | { 115 | "internalType": "address", 116 | "name": "to", 117 | "type": "address" 118 | }, 119 | { 120 | "internalType": "address", 121 | "name": "from", 122 | "type": "address" 123 | }, 124 | { 125 | "internalType": "bytes32", 126 | "name": "rights", 127 | "type": "bytes32" 128 | }, 129 | { 130 | "internalType": "address", 131 | "name": "contract_", 132 | "type": "address" 133 | }, 134 | { 135 | "internalType": "uint256", 136 | "name": "tokenId", 137 | "type": "uint256" 138 | }, 139 | { 140 | "internalType": "uint256", 141 | "name": "amount", 142 | "type": "uint256" 143 | } 144 | ], 145 | "internalType": "struct IDelegateRegistryV2.Delegation[]", 146 | "name": "delegations", 147 | "type": "tuple[]" 148 | } 149 | ], 150 | "stateMutability": "view", 151 | "type": "function" 152 | } 153 | ] 154 | -------------------------------------------------------------------------------- /contracts/libraries/ApeStakingLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | import {IApeCoinStaking} from "../interfaces/IApeCoinStaking.sol"; 4 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | library ApeStakingLib { 8 | uint256 internal constant APE_COIN_PRECISION = 1e18; 9 | uint256 internal constant SECONDS_PER_HOUR = 3600; 10 | uint256 internal constant SECONDS_PER_MINUTE = 60; 11 | 12 | uint256 internal constant APE_COIN_POOL_ID = 0; 13 | uint256 internal constant BAYC_POOL_ID = 1; 14 | uint256 internal constant MAYC_POOL_ID = 2; 15 | uint256 internal constant BAKC_POOL_ID = 3; 16 | 17 | // uint256 internal constant BAYC_MAX_CAP = 10094e18; 18 | // uint256 internal constant MAYC_MAX_CAP = 2042e18; 19 | // uint256 internal constant BAKC_MAX_CAP = 856e18; 20 | 21 | function getCurrentTimeRange( 22 | IApeCoinStaking apeCoinStaking_, 23 | uint256 poolId 24 | ) internal view returns (IApeCoinStaking.TimeRange memory) { 25 | ( 26 | IApeCoinStaking.PoolUI memory baycPoolUI, 27 | IApeCoinStaking.PoolUI memory maycPoolUI, 28 | IApeCoinStaking.PoolUI memory bakcPoolUI 29 | ) = apeCoinStaking_.getPoolsUI(); 30 | 31 | if (poolId == baycPoolUI.poolId) { 32 | return baycPoolUI.currentTimeRange; 33 | } 34 | 35 | if (poolId == maycPoolUI.poolId) { 36 | return maycPoolUI.currentTimeRange; 37 | } 38 | if (poolId == bakcPoolUI.poolId) { 39 | return bakcPoolUI.currentTimeRange; 40 | } 41 | 42 | revert("invalid pool id"); 43 | } 44 | 45 | function getNftPoolId(IApeCoinStaking apeCoinStaking_, address nft_) internal view returns (uint256) { 46 | if (nft_ == apeCoinStaking_.nftContracts(BAYC_POOL_ID)) { 47 | return BAYC_POOL_ID; 48 | } 49 | 50 | if (nft_ == apeCoinStaking_.nftContracts(MAYC_POOL_ID)) { 51 | return MAYC_POOL_ID; 52 | } 53 | if (nft_ == apeCoinStaking_.nftContracts(BAKC_POOL_ID)) { 54 | return BAKC_POOL_ID; 55 | } 56 | revert("invalid nft"); 57 | } 58 | 59 | function getNftPosition( 60 | IApeCoinStaking apeCoinStaking_, 61 | address nft_, 62 | uint256 tokenId_ 63 | ) internal view returns (IApeCoinStaking.Position memory) { 64 | return apeCoinStaking_.nftPosition(getNftPoolId(apeCoinStaking_, nft_), tokenId_); 65 | } 66 | 67 | function getNftPool( 68 | IApeCoinStaking apeCoinStaking_, 69 | address nft_ 70 | ) internal view returns (IApeCoinStaking.PoolWithoutTimeRange memory) { 71 | return apeCoinStaking_.pools(getNftPoolId(apeCoinStaking_, nft_)); 72 | } 73 | 74 | function getNftRewardsBy( 75 | IApeCoinStaking apeCoinStaking_, 76 | address nft_, 77 | uint256 from_, 78 | uint256 to_ 79 | ) internal view returns (uint256, uint256) { 80 | return apeCoinStaking_.rewardsBy(getNftPoolId(apeCoinStaking_, nft_), from_, to_); 81 | } 82 | 83 | function bayc(IApeCoinStaking apeCoinStaking_) internal view returns (IERC721) { 84 | return IERC721(apeCoinStaking_.nftContracts(BAYC_POOL_ID)); 85 | } 86 | 87 | function mayc(IApeCoinStaking apeCoinStaking_) internal view returns (IERC721) { 88 | return IERC721(apeCoinStaking_.nftContracts(MAYC_POOL_ID)); 89 | } 90 | 91 | function bakc(IApeCoinStaking apeCoinStaking_) internal view returns (IERC721) { 92 | return IERC721(apeCoinStaking_.nftContracts(BAKC_POOL_ID)); 93 | } 94 | 95 | function getPreviousTimestampHour() internal view returns (uint256) { 96 | return block.timestamp - (getMinute(block.timestamp) * 60 + getSecond(block.timestamp)); 97 | } 98 | 99 | function getMinute(uint256 timestamp) internal pure returns (uint256 minute) { 100 | uint256 secs = timestamp % SECONDS_PER_HOUR; 101 | minute = secs / SECONDS_PER_MINUTE; 102 | } 103 | 104 | /// @notice the seconds (0 to 59) of a timestamp 105 | function getSecond(uint256 timestamp) internal pure returns (uint256 second) { 106 | second = timestamp % SECONDS_PER_MINUTE; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /contracts/test/MintableERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 5 | import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 7 | 8 | interface IApeCoinStakingCallback { 9 | function executeCallback(bytes32 guid) external; 10 | } 11 | 12 | /** 13 | * @title MintableERC721 14 | * @dev ERC721 minting logic 15 | */ 16 | contract MintableERC721 is ERC721Enumerable, Ownable { 17 | event ReadWithCallback(bytes32 guid, uint256[] tokenIds); 18 | event ExecuteCallback(bytes32 guid); 19 | 20 | string public baseURI; 21 | mapping(address => uint256) public mintCounts; 22 | uint256 public maxSupply; 23 | uint256 public maxTokenId; 24 | mapping(uint256 => bool) public lockedTokens; 25 | bool public disableTransfer; 26 | uint256 public nextReadId; 27 | 28 | constructor(string memory name, string memory symbol) Ownable() ERC721(name, symbol) { 29 | maxSupply = 10000; 30 | maxTokenId = maxSupply - 1; 31 | baseURI = "https://MintableERC721/"; 32 | } 33 | 34 | /** 35 | * @dev Function to mint tokens 36 | * @param tokenId The id of tokens to mint. 37 | * @return A boolean that indicates if the operation was successful. 38 | */ 39 | function mint(uint256 tokenId) public returns (bool) { 40 | require(tokenId <= maxTokenId, "exceed max token id"); 41 | require(totalSupply() + 1 <= maxSupply, "exceed max supply"); 42 | 43 | mintCounts[_msgSender()] += 1; 44 | require(mintCounts[_msgSender()] <= 100, "exceed mint limit"); 45 | 46 | _mint(_msgSender(), tokenId); 47 | return true; 48 | } 49 | 50 | function privateMint(address to, uint256 tokenId) public onlyOwner returns (bool) { 51 | require(tokenId <= maxTokenId, "exceed max token id"); 52 | require(totalSupply() + 1 <= maxSupply, "exceed max supply"); 53 | 54 | _mint(to, tokenId); 55 | return true; 56 | } 57 | 58 | function privateBurn(uint256 tokenId) public onlyOwner { 59 | require(_exists(tokenId), "token does not exist"); 60 | 61 | _burn(tokenId); 62 | } 63 | 64 | function _baseURI() internal view virtual override returns (string memory) { 65 | return baseURI; 66 | } 67 | 68 | function setBaseURI(string memory baseURI_) public onlyOwner { 69 | baseURI = baseURI_; 70 | } 71 | 72 | function setMaxSupply(uint256 maxSupply_) public onlyOwner { 73 | maxSupply = maxSupply_; 74 | } 75 | 76 | function setMaxTokenId(uint256 maxTokenId_) public onlyOwner { 77 | maxTokenId = maxTokenId_; 78 | } 79 | 80 | function executeCallback(address apeCoinStaking_, bytes32 guid_) public { 81 | IApeCoinStakingCallback(apeCoinStaking_).executeCallback(guid_); 82 | 83 | emit ExecuteCallback(guid_); 84 | } 85 | 86 | function readWithCallback( 87 | uint256[] calldata tokenIds, 88 | uint32[] calldata eids, 89 | uint128 callbackGasLimit 90 | ) public payable returns (bytes32) { 91 | tokenIds; 92 | eids; 93 | callbackGasLimit; 94 | 95 | bytes32 guid = getNextGUID(); 96 | 97 | nextReadId += 1; 98 | 99 | emit ReadWithCallback(guid, tokenIds); 100 | 101 | return guid; 102 | } 103 | 104 | function getNextGUID() public view returns (bytes32) { 105 | return keccak256(abi.encodePacked(address(this), nextReadId)); 106 | } 107 | 108 | function locked(uint256 tokenId) public view returns (bool) { 109 | return lockedTokens[tokenId]; 110 | } 111 | 112 | function setLocked(uint256 tokenId, bool flag) public onlyOwner { 113 | lockedTokens[tokenId] = flag; 114 | } 115 | 116 | function setDisableTransfer(bool flag) public onlyOwner { 117 | disableTransfer = flag; 118 | } 119 | 120 | function _beforeTokenTransfer( 121 | address from, 122 | address to, 123 | uint256 firstTokenId, 124 | uint256 batchSize 125 | ) internal virtual override { 126 | from; 127 | to; 128 | firstTokenId; 129 | batchSize; 130 | 131 | require(!disableTransfer, "transfer disabled"); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tasks/utils/verification.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ 3 | import fs from "fs"; 4 | import { exit } from "process"; 5 | import { file } from "tmp-promise"; 6 | 7 | import { DRE } from "./DRE"; 8 | 9 | const fatalErrors = [ 10 | `The address provided as argument contains a contract, but its bytecode`, 11 | `Daily limit of 100 source code submissions reached`, 12 | `has no bytecode. Is the contract deployed to this network`, 13 | `The constructor for`, 14 | ]; 15 | 16 | const okErrors = [`Contract source code already verified`, `Already Verified`]; 17 | 18 | const unableVerifyError = "Fail - Unable to verify"; 19 | 20 | export const SUPPORTED_ETHERSCAN_NETWORKS = ["mainnet", "sepolia", "apechain", "curtis"]; 21 | 22 | function delay(ms: number) { 23 | return new Promise((resolve) => setTimeout(resolve, ms)); 24 | } 25 | 26 | export const verifyEtherscanContract = async ( 27 | address: string, 28 | constructorArguments: (string | string[])[], 29 | contract?: string, 30 | libraries?: string 31 | ): Promise => { 32 | const currentNetwork = DRE.network.name; 33 | 34 | if (!process.env.ETHERSCAN_KEY) { 35 | throw Error("Missing process.env.ETHERSCAN_KEY."); 36 | } 37 | if (!SUPPORTED_ETHERSCAN_NETWORKS.includes(currentNetwork)) { 38 | throw Error( 39 | `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${SUPPORTED_ETHERSCAN_NETWORKS.toString()}` 40 | ); 41 | } 42 | 43 | try { 44 | console.log( 45 | "[ETHERSCAN][WARNING] Delaying Etherscan verification due their API can not find newly deployed contracts" 46 | ); 47 | const msDelay = 3000; 48 | const times = 4; 49 | // write a javascript module that exports the argument list 50 | // https://hardhat.org/plugins/nomiclabs-hardhat-etherscan.html#complex-arguments 51 | const { fd, path, cleanup } = await file({ 52 | prefix: "verify-params-", 53 | postfix: ".js", 54 | }); 55 | fs.writeSync(fd, `module.exports = ${JSON.stringify([...constructorArguments])};`); 56 | 57 | const params = { 58 | address, 59 | contract, 60 | libraries, 61 | constructorArguments, 62 | constructorArgsParams: constructorArguments, 63 | constructorArgs: path, 64 | relatedSources: true, 65 | }; 66 | await runTaskWithRetry("verify:verify", params, times, msDelay, cleanup); 67 | } catch (error) {} 68 | }; 69 | 70 | export const runTaskWithRetry = async ( 71 | task: string, 72 | params: any, 73 | times: number, 74 | msDelay: number, 75 | cleanup: () => void 76 | ): Promise => { 77 | let counter = times; 78 | await delay(msDelay); 79 | 80 | try { 81 | if (times > 1) { 82 | await DRE.run(task, params); 83 | cleanup(); 84 | } else if (times === 1) { 85 | console.log("[ETHERSCAN][WARNING] Trying to verify via uploading all sources."); 86 | delete params.relatedSources; 87 | await DRE.run(task, params); 88 | cleanup(); 89 | } else { 90 | cleanup(); 91 | console.error("[ETHERSCAN][ERROR] Errors after all the retries, check the logs for more information."); 92 | } 93 | } catch (error: any) { 94 | counter--; 95 | 96 | if (okErrors.some((okReason) => error.message.includes(okReason))) { 97 | console.info("[ETHERSCAN][INFO] Skipping due OK response: ", error.message); 98 | return; 99 | } 100 | 101 | if (fatalErrors.some((fatalError) => error.message.includes(fatalError))) { 102 | console.error("[ETHERSCAN][ERROR] Fatal error detected, skip retries and resume deployment.", error.message); 103 | return; 104 | } 105 | console.error("[ETHERSCAN][ERROR]", error.message); 106 | console.log(); 107 | console.info(`[ETHERSCAN][[INFO] Retrying attemps: ${counter}.`); 108 | if (error.message.includes(unableVerifyError)) { 109 | console.log("[ETHERSCAN][WARNING] Trying to verify via uploading all sources."); 110 | delete params.relatedSources; 111 | } 112 | await runTaskWithRetry(task, params, counter, msDelay, cleanup); 113 | } 114 | }; 115 | 116 | export const checkVerification = () => { 117 | const currentNetwork = DRE.network.name; 118 | if (!process.env.ETHERSCAN_KEY) { 119 | console.error("Missing process.env.ETHERSCAN_KEY."); 120 | exit(3); 121 | } 122 | if (!SUPPORTED_ETHERSCAN_NETWORKS.includes(currentNetwork)) { 123 | console.error( 124 | `Current network ${currentNetwork} not supported. Please change to one of the next networks: ${SUPPORTED_ETHERSCAN_NETWORKS.toString()}` 125 | ); 126 | exit(5); 127 | } 128 | }; 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Business Source License 1.1 2 | 3 | License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. 4 | "Business Source License" is a trademark of MariaDB Corporation Ab. 5 | 6 | ----------------------------------------------------------------------------- 7 | 8 | Parameters 9 | 10 | Licensor: BendDAO 11 | 12 | Licensed Work: Bend Ape Staking v2 13 | The Licensed Work is (c) 2023 BendDAO 14 | 15 | Additional Use Grant: Any uses listed and defined at 16 | v2-staking-license-grants.benddao.eth 17 | 18 | Change Date: The earlier of 2027-04-01 or a date specified at 19 | v2-staking-license-date.benddao.eth 20 | 21 | Change License: GNU General Public License v2.0 or later 22 | 23 | ----------------------------------------------------------------------------- 24 | 25 | Terms 26 | 27 | The Licensor hereby grants you the right to copy, modify, create derivative 28 | works, redistribute, and make non-production use of the Licensed Work. The 29 | Licensor may make an Additional Use Grant, above, permitting limited 30 | production use. 31 | 32 | Effective on the Change Date, or the fourth anniversary of the first publicly 33 | available distribution of a specific version of the Licensed Work under this 34 | License, whichever comes first, the Licensor hereby grants you rights under 35 | the terms of the Change License, and the rights granted in the paragraph 36 | above terminate. 37 | 38 | If your use of the Licensed Work does not comply with the requirements 39 | currently in effect as described in this License, you must purchase a 40 | commercial license from the Licensor, its affiliated entities, or authorized 41 | resellers, or you must refrain from using the Licensed Work. 42 | 43 | All copies of the original and modified Licensed Work, and derivative works 44 | of the Licensed Work, are subject to this License. This License applies 45 | separately for each version of the Licensed Work and the Change Date may vary 46 | for each version of the Licensed Work released by Licensor. 47 | 48 | You must conspicuously display this License on each original or modified copy 49 | of the Licensed Work. If you receive the Licensed Work in original or 50 | modified form from a third party, the terms and conditions set forth in this 51 | License apply to your use of that work. 52 | 53 | Any use of the Licensed Work in violation of this License will automatically 54 | terminate your rights under this License for the current and all other 55 | versions of the Licensed Work. 56 | 57 | This License does not grant you any right in any trademark or logo of 58 | Licensor or its affiliates (provided that you may use a trademark or logo of 59 | Licensor as expressly required by this License). 60 | 61 | TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON 62 | AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, 63 | EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND 65 | TITLE. 66 | 67 | MariaDB hereby grants you permission to use this License’s text to license 68 | your works, and to refer to it using the trademark "Business Source License", 69 | as long as you comply with the Covenants of Licensor below. 70 | 71 | ----------------------------------------------------------------------------- 72 | 73 | Covenants of Licensor 74 | 75 | In consideration of the right to use this License’s text and the "Business 76 | Source License" name and trademark, Licensor covenants to MariaDB, and to all 77 | other recipients of the licensed work to be provided by Licensor: 78 | 79 | 1. To specify as the Change License the GPL Version 2.0 or any later version, 80 | or a license that is compatible with GPL Version 2.0 or a later version, 81 | where "compatible" means that software provided under the Change License can 82 | be included in a program with software provided under GPL Version 2.0 or a 83 | later version. Licensor may specify additional Change Licenses without 84 | limitation. 85 | 86 | 2. To either: (a) specify an additional grant of rights to use that does not 87 | impose any additional restriction on the right granted in this License, as 88 | the Additional Use Grant; or (b) insert the text "None". 89 | 90 | 3. To specify a Change Date. 91 | 92 | 4. Not to modify this License in any other way. 93 | 94 | ----------------------------------------------------------------------------- 95 | 96 | Notice 97 | 98 | The Business Source License (this document, or the "License") is not an Open 99 | Source license. However, the Licensed Work will eventually be made available 100 | under an Open Source License, as stated in this License. -------------------------------------------------------------------------------- /abis/ILendPool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "address", 6 | "name": "reserveAsset", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "uint256", 11 | "name": "amount", 12 | "type": "uint256" 13 | }, 14 | { 15 | "internalType": "address", 16 | "name": "nftAsset", 17 | "type": "address" 18 | }, 19 | { 20 | "internalType": "uint256", 21 | "name": "nftTokenId", 22 | "type": "uint256" 23 | }, 24 | { 25 | "internalType": "address", 26 | "name": "onBehalfOf", 27 | "type": "address" 28 | }, 29 | { 30 | "internalType": "uint16", 31 | "name": "referralCode", 32 | "type": "uint16" 33 | } 34 | ], 35 | "name": "borrow", 36 | "outputs": [], 37 | "stateMutability": "nonpayable", 38 | "type": "function" 39 | }, 40 | { 41 | "inputs": [ 42 | { 43 | "internalType": "address", 44 | "name": "nftAsset", 45 | "type": "address" 46 | }, 47 | { 48 | "internalType": "uint256", 49 | "name": "nftTokenId", 50 | "type": "uint256" 51 | } 52 | ], 53 | "name": "getNftAuctionData", 54 | "outputs": [ 55 | { 56 | "internalType": "uint256", 57 | "name": "loanId", 58 | "type": "uint256" 59 | }, 60 | { 61 | "internalType": "address", 62 | "name": "bidderAddress", 63 | "type": "address" 64 | }, 65 | { 66 | "internalType": "uint256", 67 | "name": "bidPrice", 68 | "type": "uint256" 69 | }, 70 | { 71 | "internalType": "uint256", 72 | "name": "bidBorrowAmount", 73 | "type": "uint256" 74 | }, 75 | { 76 | "internalType": "uint256", 77 | "name": "bidFine", 78 | "type": "uint256" 79 | } 80 | ], 81 | "stateMutability": "view", 82 | "type": "function" 83 | }, 84 | { 85 | "inputs": [ 86 | { 87 | "internalType": "address", 88 | "name": "nftAsset", 89 | "type": "address" 90 | }, 91 | { 92 | "internalType": "uint256", 93 | "name": "nftTokenId", 94 | "type": "uint256" 95 | } 96 | ], 97 | "name": "getNftDebtData", 98 | "outputs": [ 99 | { 100 | "internalType": "uint256", 101 | "name": "loanId", 102 | "type": "uint256" 103 | }, 104 | { 105 | "internalType": "address", 106 | "name": "reserveAsset", 107 | "type": "address" 108 | }, 109 | { 110 | "internalType": "uint256", 111 | "name": "totalCollateral", 112 | "type": "uint256" 113 | }, 114 | { 115 | "internalType": "uint256", 116 | "name": "totalDebt", 117 | "type": "uint256" 118 | }, 119 | { 120 | "internalType": "uint256", 121 | "name": "availableBorrows", 122 | "type": "uint256" 123 | }, 124 | { 125 | "internalType": "uint256", 126 | "name": "healthFactor", 127 | "type": "uint256" 128 | } 129 | ], 130 | "stateMutability": "view", 131 | "type": "function" 132 | }, 133 | { 134 | "inputs": [ 135 | { 136 | "internalType": "address", 137 | "name": "nftAsset", 138 | "type": "address" 139 | }, 140 | { 141 | "internalType": "uint256", 142 | "name": "nftTokenId", 143 | "type": "uint256" 144 | }, 145 | { 146 | "internalType": "uint256", 147 | "name": "amount", 148 | "type": "uint256" 149 | }, 150 | { 151 | "internalType": "uint256", 152 | "name": "bidFine", 153 | "type": "uint256" 154 | } 155 | ], 156 | "name": "redeem", 157 | "outputs": [ 158 | { 159 | "internalType": "uint256", 160 | "name": "", 161 | "type": "uint256" 162 | } 163 | ], 164 | "stateMutability": "nonpayable", 165 | "type": "function" 166 | }, 167 | { 168 | "inputs": [ 169 | { 170 | "internalType": "address", 171 | "name": "nftAsset", 172 | "type": "address" 173 | }, 174 | { 175 | "internalType": "uint256", 176 | "name": "nftTokenId", 177 | "type": "uint256" 178 | }, 179 | { 180 | "internalType": "uint256", 181 | "name": "amount", 182 | "type": "uint256" 183 | } 184 | ], 185 | "name": "repay", 186 | "outputs": [ 187 | { 188 | "internalType": "uint256", 189 | "name": "", 190 | "type": "uint256" 191 | }, 192 | { 193 | "internalType": "bool", 194 | "name": "", 195 | "type": "bool" 196 | } 197 | ], 198 | "stateMutability": "nonpayable", 199 | "type": "function" 200 | } 201 | ] 202 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import type { HardhatUserConfig } from "hardhat/types"; 2 | import { task } from "hardhat/config"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | import "@nomicfoundation/hardhat-chai-matchers"; 6 | import "@openzeppelin/hardhat-upgrades"; 7 | import "@nomiclabs/hardhat-etherscan"; 8 | import "@nomiclabs/hardhat-ethers"; 9 | 10 | import "@typechain/hardhat"; 11 | import "hardhat-abi-exporter"; 12 | import "hardhat-gas-reporter"; 13 | import "hardhat-contract-sizer"; 14 | import "solidity-coverage"; 15 | import "dotenv/config"; 16 | 17 | import { Network, NETWORKS_RPC_URL } from "./tasks/config"; 18 | 19 | const ETHERSCAN_KEY = process.env.ETHERSCAN_KEY || ""; 20 | const ETHERSCAN_KEY_APECHAIN = process.env.ETHERSCAN_KEY_APECHAIN || ""; 21 | const MNEMONIC_PATH = "m/44'/60'/0'/0"; 22 | const MNEMONIC = process.env.MNEMONIC || ""; 23 | const PRIVATE_KEY = process.env.PRIVATE_KEY || ""; 24 | const REPORT_GAS = !!process.env.REPORT_GAS; 25 | 26 | const GWEI = 1000 * 1000 * 1000; 27 | 28 | const tasksPath = path.join(__dirname, "tasks"); 29 | fs.readdirSync(tasksPath) 30 | .filter((pth) => pth.endsWith(".ts")) 31 | .forEach((task) => { 32 | require(`${tasksPath}/${task}`); 33 | }); 34 | 35 | task("accounts", "Prints the list of accounts", async (_args, hre) => { 36 | const accounts = await hre.ethers.getSigners(); 37 | accounts.forEach(async (account) => console.info(account.address)); 38 | }); 39 | 40 | const config: HardhatUserConfig = { 41 | defaultNetwork: "hardhat", 42 | networks: { 43 | hardhat: { 44 | initialBaseFeePerGas: 0, 45 | allowUnlimitedContractSize: true, 46 | accounts: { 47 | accountsBalance: "100000000000000000000000000", // 100000K ETH 48 | }, 49 | }, 50 | sepolia: { 51 | // gasPrice: 1 * GWEI, 52 | url: NETWORKS_RPC_URL[Network.sepolia], 53 | accounts: PRIVATE_KEY 54 | ? [PRIVATE_KEY] 55 | : { 56 | mnemonic: MNEMONIC, 57 | path: MNEMONIC_PATH, 58 | initialIndex: 0, 59 | count: 20, 60 | }, 61 | }, 62 | mainnet: { 63 | // gasPrice: 1 * GWEI, 64 | url: NETWORKS_RPC_URL[Network.mainnet], 65 | accounts: PRIVATE_KEY 66 | ? [PRIVATE_KEY] 67 | : { 68 | mnemonic: MNEMONIC, 69 | path: MNEMONIC_PATH, 70 | initialIndex: 0, 71 | count: 20, 72 | }, 73 | }, 74 | curtis: { 75 | // gasPrice: 1 * GWEI, 76 | url: NETWORKS_RPC_URL[Network.curtis], 77 | accounts: PRIVATE_KEY 78 | ? [PRIVATE_KEY] 79 | : { 80 | mnemonic: MNEMONIC, 81 | path: MNEMONIC_PATH, 82 | initialIndex: 0, 83 | count: 20, 84 | }, 85 | }, 86 | apechain: { 87 | // gasPrice: 1 * GWEI, 88 | url: NETWORKS_RPC_URL[Network.apechain], 89 | accounts: PRIVATE_KEY 90 | ? [PRIVATE_KEY] 91 | : { 92 | mnemonic: MNEMONIC, 93 | path: MNEMONIC_PATH, 94 | initialIndex: 0, 95 | count: 20, 96 | }, 97 | }, 98 | }, 99 | // sourcify: { 100 | // enabled: false, 101 | // apiUrl: "https://sourcify.dev/server", 102 | // browserUrl: "https://repo.sourcify.dev", 103 | // }, 104 | etherscan: { 105 | apiKey: { 106 | sepolia: ETHERSCAN_KEY, 107 | mainnet: ETHERSCAN_KEY, 108 | curtis: ETHERSCAN_KEY, 109 | apechain: ETHERSCAN_KEY_APECHAIN, 110 | }, 111 | customChains: [ 112 | { 113 | network: "apechain", 114 | chainId: 33139, 115 | urls: { 116 | apiURL: "https://api.apescan.io/api", 117 | browserURL: "https://apescan.io", 118 | }, 119 | }, 120 | { 121 | network: "curtis", 122 | chainId: 33111, 123 | urls: { 124 | apiURL: "https://curtis.explorer.caldera.xyz/api", 125 | browserURL: "https://curtis.explorer.caldera.xyz/", 126 | }, 127 | }, 128 | ], 129 | }, 130 | solidity: { 131 | compilers: [ 132 | { 133 | version: "0.8.18", 134 | settings: { optimizer: { enabled: true, runs: 200 } }, 135 | }, 136 | { 137 | version: "0.8.10", 138 | settings: { optimizer: { enabled: true, runs: 200 } }, 139 | }, 140 | ], 141 | }, 142 | paths: { 143 | sources: "./contracts/", 144 | tests: "./test", 145 | cache: "./cache", 146 | artifacts: "./artifacts", 147 | }, 148 | abiExporter: { 149 | path: "./abis", 150 | runOnCompile: true, 151 | clear: true, 152 | flat: true, 153 | pretty: false, 154 | except: ["test*", "@openzeppelin*"], 155 | }, 156 | gasReporter: { 157 | enabled: REPORT_GAS, 158 | excludeContracts: ["test*", "@openzeppelin*"], 159 | }, 160 | contractSizer: { 161 | alphaSort: true, 162 | runOnCompile: false, 163 | disambiguatePaths: false, 164 | }, 165 | }; 166 | 167 | export default config; 168 | -------------------------------------------------------------------------------- /contracts/test/MockBendLendPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import {ILendPoolAddressesProvider} from "../misc/interfaces/ILendPoolAddressesProvider.sol"; 8 | import {ILendPool} from "../misc/interfaces/ILendPool.sol"; 9 | 10 | import "./MockBendLendPoolLoan.sol"; 11 | 12 | contract MockBendLendPool is ILendPool { 13 | ILendPoolAddressesProvider public addressesProvider; 14 | 15 | function setAddressesProvider(address addressesProvider_) public { 16 | addressesProvider = ILendPoolAddressesProvider(addressesProvider_); 17 | } 18 | 19 | function borrow( 20 | address reserveAsset, 21 | uint256 amount, 22 | address nftAsset, 23 | uint256 nftTokenId, 24 | address onBehalfOf, 25 | uint16 /*referralCode*/ 26 | ) external { 27 | MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( 28 | ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() 29 | ); 30 | lendPoolLoan.setLoanData(nftAsset, nftTokenId, onBehalfOf, reserveAsset, amount); 31 | 32 | IERC721(nftAsset).transferFrom(msg.sender, address(this), nftTokenId); 33 | IERC20(reserveAsset).transfer(msg.sender, amount); 34 | } 35 | 36 | function repay(address nftAsset, uint256 nftTokenId, uint256 amount) external returns (uint256, bool) { 37 | MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( 38 | ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() 39 | ); 40 | 41 | uint256 loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); 42 | 43 | (address borrower, address reserveAsset, uint256 totalDebt, uint256 bidFineInLoan) = lendPoolLoan.getLoanData( 44 | loanId 45 | ); 46 | require(bidFineInLoan == 0, "loan is in auction"); 47 | 48 | if (amount > totalDebt) { 49 | amount = totalDebt; 50 | } 51 | totalDebt -= amount; 52 | lendPoolLoan.setTotalDebt(loanId, totalDebt); 53 | 54 | IERC20(reserveAsset).transferFrom(msg.sender, address(this), amount); 55 | 56 | if (totalDebt == 0) { 57 | IERC721(nftAsset).transferFrom(address(this), borrower, nftTokenId); 58 | } 59 | 60 | return (amount, true); 61 | } 62 | 63 | function redeem(address nftAsset, uint256 nftTokenId, uint256 amount, uint256 bidFine) external returns (uint256) { 64 | MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( 65 | ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() 66 | ); 67 | uint256 loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); 68 | 69 | (, address reserveAsset, uint256 totalDebt, uint256 bidFineInLoan) = lendPoolLoan.getLoanData(loanId); 70 | uint256 maxRedeemAmount = (totalDebt * 9) / 10; 71 | require(amount <= maxRedeemAmount, "exceed max redeem amount"); 72 | require(bidFine == bidFineInLoan, "insufficient bid fine"); 73 | 74 | IERC20(reserveAsset).transferFrom(msg.sender, address(this), (amount + bidFine)); 75 | 76 | totalDebt -= amount; 77 | lendPoolLoan.setTotalDebt(loanId, totalDebt); 78 | lendPoolLoan.setBidFine(loanId, 0); 79 | 80 | return amount; 81 | } 82 | 83 | function getNftDebtData( 84 | address nftAsset, 85 | uint256 nftTokenId 86 | ) 87 | external 88 | view 89 | returns ( 90 | uint256 loanId, 91 | address reserveAsset, 92 | uint256 totalCollateral, 93 | uint256 totalDebt, 94 | uint256 availableBorrows, 95 | uint256 healthFactor 96 | ) 97 | { 98 | MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( 99 | ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() 100 | ); 101 | 102 | loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); 103 | (, reserveAsset, totalDebt, ) = lendPoolLoan.getLoanData(loanId); 104 | 105 | totalCollateral = totalDebt; 106 | availableBorrows = 0; 107 | healthFactor = 1e18; 108 | } 109 | 110 | function getNftAuctionData( 111 | address nftAsset, 112 | uint256 nftTokenId 113 | ) 114 | external 115 | view 116 | returns (uint256 loanId, address bidderAddress, uint256 bidPrice, uint256 bidBorrowAmount, uint256 bidFine) 117 | { 118 | MockBendLendPoolLoan lendPoolLoan = MockBendLendPoolLoan( 119 | ILendPoolAddressesProvider(addressesProvider).getLendPoolLoan() 120 | ); 121 | 122 | loanId = lendPoolLoan.getCollateralLoanId(nftAsset, nftTokenId); 123 | 124 | (, , bidBorrowAmount, bidFine) = lendPoolLoan.getLoanData(loanId); 125 | 126 | bidderAddress = msg.sender; 127 | bidPrice = (bidBorrowAmount * 105) / 100; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /abis/DefaultWithdrawStrategy.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [ 4 | { 5 | "internalType": "contract IApeCoinStaking", 6 | "name": "apeCoinStaking_", 7 | "type": "address" 8 | }, 9 | { 10 | "internalType": "contract INftVault", 11 | "name": "nftVault_", 12 | "type": "address" 13 | }, 14 | { 15 | "internalType": "contract ICoinPool", 16 | "name": "coinPool_", 17 | "type": "address" 18 | }, 19 | { 20 | "internalType": "contract IStakeManager", 21 | "name": "staker_", 22 | "type": "address" 23 | } 24 | ], 25 | "stateMutability": "nonpayable", 26 | "type": "constructor" 27 | }, 28 | { 29 | "anonymous": false, 30 | "inputs": [ 31 | { 32 | "indexed": true, 33 | "internalType": "address", 34 | "name": "previousOwner", 35 | "type": "address" 36 | }, 37 | { 38 | "indexed": true, 39 | "internalType": "address", 40 | "name": "newOwner", 41 | "type": "address" 42 | } 43 | ], 44 | "name": "OwnershipTransferred", 45 | "type": "event" 46 | }, 47 | { 48 | "inputs": [], 49 | "name": "apeCoinStaking", 50 | "outputs": [ 51 | { 52 | "internalType": "contract IApeCoinStaking", 53 | "name": "", 54 | "type": "address" 55 | } 56 | ], 57 | "stateMutability": "view", 58 | "type": "function" 59 | }, 60 | { 61 | "inputs": [], 62 | "name": "bakc", 63 | "outputs": [ 64 | { 65 | "internalType": "address", 66 | "name": "", 67 | "type": "address" 68 | } 69 | ], 70 | "stateMutability": "view", 71 | "type": "function" 72 | }, 73 | { 74 | "inputs": [], 75 | "name": "bayc", 76 | "outputs": [ 77 | { 78 | "internalType": "address", 79 | "name": "", 80 | "type": "address" 81 | } 82 | ], 83 | "stateMutability": "view", 84 | "type": "function" 85 | }, 86 | { 87 | "inputs": [], 88 | "name": "coinPool", 89 | "outputs": [ 90 | { 91 | "internalType": "contract ICoinPool", 92 | "name": "", 93 | "type": "address" 94 | } 95 | ], 96 | "stateMutability": "view", 97 | "type": "function" 98 | }, 99 | { 100 | "inputs": [], 101 | "name": "initGlobalState", 102 | "outputs": [], 103 | "stateMutability": "nonpayable", 104 | "type": "function" 105 | }, 106 | { 107 | "inputs": [], 108 | "name": "mayc", 109 | "outputs": [ 110 | { 111 | "internalType": "address", 112 | "name": "", 113 | "type": "address" 114 | } 115 | ], 116 | "stateMutability": "view", 117 | "type": "function" 118 | }, 119 | { 120 | "inputs": [], 121 | "name": "nftVault", 122 | "outputs": [ 123 | { 124 | "internalType": "contract INftVault", 125 | "name": "", 126 | "type": "address" 127 | } 128 | ], 129 | "stateMutability": "view", 130 | "type": "function" 131 | }, 132 | { 133 | "inputs": [], 134 | "name": "owner", 135 | "outputs": [ 136 | { 137 | "internalType": "address", 138 | "name": "", 139 | "type": "address" 140 | } 141 | ], 142 | "stateMutability": "view", 143 | "type": "function" 144 | }, 145 | { 146 | "inputs": [], 147 | "name": "renounceOwnership", 148 | "outputs": [], 149 | "stateMutability": "nonpayable", 150 | "type": "function" 151 | }, 152 | { 153 | "inputs": [ 154 | { 155 | "internalType": "address", 156 | "name": "apeCoinStaking_", 157 | "type": "address" 158 | } 159 | ], 160 | "name": "setApeCoinStaking", 161 | "outputs": [], 162 | "stateMutability": "nonpayable", 163 | "type": "function" 164 | }, 165 | { 166 | "inputs": [], 167 | "name": "staker", 168 | "outputs": [ 169 | { 170 | "internalType": "contract IStakeManager", 171 | "name": "", 172 | "type": "address" 173 | } 174 | ], 175 | "stateMutability": "view", 176 | "type": "function" 177 | }, 178 | { 179 | "inputs": [ 180 | { 181 | "internalType": "address", 182 | "name": "newOwner", 183 | "type": "address" 184 | } 185 | ], 186 | "name": "transferOwnership", 187 | "outputs": [], 188 | "stateMutability": "nonpayable", 189 | "type": "function" 190 | }, 191 | { 192 | "inputs": [ 193 | { 194 | "internalType": "uint256", 195 | "name": "required", 196 | "type": "uint256" 197 | } 198 | ], 199 | "name": "withdrawApeCoin", 200 | "outputs": [ 201 | { 202 | "internalType": "uint256", 203 | "name": "withdrawn", 204 | "type": "uint256" 205 | } 206 | ], 207 | "stateMutability": "nonpayable", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [], 212 | "name": "wrapApeCoin", 213 | "outputs": [ 214 | { 215 | "internalType": "contract IERC20", 216 | "name": "", 217 | "type": "address" 218 | } 219 | ], 220 | "stateMutability": "view", 221 | "type": "function" 222 | } 223 | ] 224 | -------------------------------------------------------------------------------- /test/hardhat/stakednft/StNft.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contracts, Env, makeSuite, Snapshots } from "../setup"; 3 | import { mintNft } from "../utils"; 4 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 5 | import { MintableERC721, IStakedNft } from "../../../typechain-types"; 6 | import { constants } from "ethers"; 7 | 8 | export function makeStNftTest(name: string, getNfts: (contracts: Contracts) => [IStakedNft, MintableERC721]): void { 9 | makeSuite(name, (contracts: Contracts, env: Env, snapshots: Snapshots) => { 10 | let staker: SignerWithAddress; 11 | let stNftOwner: SignerWithAddress; 12 | let lastRevert: string; 13 | let tokenIds: number[]; 14 | let nft: MintableERC721; 15 | let stNft: IStakedNft; 16 | 17 | before(async () => { 18 | staker = env.accounts[1]; 19 | stNftOwner = env.accounts[2]; 20 | [stNft, nft] = getNfts(contracts); 21 | expect(await stNft.underlyingAsset()).eq(nft.address); 22 | 23 | tokenIds = [0, 1, 2, 3, 4, 5]; 24 | await mintNft(staker, nft, tokenIds); 25 | for (const id of tokenIds) { 26 | expect(await stNft.tokenURI(id)).eq(await nft.tokenURI(id)); 27 | } 28 | await nft.connect(staker).setApprovalForAll(stNft.address, true); 29 | 30 | lastRevert = "init"; 31 | await snapshots.capture(lastRevert); 32 | }); 33 | 34 | afterEach(async () => { 35 | if (lastRevert) { 36 | await snapshots.revert(lastRevert); 37 | } 38 | }); 39 | 40 | it("onlyAuthorized: reverts", async () => { 41 | await expect(stNft.connect(staker).mint(stNftOwner.address, tokenIds)).revertedWith( 42 | "StNft: caller is not authorized" 43 | ); 44 | 45 | await stNft.connect(env.admin).authorise(staker.address, true); 46 | lastRevert = "init"; 47 | await snapshots.capture(lastRevert); 48 | }); 49 | 50 | it("mint", async () => { 51 | expect(await stNft.totalStaked(staker.address)).eq(0); 52 | expect(await stNft.totalStaked(stNftOwner.address)).eq(0); 53 | 54 | for (const id of tokenIds) { 55 | await nft.connect(staker).transferFrom(staker.address, contracts.nftVault.address, id); 56 | } 57 | 58 | await expect(stNft.connect(staker).mint(stNftOwner.address, tokenIds)).not.reverted; 59 | 60 | expect(await stNft.totalStaked(staker.address)).eq(tokenIds.length); 61 | expect(await stNft.totalStaked(stNftOwner.address)).eq(0); 62 | for (const [i, id] of tokenIds.entries()) { 63 | expect(await contracts.nftVault.stakerOf(nft.address, id)).eq(staker.address); 64 | expect(await nft.ownerOf(id)).eq(contracts.nftVault.address); 65 | expect(await stNft.ownerOf(id)).eq(stNftOwner.address); 66 | expect(await stNft.tokenOfStakerByIndex(staker.address, i)).eq(id); 67 | } 68 | lastRevert = "mint"; 69 | await snapshots.capture(lastRevert); 70 | }); 71 | 72 | it("setDelegateCash", async () => { 73 | { 74 | await stNft.connect(stNftOwner).setDelegateCash(stNftOwner.address, tokenIds, true); 75 | 76 | const delegates = await stNft.getDelegateCashForToken(tokenIds); 77 | expect(delegates.length).eq(tokenIds.length); 78 | for (let i = 0; i < delegates.length; i++) { 79 | expect(delegates[i].length).eq(1); 80 | expect(delegates[i][0]).eq(stNftOwner.address); 81 | } 82 | } 83 | 84 | { 85 | await stNft.connect(stNftOwner).setDelegateCash(stNftOwner.address, tokenIds, false); 86 | 87 | const delegates = await stNft.getDelegateCashForToken(tokenIds); 88 | expect(delegates.length).eq(tokenIds.length); 89 | for (let i = 0; i < delegates.length; i++) { 90 | expect(delegates[i].length).eq(0); 91 | } 92 | } 93 | 94 | lastRevert = "mint"; 95 | }); 96 | 97 | it("setDelegateCashV2", async () => { 98 | { 99 | await stNft.connect(stNftOwner).setDelegateCashV2(stNftOwner.address, tokenIds, true); 100 | 101 | const delegates = await stNft.getDelegateCashForTokenV2(tokenIds); 102 | expect(delegates.length).eq(tokenIds.length); 103 | for (let i = 0; i < delegates.length; i++) { 104 | expect(delegates[i].length).eq(1); 105 | expect(delegates[i][0]).eq(stNftOwner.address); 106 | } 107 | } 108 | 109 | { 110 | await stNft.connect(stNftOwner).setDelegateCashV2(stNftOwner.address, tokenIds, false); 111 | 112 | const delegates = await stNft.getDelegateCashForTokenV2(tokenIds); 113 | expect(delegates.length).eq(tokenIds.length); 114 | for (let i = 0; i < delegates.length; i++) { 115 | expect(delegates[i].length).eq(0); 116 | } 117 | } 118 | 119 | lastRevert = "mint"; 120 | }); 121 | 122 | it("burn", async () => { 123 | expect(await stNft.totalStaked(staker.address)).eq(tokenIds.length); 124 | expect(await stNft.totalStaked(stNftOwner.address)).eq(0); 125 | 126 | await expect(stNft.connect(staker).burn(stNftOwner.address, tokenIds)).not.reverted; 127 | 128 | expect(await stNft.totalStaked(staker.address)).eq(0); 129 | expect(await stNft.totalStaked(stNftOwner.address)).eq(0); 130 | 131 | for (const [i, id] of tokenIds.entries()) { 132 | expect(await contracts.nftVault.stakerOf(nft.address, id)).eq(constants.AddressZero); 133 | expect(await nft.ownerOf(id)).eq(contracts.nftVault.address); 134 | await expect(stNft.ownerOf(id)).revertedWith("ERC721: invalid token ID"); 135 | await expect(stNft.tokenOfStakerByIndex(staker.address, i)).revertedWith("stNft: staker index out of bounds"); 136 | } 137 | }); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /tasks/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { constants, Contract, ContractReceipt, ContractTransaction, Signer } from "ethers"; 3 | import { verifyEtherscanContract } from "./verification"; 4 | import { DRE, DB } from "./DRE"; 5 | 6 | export const waitForTx = async (tx: ContractTransaction): Promise => await tx.wait(1); 7 | 8 | export const registerContractInJsonDB = async (contractId: string, contractInstance: Contract): Promise => { 9 | const currentNetwork = DRE.network.name; 10 | console.log(`\n*** ${contractId} ***\n`); 11 | console.log(`Network: ${currentNetwork}`); 12 | console.log(`tx: ${contractInstance.deployTransaction.hash}`); 13 | console.log(`contract address: ${contractInstance.address}`); 14 | console.log(`deployer address: ${contractInstance.deployTransaction.from}`); 15 | console.log(`gas price: ${contractInstance.deployTransaction.gasPrice}`); 16 | console.log(`gas used: ${contractInstance.deployTransaction.gasLimit}`); 17 | console.log(`\n******`); 18 | console.log(); 19 | 20 | DB.set(contractId, { 21 | address: contractInstance.address, 22 | deployer: contractInstance.deployTransaction.from, 23 | }).write(); 24 | }; 25 | 26 | export const getContractAddressFromDB = async (id: string): Promise => { 27 | const contractAtDb = DB.get(id).value(); 28 | if (contractAtDb?.address) { 29 | return contractAtDb.address; 30 | } 31 | throw Error(`Missing contract address ${id} from local DB`); 32 | }; 33 | 34 | export const getDeploySigner = async (): Promise => (await getSigners())[0]; 35 | 36 | export const getSigners = async (): Promise => { 37 | return await Promise.all(await DRE.ethers.getSigners()); 38 | }; 39 | 40 | export const getSignerByAddress = async (address: string): Promise => { 41 | return await DRE.ethers.getSigner(address); 42 | }; 43 | 44 | export const getSignersAddresses = async (): Promise => 45 | await Promise.all((await getSigners()).map((signer) => signer.getAddress())); 46 | 47 | export const deployImplementation = async ( 48 | contractName: string, 49 | verify?: boolean, 50 | libraries?: { [libraryName: string]: string } 51 | ): Promise => { 52 | console.log("deploy", contractName, "with libraries:", libraries); 53 | const instance = await (await DRE.ethers.getContractFactory(contractName, { libraries })) 54 | .connect(await getDeploySigner()) 55 | .deploy(); 56 | console.log("Impl address:", instance.address); 57 | 58 | if (verify) { 59 | await verifyEtherscanContract(instance.address, []); 60 | } 61 | 62 | return instance as ContractType; 63 | }; 64 | 65 | export const deployContract = async ( 66 | contractName: string, 67 | args: any[], 68 | verify?: boolean, 69 | dbKey?: string, 70 | libraries?: { [libraryName: string]: string } 71 | ): Promise => { 72 | dbKey = dbKey || contractName; 73 | console.log("deploy", dbKey); 74 | const instance = await (await DRE.ethers.getContractFactory(contractName, { libraries })) 75 | .connect(await getDeploySigner()) 76 | .deploy(...args); 77 | 78 | await withSaveAndVerify(instance, dbKey, args, verify); 79 | return instance as ContractType; 80 | }; 81 | 82 | export const deployProxyContract = async ( 83 | contractName: string, 84 | args: any[], 85 | verify?: boolean, 86 | dbKey?: string, 87 | libraries?: { [libraryName: string]: string } 88 | ): Promise => { 89 | dbKey = dbKey || contractName; 90 | console.log("deploy", dbKey); 91 | let hasLib = false; 92 | if (libraries) { 93 | hasLib = true; 94 | } 95 | const factory = await DRE.ethers.getContractFactory(contractName, { libraries }); 96 | const instance = await DRE.upgrades.deployProxy(factory, args, { 97 | timeout: 0, 98 | unsafeAllowLinkedLibraries: hasLib, 99 | }); 100 | await withSaveAndVerify(instance, dbKey, args, verify); 101 | return instance as ContractType; 102 | }; 103 | 104 | export const deployProxyContractWithoutInit = async ( 105 | contractName: string, 106 | args: any[], 107 | verify?: boolean, 108 | dbKey?: string 109 | ): Promise => { 110 | dbKey = dbKey || contractName; 111 | console.log("deploy", dbKey); 112 | const factory = await DRE.ethers.getContractFactory(contractName); 113 | const instance = await DRE.upgrades.deployProxy(factory, args, { 114 | timeout: 0, 115 | initializer: false, 116 | }); 117 | await withSaveAndVerify(instance, dbKey, args, verify); 118 | return instance as ContractType; 119 | }; 120 | 121 | export const withSaveAndVerify = async ( 122 | instance: Contract, 123 | id: string, 124 | args: (string | string[] | any)[], 125 | verify?: boolean 126 | ): Promise => { 127 | await waitForTx(instance.deployTransaction); 128 | await registerContractInJsonDB(id, instance); 129 | if (verify) { 130 | let impl = constants.AddressZero; 131 | try { 132 | impl = await DRE.upgrades.erc1967.getImplementationAddress(instance.address); 133 | } catch (error) { 134 | impl = constants.AddressZero; 135 | } 136 | if (impl !== constants.AddressZero) { 137 | await verifyEtherscanContract(impl, []); 138 | } else { 139 | await verifyEtherscanContract(instance.address, args); 140 | } 141 | } 142 | return instance; 143 | }; 144 | 145 | export const getChainId = async (): Promise => { 146 | return (await DRE.ethers.provider.getNetwork()).chainId; 147 | }; 148 | 149 | export const getContract = async ( 150 | contractName: string, 151 | address: string 152 | ): Promise => (await DRE.ethers.getContractAt(contractName, address)) as ContractType; 153 | 154 | export const getContractFromDB = async (id: string): Promise => { 155 | return getContract(id, await getContractAddressFromDB(id)); 156 | }; 157 | -------------------------------------------------------------------------------- /contracts/interfaces/INftVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {EnumerableSetUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/structs/EnumerableSetUpgradeable.sol"; 5 | import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; 6 | import {IERC721ReceiverUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721ReceiverUpgradeable.sol"; 7 | 8 | import {IApeCoinStaking} from "./IApeCoinStaking.sol"; 9 | import {IDelegationRegistry} from "../interfaces/IDelegationRegistry.sol"; 10 | import {IDelegateRegistryV2} from "../interfaces/IDelegateRegistryV2.sol"; 11 | 12 | interface INftVault is IERC721ReceiverUpgradeable { 13 | event NftDeposited(address indexed nft, address indexed owner, address indexed staker, uint256[] tokenIds); 14 | event NftWithdrawn(address indexed nft, address indexed owner, address indexed staker, uint256[] tokenIds); 15 | 16 | event SingleNftStaked(address indexed nft, address indexed staker, uint256[] tokenIds, uint256[] amounts); 17 | event SingleNftUnstaked(address indexed nft, address indexed staker, uint256[] tokenIds, uint256[] amounts); 18 | 19 | event SingleNftClaimed(address indexed nft, address indexed staker, uint256[] tokenIds, uint256 rewards); 20 | 21 | struct NftStatus { 22 | address owner; 23 | address staker; 24 | } 25 | 26 | struct VaultStorage { 27 | // nft address => nft tokenId => nftStatus 28 | mapping(address => mapping(uint256 => NftStatus)) nfts; 29 | // nft address => staker address => position 30 | mapping(address => mapping(address => Position)) positions; 31 | // nft address => staker address => staking nft tokenId array 32 | mapping(address => mapping(address => EnumerableSetUpgradeable.UintSet)) stakingTokenIds; 33 | IApeCoinStaking apeCoinStaking; 34 | IERC20Upgradeable wrapApeCoin; 35 | address bayc; 36 | address mayc; 37 | address bakc; 38 | IDelegationRegistry delegationRegistry; 39 | mapping(address => bool) authorized; 40 | IDelegateRegistryV2 delegationRegistryV2; 41 | uint256 minGasFeeAmount; 42 | uint256 totalPendingFunds; 43 | // poolId => tokenId => rewardsDebt 44 | mapping(uint256 => mapping(uint256 => int256)) pendingClaimRewardsDebts; 45 | } 46 | 47 | struct VaultStorageUI { 48 | IApeCoinStaking apeCoinStaking; 49 | IERC20Upgradeable wrapApeCoin; 50 | address bayc; 51 | address mayc; 52 | address bakc; 53 | IDelegationRegistry delegationRegistry; 54 | IDelegateRegistryV2 delegationRegistryV2; 55 | uint256 minGasFeeAmount; 56 | uint256 totalPendingFunds; 57 | } 58 | 59 | struct Position { 60 | uint256 stakedAmount; 61 | int256 rewardsDebt; 62 | } 63 | 64 | function authorise(address addr_, bool authorized_) external; 65 | 66 | function stakerOf(address nft_, uint256 tokenId_) external view returns (address); 67 | 68 | function ownerOf(address nft_, uint256 tokenId_) external view returns (address); 69 | 70 | function positionOf(address nft_, address staker_) external view returns (Position memory); 71 | 72 | function pendingRewards(address nft_, address staker_) external view returns (uint256); 73 | 74 | function totalStakingNft(address nft_, address staker_) external view returns (uint256); 75 | 76 | function stakingNftIdByIndex(address nft_, address staker_, uint256 index_) external view returns (uint256); 77 | 78 | function isStaking(address nft_, address staker_, uint256 tokenId_) external view returns (bool); 79 | 80 | // delegate.cash V1 81 | 82 | function setDelegateCash(address delegate_, address nft_, uint256[] calldata tokenIds, bool value) external; 83 | 84 | function getDelegateCashForToken( 85 | address nft_, 86 | uint256[] calldata tokenIds_ 87 | ) external view returns (address[][] memory); 88 | 89 | // delegate.cash V2 90 | 91 | function setDelegateCashV2(address delegate_, address nft_, uint256[] calldata tokenIds, bool value) external; 92 | 93 | function getDelegateCashForTokenV2( 94 | address nft_, 95 | uint256[] calldata tokenIds_ 96 | ) external view returns (address[][] memory); 97 | 98 | // deposit nft 99 | function depositNft(address nft_, uint256[] calldata tokenIds_, address staker_) external; 100 | 101 | // withdraw nft 102 | function withdrawNft(address nft_, uint256[] calldata tokenIds_) external; 103 | 104 | // stake 105 | function stakeBaycPool(uint256[] calldata tokenIds_, uint256[] calldata amounts_) external; 106 | 107 | function stakeMaycPool(uint256[] calldata tokenIds_, uint256[] calldata amounts_) external; 108 | 109 | function stakeBakcPool(uint256[] calldata tokenIds_, uint256[] calldata amounts_) external; 110 | 111 | // unstake 112 | function unstakeBaycPool( 113 | uint256[] calldata tokenIds_, 114 | uint256[] calldata amounts_, 115 | address recipient_ 116 | ) external returns (uint256 principal, uint256 rewards); 117 | 118 | function unstakeMaycPool( 119 | uint256[] calldata tokenIds_, 120 | uint256[] calldata amounts_, 121 | address recipient_ 122 | ) external returns (uint256 principal, uint256 rewards); 123 | 124 | function unstakeBakcPool( 125 | uint256[] calldata tokenIds_, 126 | uint256[] calldata amounts_, 127 | address recipient_ 128 | ) external returns (uint256 principal, uint256 rewards); 129 | 130 | // claim rewards 131 | function claimBaycPool(uint256[] calldata tokenIds_, address recipient_) external returns (uint256 rewards); 132 | 133 | function claimMaycPool(uint256[] calldata tokenIds_, address recipient_) external returns (uint256 rewards); 134 | 135 | function claimBakcPool(uint256[] calldata tokenIds_, address recipient_) external returns (uint256 rewards); 136 | 137 | function withdrawPendingFunds(address recipient_) external; 138 | } 139 | -------------------------------------------------------------------------------- /tasks/config.ts: -------------------------------------------------------------------------------- 1 | export enum Network { 2 | mainnet = "mainnet", 3 | sepolia = "sepolia", 4 | apechain = "apechain", 5 | curtis = "curtis", 6 | } 7 | 8 | export interface Params { 9 | [Network.mainnet]: T; 10 | [Network.sepolia]: T; 11 | [Network.apechain]: T; 12 | [Network.curtis]: T; 13 | } 14 | 15 | export const getParams = ({ mainnet, sepolia, apechain, curtis }: Params, network: string): T => { 16 | network = Network[network as keyof typeof Network]; 17 | switch (network) { 18 | case Network.mainnet: 19 | return mainnet; 20 | case Network.sepolia: 21 | return sepolia; 22 | case Network.curtis: 23 | return curtis; 24 | case Network.apechain: 25 | return apechain; 26 | default: 27 | return curtis; 28 | } 29 | }; 30 | 31 | const INFURA_KEY = process.env.INFURA_KEY || ""; 32 | const ALCHEMY_KEY = process.env.ALCHEMY_KEY || ""; 33 | 34 | export const NETWORKS_RPC_URL: Params = { 35 | [Network.mainnet]: ALCHEMY_KEY 36 | ? `https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}` 37 | : `https://mainnet.infura.io/v3/${INFURA_KEY}`, 38 | [Network.sepolia]: ALCHEMY_KEY 39 | ? `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_KEY}` 40 | : `https://sepolia.infura.io/v3/${INFURA_KEY}`, 41 | [Network.apechain]: "https://rpc.apechain.com/http", 42 | [Network.curtis]: "https://curtis.rpc.caldera.xyz/http", 43 | }; 44 | 45 | export const FEE: Params = { 46 | [Network.mainnet]: "400", 47 | [Network.sepolia]: "400", 48 | [Network.apechain]: "400", 49 | [Network.curtis]: "400", 50 | }; 51 | 52 | export const FEE_RECIPIENT: Params = { 53 | [Network.mainnet]: "", 54 | [Network.sepolia]: "", 55 | [Network.apechain]: "0x5F532d6D901fF6591652355Ed5769AD414E6c6cb", 56 | [Network.curtis]: "0xafF5C36642385b6c7Aaf7585eC785aB2316b5db6", 57 | }; 58 | 59 | export const WAPE_COIN: Params = { 60 | [Network.mainnet]: "", 61 | [Network.sepolia]: "", 62 | [Network.apechain]: "0x48b62137EdfA95a428D35C09E44256a739F6B557", 63 | [Network.curtis]: "0x647dc527Bd7dFEE4DD468cE6fC62FC50fa42BD8b", 64 | }; 65 | 66 | export const BEACON: Params = { 67 | [Network.mainnet]: "", 68 | [Network.sepolia]: "", 69 | [Network.apechain]: "0x00000000000087c6dbaDC090d39BC10316f20658", 70 | [Network.curtis]: "0x52912571414Db261E4122B0658037122dAe9718C", 71 | }; 72 | 73 | export const APE_STAKING: Params = { 74 | [Network.mainnet]: "", 75 | [Network.sepolia]: "", 76 | [Network.apechain]: "0x4ba2396086d52ca68a37d9c0fa364286e9c7835a", 77 | [Network.curtis]: "0x5350A3FFEDCb50775f425982a11E31E2Ba43F34B", 78 | }; 79 | 80 | export const BAYC: Params = { 81 | [Network.mainnet]: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", 82 | [Network.sepolia]: "0xE15A78992dd4a9d6833eA7C9643650d3b0a2eD2B", 83 | [Network.apechain]: "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d", 84 | [Network.curtis]: "0x52f51701ed1B560108fF71862330f9C8b4a15c8e", 85 | }; 86 | 87 | export const MAYC: Params = { 88 | [Network.mainnet]: "0x60E4d786628Fea6478F785A6d7e704777c86a7c6", 89 | [Network.sepolia]: "0xD0ff8ae7E3D9591605505D3db9C33b96c4809CDC", 90 | [Network.apechain]: "0x60E4d786628Fea6478F785A6d7e704777c86a7c6", 91 | [Network.curtis]: "0x35e13d4545Dd3935F87a056495b5Bae4f3aB9049", 92 | }; 93 | 94 | export const BAKC: Params = { 95 | [Network.mainnet]: "0xba30E5F9Bb24caa003E9f2f0497Ad287FDF95623", 96 | [Network.sepolia]: "0xE8636AFf2F1Cf508988b471d7e221e1B83873FD9", 97 | [Network.apechain]: "0xba30E5F9Bb24caa003E9f2f0497Ad287FDF95623", 98 | [Network.curtis]: "0xFe94A8d9260a96e6da8BD6BfA537b49E73791567", 99 | }; 100 | 101 | export const DELEAGATE_CASH: Params = { 102 | [Network.mainnet]: "", 103 | [Network.sepolia]: "", 104 | [Network.apechain]: "0x0000000000000000000000000000000000000000", 105 | [Network.curtis]: "0x0000000000000000000000000000000000000000", 106 | }; 107 | 108 | export const DELEAGATE_CASH_V2: Params = { 109 | [Network.mainnet]: "0x00000000000000447e69651d841bD8D104Bed493", 110 | [Network.sepolia]: "0x00000000000000447e69651d841bD8D104Bed493", 111 | [Network.apechain]: "", 112 | [Network.curtis]: "", 113 | }; 114 | 115 | export const BNFT_REGISTRY: Params = { 116 | [Network.mainnet]: "", 117 | [Network.sepolia]: "", 118 | [Network.apechain]: "0xcAbe4E00a44Ff38990A42f43312d470DE5796FA6", 119 | [Network.curtis]: "0xc31078cC745daE8f577EdBa2803405CE571cb9f8", 120 | }; 121 | 122 | export const AAVE_ADDRESS_PROVIDER: Params = { 123 | [Network.mainnet]: "", 124 | [Network.sepolia]: "", 125 | [Network.apechain]: "", 126 | [Network.curtis]: "", 127 | }; 128 | 129 | export const BEND_ADDRESS_PROVIDER: Params = { 130 | [Network.mainnet]: "", 131 | [Network.sepolia]: "", 132 | [Network.apechain]: "", 133 | [Network.curtis]: "", 134 | }; 135 | 136 | export const BAYC_REWARDS_SHARE_RATIO: Params = { 137 | [Network.mainnet]: "5000", 138 | [Network.sepolia]: "5000", 139 | [Network.apechain]: "5000", 140 | [Network.curtis]: "5000", 141 | }; 142 | 143 | export const MAYC_REWARDS_SHARE_RATIO: Params = { 144 | [Network.mainnet]: "5000", 145 | [Network.sepolia]: "5000", 146 | [Network.apechain]: "5000", 147 | [Network.curtis]: "5000", 148 | }; 149 | 150 | export const BAKC_REWARDS_SHARE_RATIO: Params = { 151 | [Network.mainnet]: "5000", 152 | [Network.sepolia]: "5000", 153 | [Network.apechain]: "5000", 154 | [Network.curtis]: "5000", 155 | }; 156 | 157 | export const STAKER_MANAGER_V1: Params = { 158 | [Network.mainnet]: "", 159 | [Network.sepolia]: "", 160 | [Network.apechain]: "", 161 | [Network.curtis]: "", 162 | }; 163 | 164 | export const COIN_POOL_V1: Params = { 165 | [Network.mainnet]: "", 166 | [Network.sepolia]: "", 167 | [Network.apechain]: "", 168 | [Network.curtis]: "", 169 | }; 170 | 171 | export const BENDV2_ADDRESS_PROVIDER: Params = { 172 | [Network.mainnet]: "", 173 | [Network.sepolia]: "", 174 | [Network.apechain]: "0x0000000000000000000000000000000000000000", 175 | [Network.curtis]: "0x0000000000000000000000000000000000000000", 176 | }; 177 | -------------------------------------------------------------------------------- /test/foundry/BendCoinPool.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | import "./SetupHelper.sol"; 4 | 5 | contract BendCoinPoolTest is SetupHelper { 6 | function setUp() public override { 7 | super.setUp(); 8 | } 9 | 10 | function testSingleUserDepositWithdrawNoRewards() public { 11 | address testUser = testUsers[0]; 12 | uint256 depositAmount = 1000000 * 10 ** 18; 13 | 14 | vm.startPrank(testUser); 15 | 16 | mockWAPE.deposit{value: depositAmount}(); 17 | mockWAPE.approve(address(coinPool), depositAmount); 18 | 19 | uint256 poolBalanceBeforeDeposit = mockWAPE.balanceOf(address(coinPool)); 20 | 21 | // deposit some coins 22 | coinPool.deposit(depositAmount, testUser); 23 | 24 | // withdraw all coins 25 | coinPool.withdraw(depositAmount, testUser, testUser); 26 | 27 | // check results 28 | uint256 userBalanceAfterWithdraw = mockWAPE.balanceOf(testUser); 29 | assertEq(userBalanceAfterWithdraw, depositAmount, "user balance not match after withdraw"); 30 | 31 | uint256 poolBalanceAfterWithdraw = mockWAPE.balanceOf(address(coinPool)); 32 | assertEq(poolBalanceAfterWithdraw, poolBalanceBeforeDeposit, "pool balance not match after withdraw"); 33 | 34 | vm.stopPrank(); 35 | } 36 | 37 | function testSingleUserDepositWithdrawHasRewards() public { 38 | address testUser = testUsers[0]; 39 | uint256 depositAmount = 100000 * 10 ** 18; 40 | uint256 rewardsAmount = 200000 * 10 ** 18; 41 | 42 | uint256 poolBalanceBeforeDeposit = mockWAPE.balanceOf(address(coinPool)); 43 | 44 | // deposit some coins 45 | vm.startPrank(testUser); 46 | mockWAPE.deposit{value: depositAmount}(); 47 | mockWAPE.approve(address(coinPool), depositAmount); 48 | coinPool.deposit(depositAmount, testUser); 49 | vm.stopPrank(); 50 | 51 | // make some rewards 52 | vm.deal(address(stakeManager), rewardsAmount); 53 | vm.startPrank(address(stakeManager)); 54 | mockWAPE.deposit{value: rewardsAmount}(); 55 | mockWAPE.approve(address(coinPool), rewardsAmount); 56 | coinPool.receiveApeCoin(0, rewardsAmount); 57 | vm.stopPrank(); 58 | 59 | uint256 expectedUserBalanceAfterWithdraw = coinPool.assetBalanceOf(testUser); 60 | 61 | // withdraw all coins 62 | vm.startPrank(testUser); 63 | coinPool.withdraw(expectedUserBalanceAfterWithdraw, testUser, testUser); 64 | vm.stopPrank(); 65 | 66 | // check results 67 | uint256 userBalanceAfterWithdraw = mockWAPE.balanceOf(testUser); 68 | assertEq(userBalanceAfterWithdraw, expectedUserBalanceAfterWithdraw, "user balance not match after withdraw"); 69 | 70 | uint256 poolBalanceAfterWithdraw = mockWAPE.balanceOf(address(coinPool)); 71 | assertGt(poolBalanceAfterWithdraw, poolBalanceBeforeDeposit, "pool balance not match after withdraw"); 72 | } 73 | 74 | function testMutipleUserDepositWithdrawNoRewards() public { 75 | uint256 userIndex = 0; 76 | uint256 userCount = 3; 77 | uint256[] memory depositAmounts = new uint256[](userCount); 78 | 79 | for (userIndex = 0; userIndex < userCount; userIndex++) { 80 | vm.startPrank(testUsers[userIndex]); 81 | 82 | depositAmounts[userIndex] = 1000000 * 10 ** 18 * (userIndex + 1); 83 | mockWAPE.deposit{value: depositAmounts[userIndex]}(); 84 | mockWAPE.approve(address(coinPool), depositAmounts[userIndex]); 85 | 86 | coinPool.deposit(depositAmounts[userIndex], testUsers[userIndex]); 87 | 88 | vm.stopPrank(); 89 | } 90 | 91 | // withdraw all coins 92 | for (userIndex = 0; userIndex < userCount; userIndex++) { 93 | vm.startPrank(testUsers[userIndex]); 94 | 95 | coinPool.withdraw(depositAmounts[userIndex], testUsers[userIndex], testUsers[userIndex]); 96 | 97 | vm.stopPrank(); 98 | } 99 | 100 | // check results 101 | for (userIndex = 0; userIndex < userCount; userIndex++) { 102 | uint256 userBalanceAfterWithdraw = mockWAPE.balanceOf(testUsers[userIndex]); 103 | assertEq(userBalanceAfterWithdraw, depositAmounts[userIndex], "user balance not match after withdraw"); 104 | } 105 | } 106 | 107 | function testMutipleUserDepositWithdrawHasRewards() public { 108 | uint256 userIndex = 0; 109 | uint256 userCount = 3; 110 | uint256 totalDepositAmount = 0; 111 | uint256[] memory depositAmounts = new uint256[](userCount); 112 | uint256 totalRewardsAmount = 100000 * 10 ** 18 * userCount; 113 | 114 | for (userIndex = 0; userIndex < userCount; userIndex++) { 115 | vm.startPrank(testUsers[userIndex]); 116 | 117 | depositAmounts[userIndex] = 1000000 * 10 ** 18 * (userIndex + 1); 118 | totalDepositAmount += depositAmounts[userIndex]; 119 | 120 | mockWAPE.deposit{value: depositAmounts[userIndex]}(); 121 | mockWAPE.approve(address(coinPool), depositAmounts[userIndex]); 122 | 123 | coinPool.deposit(depositAmounts[userIndex], testUsers[userIndex]); 124 | 125 | vm.stopPrank(); 126 | } 127 | 128 | // make some rewards 129 | vm.deal(address(stakeManager), totalRewardsAmount); 130 | vm.startPrank(address(stakeManager)); 131 | mockWAPE.deposit{value: totalRewardsAmount}(); 132 | mockWAPE.approve(address(coinPool), totalRewardsAmount); 133 | coinPool.receiveApeCoin(0, totalRewardsAmount); 134 | vm.stopPrank(); 135 | 136 | // withdraw all coins 137 | for (userIndex = 0; userIndex < userCount; userIndex++) { 138 | vm.startPrank(testUsers[userIndex]); 139 | 140 | uint256 userBalanceBeforeWithdraw = coinPool.assetBalanceOf(testUsers[userIndex]); 141 | coinPool.withdraw(userBalanceBeforeWithdraw, testUsers[userIndex], testUsers[userIndex]); 142 | 143 | vm.stopPrank(); 144 | } 145 | 146 | // check results 147 | for (userIndex = 0; userIndex < userCount; userIndex++) { 148 | uint256 userBalanceAfterWithdraw = mockWAPE.balanceOf(testUsers[userIndex]); 149 | assertGt(userBalanceAfterWithdraw, depositAmounts[userIndex], "user balance not match after withdraw"); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/foundry/BendStakeManager.t.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.18; 2 | 3 | import "./SetupHelper.sol"; 4 | 5 | contract BendStakeManagerTest is SetupHelper { 6 | function setUp() public override { 7 | super.setUp(); 8 | } 9 | 10 | function test_compound_StakeApeCoin() public { 11 | address testUser = testUsers[0]; 12 | uint256 depositCoinAmount = 1_000_000 * 1e18; 13 | 14 | // deposit some coins 15 | vm.startPrank(testUser); 16 | mockWAPE.deposit{value: depositCoinAmount}(); 17 | mockWAPE.approve(address(coinPool), depositCoinAmount); 18 | coinPool.deposit(depositCoinAmount, testUser); 19 | vm.stopPrank(); 20 | 21 | // stake all coins 22 | vm.startPrank(botAdmin); 23 | IStakeManager.CompoundArgs memory compoundArgs1; 24 | compoundArgs1.coinStakeThreshold = 0; 25 | stakeManager.compound(compoundArgs1); 26 | vm.stopPrank(); 27 | 28 | // make some rewards 29 | advanceTimeAndBlock(2 hours, 100); 30 | 31 | vm.startPrank(botAdmin); 32 | IStakeManager.CompoundArgs memory compoundArgs2; 33 | compoundArgs2.claimCoinPool = true; 34 | stakeManager.compound(compoundArgs2); 35 | vm.stopPrank(); 36 | 37 | uint256 userAssetAmount = coinPool.assetBalanceOf(testUser); 38 | 39 | // withdraw all coins 40 | vm.startPrank(testUser); 41 | coinPool.withdraw(userAssetAmount, testUser, testUser); 42 | vm.stopPrank(); 43 | 44 | uint256 userBalanceAfterWithdraw = mockWAPE.balanceOf(testUser); 45 | assertEq(userBalanceAfterWithdraw, userAssetAmount, "user balance not match after withdraw"); 46 | // there's no apecoin pool staking & rewards now 47 | assertEq(userAssetAmount, depositCoinAmount, "user asset not match deposited amout"); 48 | } 49 | 50 | function test_compound_StakeBAYC() public { 51 | address testUser = testUsers[0]; 52 | uint256 depositCoinAmount = 1_000_000 * 1e18; 53 | uint256[] memory testBaycTokenIds = new uint256[](1); 54 | 55 | // deposit some coins 56 | vm.startPrank(testUser); 57 | mockWAPE.deposit{value: depositCoinAmount}(); 58 | mockWAPE.approve(address(coinPool), depositCoinAmount); 59 | coinPool.deposit(depositCoinAmount, testUser); 60 | vm.stopPrank(); 61 | 62 | // deposit some nfts 63 | vm.startPrank(testUser); 64 | mockBAYC.setApprovalForAll(address(nftPool), true); 65 | 66 | testBaycTokenIds[0] = 100; 67 | mockBAYC.mint(testBaycTokenIds[0]); 68 | 69 | address[] memory nfts = new address[](1); 70 | uint256[][] memory tokenIds = new uint256[][](1); 71 | nfts[0] = address(mockBAYC); 72 | tokenIds[0] = testBaycTokenIds; 73 | 74 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[0]); 75 | 76 | vm.stopPrank(); 77 | 78 | // deposit all nfts 79 | vm.startPrank(botAdmin); 80 | IStakeManager.CompoundArgs memory compoundArgs0; 81 | compoundArgs0.deposit.bayc.tokenIds = testBaycTokenIds; 82 | compoundArgs0.deposit.bayc.owner = testUser; 83 | stakeManager.compound(compoundArgs0); 84 | vm.stopPrank(); 85 | 86 | // stake all nfts 87 | vm.startPrank(botAdmin); 88 | IStakeManager.CompoundArgs memory compoundArgs1; 89 | compoundArgs1.stake.bayc = testBaycTokenIds; 90 | stakeManager.compound(compoundArgs1); 91 | vm.stopPrank(); 92 | 93 | // make some rewards 94 | advanceTimeAndBlock(2 hours, 100); 95 | 96 | vm.startPrank(botAdmin); 97 | IStakeManager.CompoundArgs memory compoundArgs2; 98 | compoundArgs2.claimCoinPool = true; 99 | compoundArgs2.claim.bayc = testBaycTokenIds; 100 | stakeManager.compound(compoundArgs2); 101 | vm.stopPrank(); 102 | 103 | vm.startPrank(testUser); 104 | uint256 rewardsAmount = nftPool.claimable(nfts, tokenIds); 105 | assertGt(rewardsAmount, 0, "rewards should greater than 0"); 106 | vm.stopPrank(); 107 | 108 | uint256 balanceBeforeUnstake = testUser.balance; 109 | 110 | // unstake all nfts 111 | vm.startPrank(botAdmin); 112 | IStakeManager.CompoundArgs memory compoundArgs3; 113 | compoundArgs3.unstake.bayc = testBaycTokenIds; 114 | stakeManager.compound(compoundArgs3); 115 | vm.stopPrank(); 116 | 117 | // withdraw all nfts 118 | vm.startPrank(botAdmin); 119 | IStakeManager.CompoundArgs memory compoundArgs4; 120 | compoundArgs4.withdraw.bayc.tokenIds = testBaycTokenIds; 121 | compoundArgs4.withdraw.bayc.owner = testUser; 122 | stakeManager.compound(compoundArgs4); 123 | vm.stopPrank(); 124 | 125 | uint256 balanceAmount = testUser.balance; 126 | assertEq(balanceAmount, balanceBeforeUnstake + rewardsAmount, "balance not match rewards"); 127 | } 128 | 129 | function test_async_StakeBAYC() public { 130 | address testUser = testUsers[0]; 131 | uint256 depositCoinAmount = 1_000_000 * 1e18; 132 | uint256[] memory testBaycTokenIds = new uint256[](1); 133 | 134 | // deposit some coins 135 | vm.startPrank(testUser); 136 | mockWAPE.deposit{value: depositCoinAmount}(); 137 | mockWAPE.approve(address(coinPool), depositCoinAmount); 138 | coinPool.deposit(depositCoinAmount, testUser); 139 | vm.stopPrank(); 140 | 141 | // deposit some nfts 142 | vm.startPrank(testUser); 143 | mockBAYC.setApprovalForAll(address(nftPool), true); 144 | 145 | testBaycTokenIds[0] = 100; 146 | mockBAYC.mint(testBaycTokenIds[0]); 147 | 148 | address[] memory nfts = new address[](1); 149 | uint256[][] memory tokenIds = new uint256[][](1); 150 | nfts[0] = address(mockBAYC); 151 | tokenIds[0] = testBaycTokenIds; 152 | 153 | mockBAYC.safeTransferFrom(testUser, address(nftVault), testBaycTokenIds[0]); 154 | 155 | vm.stopPrank(); 156 | 157 | // configure apecoin staking to async mode 158 | mockBAYC.setLocked(testBaycTokenIds[0], true); 159 | mockBeacon.setFees(399572542890580499, 0); 160 | vm.deal(address(nftVault), 100 ether); 161 | 162 | // bot do some operations 163 | vm.startPrank(botAdmin); 164 | stakeManager.depositNft(nfts, tokenIds, testUser); 165 | 166 | stakeManager.stakeBayc(testBaycTokenIds); 167 | 168 | // make some rewards 169 | advanceTimeAndBlock(2 hours, 100); 170 | 171 | bytes32 guidClaim = mockBAYC.getNextGUID(); 172 | stakeManager.claimBayc(testBaycTokenIds); 173 | 174 | mockBAYC.executeCallback(address(mockApeStaking), guidClaim); 175 | 176 | // make some rewards 177 | advanceTimeAndBlock(2 hours, 100); 178 | 179 | bytes32 guidUnstake = mockBAYC.getNextGUID(); 180 | stakeManager.unstakeBayc(testBaycTokenIds); 181 | 182 | mockBAYC.executeCallback(address(mockApeStaking), guidUnstake); 183 | 184 | vm.stopPrank(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /test/hardhat/BendNftLockup.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Contracts, Env, makeSuite, Snapshots } from "./setup"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 4 | import { advanceHours, getContract, makeBN18, mintNft, shuffledSubarray } from "./utils"; 5 | import { BigNumber, constants } from "ethers"; 6 | import { arrayify } from "ethers/lib/utils"; 7 | 8 | makeSuite("BendNftLockup", (contracts: Contracts, env: Env, snapshots: Snapshots) => { 9 | let owner: SignerWithAddress; 10 | let bot: SignerWithAddress; 11 | let baycTokenIds: number[]; 12 | let maycTokenIds: number[]; 13 | let bakcTokenIds: number[]; 14 | let lastRevert: string; 15 | let zeroBytes32 = arrayify("0x0000000000000000000000000000000000000000000000000000000000000000"); 16 | 17 | before(async () => { 18 | owner = env.accounts[1]; 19 | bot = env.accounts[3]; 20 | 21 | baycTokenIds = [0, 1, 2, 3, 4, 5]; 22 | await mintNft(owner, contracts.bayc, baycTokenIds); 23 | await contracts.bayc.connect(owner).setApprovalForAll(contracts.bendNftLockup.address, true); 24 | 25 | maycTokenIds = [6, 7, 8, 9, 10]; 26 | await mintNft(owner, contracts.mayc, maycTokenIds); 27 | await contracts.mayc.connect(owner).setApprovalForAll(contracts.bendNftLockup.address, true); 28 | 29 | bakcTokenIds = [10, 11, 12, 13, 14]; 30 | await mintNft(owner, contracts.bakc, bakcTokenIds); 31 | await contracts.bakc.connect(owner).setApprovalForAll(contracts.bendNftLockup.address, true); 32 | 33 | await contracts.bendNftLockup.setBotAdmin(bot.address); 34 | 35 | lastRevert = "init"; 36 | await snapshots.capture(lastRevert); 37 | }); 38 | 39 | afterEach(async () => { 40 | if (lastRevert) { 41 | await snapshots.revert(lastRevert); 42 | } 43 | }); 44 | 45 | it("onlyOwner: reverts", async () => { 46 | await expect(contracts.bendNftLockup.connect(owner).setBotAdmin(constants.AddressZero)).revertedWith( 47 | "Ownable: caller is not the owner" 48 | ); 49 | await expect(contracts.bendNftLockup.connect(owner).setDelegationRegistryV2(constants.AddressZero)).revertedWith( 50 | "Ownable: caller is not the owner" 51 | ); 52 | await expect(contracts.bendNftLockup.connect(owner).setNftShadowRights(zeroBytes32)).revertedWith( 53 | "Ownable: caller is not the owner" 54 | ); 55 | await expect(contracts.bendNftLockup.connect(owner).setMaxOpInterval(100)).revertedWith( 56 | "Ownable: caller is not the owner" 57 | ); 58 | await expect(contracts.bendNftLockup.connect(owner).setPause(true)).revertedWith( 59 | "Ownable: caller is not the owner" 60 | ); 61 | }); 62 | 63 | it("onlyApe: reverts", async () => { 64 | await expect(contracts.bendNftLockup.deposit([contracts.wrapApeCoin.address], [baycTokenIds])).revertedWith( 65 | "BendNftLockup: not ape" 66 | ); 67 | 68 | await expect(contracts.bendNftLockup.withdraw([contracts.wrapApeCoin.address], [baycTokenIds])).revertedWith( 69 | "BendNftLockup: not ape" 70 | ); 71 | }); 72 | 73 | it("onlyBot: reverts", async () => { 74 | await expect( 75 | contracts.bendNftLockup.finalize([contracts.bayc.address], [baycTokenIds], owner.address) 76 | ).revertedWith("BendNftLockup: caller not bot admin"); 77 | }); 78 | 79 | it("deposit: revert when paused", async () => { 80 | await contracts.bendNftLockup.setPause(true); 81 | await expect(contracts.bendNftLockup.connect(owner).deposit([contracts.bayc.address], [baycTokenIds])).revertedWith( 82 | "Pausable: paused" 83 | ); 84 | await contracts.bendNftLockup.setPause(false); 85 | }); 86 | 87 | it("deposit: bayc", async () => { 88 | await contracts.bendNftLockup.connect(owner).deposit([contracts.bayc.address], [baycTokenIds]); 89 | 90 | for (const id of baycTokenIds) { 91 | expect(await contracts.bayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 92 | } 93 | 94 | lastRevert = "deposit:bayc"; 95 | await snapshots.capture(lastRevert); 96 | }); 97 | 98 | it("withdraw: revert when paused", async () => { 99 | await contracts.bendNftLockup.setPause(true); 100 | await expect( 101 | contracts.bendNftLockup.connect(owner).withdraw([contracts.bayc.address], [baycTokenIds]) 102 | ).revertedWith("Pausable: paused"); 103 | await contracts.bendNftLockup.setPause(false); 104 | }); 105 | 106 | it("withdraw: bayc and revert", async () => { 107 | await expect( 108 | contracts.bendNftLockup.connect(owner).withdraw([contracts.bayc.address], [baycTokenIds]) 109 | ).revertedWith("BendNftLockup: interval not enough"); 110 | }); 111 | 112 | it("withdraw: bayc", async () => { 113 | await advanceHours(12); 114 | 115 | await contracts.bendNftLockup.connect(owner).withdraw([contracts.bayc.address], [baycTokenIds]); 116 | 117 | for (const id of baycTokenIds) { 118 | expect(await contracts.bayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 119 | } 120 | 121 | lastRevert = "withdraw:bayc"; 122 | await snapshots.capture(lastRevert); 123 | }); 124 | 125 | it("finalize: bayc", async () => { 126 | await advanceHours(1); 127 | 128 | await contracts.bendNftLockup.connect(bot).finalize([contracts.bayc.address], [baycTokenIds], owner.address); 129 | 130 | for (const id of baycTokenIds) { 131 | expect(await contracts.bayc.ownerOf(id)).eq(owner.address); 132 | } 133 | 134 | lastRevert = "init"; 135 | }); 136 | 137 | it("deposit: bayc & mayc & bakc", async () => { 138 | await contracts.bendNftLockup 139 | .connect(owner) 140 | .deposit( 141 | [contracts.bayc.address, contracts.mayc.address, contracts.bakc.address], 142 | [baycTokenIds, maycTokenIds, bakcTokenIds] 143 | ); 144 | 145 | for (const id of baycTokenIds) { 146 | expect(await contracts.bayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 147 | } 148 | for (const id of maycTokenIds) { 149 | expect(await contracts.mayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 150 | } 151 | for (const id of bakcTokenIds) { 152 | expect(await contracts.bakc.ownerOf(id)).eq(contracts.bendNftLockup.address); 153 | } 154 | 155 | lastRevert = "deposit:bayc:mayc:bakc"; 156 | await snapshots.capture(lastRevert); 157 | }); 158 | 159 | it("withdraw: bayc & mayc & bakc", async () => { 160 | await advanceHours(12); 161 | 162 | await contracts.bendNftLockup 163 | .connect(owner) 164 | .withdraw( 165 | [contracts.bayc.address, contracts.mayc.address, contracts.bakc.address], 166 | [baycTokenIds, maycTokenIds, bakcTokenIds] 167 | ); 168 | 169 | for (const id of baycTokenIds) { 170 | expect(await contracts.bayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 171 | } 172 | for (const id of maycTokenIds) { 173 | expect(await contracts.mayc.ownerOf(id)).eq(contracts.bendNftLockup.address); 174 | } 175 | for (const id of bakcTokenIds) { 176 | expect(await contracts.bakc.ownerOf(id)).eq(contracts.bendNftLockup.address); 177 | } 178 | 179 | lastRevert = "withdraw:bayc:mayc:bakc"; 180 | await snapshots.capture(lastRevert); 181 | }); 182 | 183 | it("finalize: bayc & mayc & bakc", async () => { 184 | await advanceHours(1); 185 | 186 | await contracts.bendNftLockup 187 | .connect(bot) 188 | .finalize( 189 | [contracts.bayc.address, contracts.mayc.address, contracts.bakc.address], 190 | [baycTokenIds, maycTokenIds, bakcTokenIds], 191 | owner.address 192 | ); 193 | 194 | for (const id of baycTokenIds) { 195 | expect(await contracts.bayc.ownerOf(id)).eq(owner.address); 196 | } 197 | 198 | lastRevert = "init"; 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /abis/INftPool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "anonymous": false, 4 | "inputs": [ 5 | { 6 | "indexed": true, 7 | "internalType": "address", 8 | "name": "nft", 9 | "type": "address" 10 | }, 11 | { 12 | "indexed": false, 13 | "internalType": "uint256[]", 14 | "name": "tokenIds", 15 | "type": "uint256[]" 16 | }, 17 | { 18 | "indexed": true, 19 | "internalType": "address", 20 | "name": "owner", 21 | "type": "address" 22 | } 23 | ], 24 | "name": "NftDeposited", 25 | "type": "event" 26 | }, 27 | { 28 | "anonymous": false, 29 | "inputs": [ 30 | { 31 | "indexed": true, 32 | "internalType": "address", 33 | "name": "nft", 34 | "type": "address" 35 | }, 36 | { 37 | "indexed": false, 38 | "internalType": "uint256[]", 39 | "name": "tokenIds", 40 | "type": "uint256[]" 41 | }, 42 | { 43 | "indexed": true, 44 | "internalType": "address", 45 | "name": "receiver", 46 | "type": "address" 47 | }, 48 | { 49 | "indexed": false, 50 | "internalType": "uint256", 51 | "name": "amount", 52 | "type": "uint256" 53 | }, 54 | { 55 | "indexed": false, 56 | "internalType": "uint256", 57 | "name": "rewardsDebt", 58 | "type": "uint256" 59 | } 60 | ], 61 | "name": "NftRewardClaimed", 62 | "type": "event" 63 | }, 64 | { 65 | "anonymous": false, 66 | "inputs": [ 67 | { 68 | "indexed": true, 69 | "internalType": "address", 70 | "name": "nft", 71 | "type": "address" 72 | }, 73 | { 74 | "indexed": false, 75 | "internalType": "uint256", 76 | "name": "rewardAmount", 77 | "type": "uint256" 78 | } 79 | ], 80 | "name": "NftRewardDistributed", 81 | "type": "event" 82 | }, 83 | { 84 | "anonymous": false, 85 | "inputs": [ 86 | { 87 | "indexed": true, 88 | "internalType": "address", 89 | "name": "nft", 90 | "type": "address" 91 | }, 92 | { 93 | "indexed": false, 94 | "internalType": "uint256[]", 95 | "name": "tokenIds", 96 | "type": "uint256[]" 97 | }, 98 | { 99 | "indexed": true, 100 | "internalType": "address", 101 | "name": "owner", 102 | "type": "address" 103 | } 104 | ], 105 | "name": "NftWithdrawn", 106 | "type": "event" 107 | }, 108 | { 109 | "inputs": [ 110 | { 111 | "internalType": "address[]", 112 | "name": "nfts_", 113 | "type": "address[]" 114 | }, 115 | { 116 | "internalType": "uint256[][]", 117 | "name": "tokenIds_", 118 | "type": "uint256[][]" 119 | } 120 | ], 121 | "name": "claim", 122 | "outputs": [], 123 | "stateMutability": "nonpayable", 124 | "type": "function" 125 | }, 126 | { 127 | "inputs": [ 128 | { 129 | "internalType": "address[]", 130 | "name": "nfts_", 131 | "type": "address[]" 132 | }, 133 | { 134 | "internalType": "uint256[][]", 135 | "name": "tokenIds_", 136 | "type": "uint256[][]" 137 | } 138 | ], 139 | "name": "claimable", 140 | "outputs": [ 141 | { 142 | "internalType": "uint256", 143 | "name": "", 144 | "type": "uint256" 145 | } 146 | ], 147 | "stateMutability": "view", 148 | "type": "function" 149 | }, 150 | { 151 | "inputs": [ 152 | { 153 | "internalType": "address", 154 | "name": "nft_", 155 | "type": "address" 156 | } 157 | ], 158 | "name": "compoundApeCoin", 159 | "outputs": [], 160 | "stateMutability": "nonpayable", 161 | "type": "function" 162 | }, 163 | { 164 | "inputs": [ 165 | { 166 | "internalType": "address[]", 167 | "name": "nfts_", 168 | "type": "address[]" 169 | }, 170 | { 171 | "internalType": "uint256[][]", 172 | "name": "tokenIds_", 173 | "type": "uint256[][]" 174 | }, 175 | { 176 | "internalType": "address", 177 | "name": "owner_", 178 | "type": "address" 179 | } 180 | ], 181 | "name": "deposit", 182 | "outputs": [], 183 | "stateMutability": "nonpayable", 184 | "type": "function" 185 | }, 186 | { 187 | "inputs": [ 188 | { 189 | "internalType": "address", 190 | "name": "nft_", 191 | "type": "address" 192 | }, 193 | { 194 | "internalType": "uint256", 195 | "name": "tokenId", 196 | "type": "uint256" 197 | } 198 | ], 199 | "name": "getNftStateUI", 200 | "outputs": [ 201 | { 202 | "internalType": "uint256", 203 | "name": "rewardsDebt", 204 | "type": "uint256" 205 | } 206 | ], 207 | "stateMutability": "view", 208 | "type": "function" 209 | }, 210 | { 211 | "inputs": [ 212 | { 213 | "internalType": "address", 214 | "name": "nft_", 215 | "type": "address" 216 | } 217 | ], 218 | "name": "getPoolStateUI", 219 | "outputs": [ 220 | { 221 | "components": [ 222 | { 223 | "internalType": "uint256", 224 | "name": "totalStakedNft", 225 | "type": "uint256" 226 | }, 227 | { 228 | "internalType": "uint256", 229 | "name": "accumulatedRewardsPerNft", 230 | "type": "uint256" 231 | }, 232 | { 233 | "internalType": "uint256", 234 | "name": "pendingApeCoin", 235 | "type": "uint256" 236 | } 237 | ], 238 | "internalType": "struct INftPool.PoolUI", 239 | "name": "", 240 | "type": "tuple" 241 | } 242 | ], 243 | "stateMutability": "view", 244 | "type": "function" 245 | }, 246 | { 247 | "inputs": [ 248 | { 249 | "internalType": "address", 250 | "name": "nft_", 251 | "type": "address" 252 | } 253 | ], 254 | "name": "pendingApeCoin", 255 | "outputs": [ 256 | { 257 | "internalType": "uint256", 258 | "name": "", 259 | "type": "uint256" 260 | } 261 | ], 262 | "stateMutability": "view", 263 | "type": "function" 264 | }, 265 | { 266 | "inputs": [ 267 | { 268 | "internalType": "address", 269 | "name": "nft_", 270 | "type": "address" 271 | }, 272 | { 273 | "internalType": "uint256", 274 | "name": "rewardsAmount_", 275 | "type": "uint256" 276 | } 277 | ], 278 | "name": "receiveApeCoin", 279 | "outputs": [], 280 | "stateMutability": "nonpayable", 281 | "type": "function" 282 | }, 283 | { 284 | "inputs": [], 285 | "name": "staker", 286 | "outputs": [ 287 | { 288 | "internalType": "contract IStakeManager", 289 | "name": "", 290 | "type": "address" 291 | } 292 | ], 293 | "stateMutability": "view", 294 | "type": "function" 295 | }, 296 | { 297 | "inputs": [ 298 | { 299 | "internalType": "address[]", 300 | "name": "nfts_", 301 | "type": "address[]" 302 | }, 303 | { 304 | "internalType": "uint256[][]", 305 | "name": "tokenIds_", 306 | "type": "uint256[][]" 307 | }, 308 | { 309 | "internalType": "address", 310 | "name": "owner_", 311 | "type": "address" 312 | } 313 | ], 314 | "name": "withdraw", 315 | "outputs": [], 316 | "stateMutability": "nonpayable", 317 | "type": "function" 318 | } 319 | ] 320 | -------------------------------------------------------------------------------- /contracts/interfaces/IDelegationRegistry.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC0-1.0 2 | pragma solidity 0.8.18; 3 | 4 | /** 5 | * @title An immutable registry contract to be deployed as a standalone primitive 6 | * @dev See EIP-5639, new project launches can read previous cold wallet -> hot wallet delegations 7 | * from here and integrate those permissions into their flow 8 | */ 9 | interface IDelegationRegistry { 10 | /// @notice Delegation type 11 | enum DelegationType { 12 | NONE, 13 | ALL, 14 | CONTRACT, 15 | TOKEN 16 | } 17 | 18 | /// @notice Info about a single delegation, used for onchain enumeration 19 | struct DelegationInfo { 20 | DelegationType type_; 21 | address vault; 22 | address delegate; 23 | address contract_; 24 | uint256 tokenId; 25 | } 26 | 27 | /// @notice Info about a single contract-level delegation 28 | struct ContractDelegation { 29 | address contract_; 30 | address delegate; 31 | } 32 | 33 | /// @notice Info about a single token-level delegation 34 | struct TokenDelegation { 35 | address contract_; 36 | uint256 tokenId; 37 | address delegate; 38 | } 39 | 40 | /// @notice Emitted when a user delegates their entire wallet 41 | event DelegateForAll(address vault, address delegate, bool value); 42 | 43 | /// @notice Emitted when a user delegates a specific contract 44 | event DelegateForContract(address vault, address delegate, address contract_, bool value); 45 | 46 | /// @notice Emitted when a user delegates a specific token 47 | event DelegateForToken(address vault, address delegate, address contract_, uint256 tokenId, bool value); 48 | 49 | /// @notice Emitted when a user revokes all delegations 50 | event RevokeAllDelegates(address vault); 51 | 52 | /// @notice Emitted when a user revoes all delegations for a given delegate 53 | event RevokeDelegate(address vault, address delegate); 54 | 55 | /** 56 | * ----------- WRITE ----------- 57 | */ 58 | 59 | /** 60 | * @notice Allow the delegate to act on your behalf for all contracts 61 | * @param delegate The hotwallet to act on your behalf 62 | * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking 63 | */ 64 | function delegateForAll(address delegate, bool value) external; 65 | 66 | /** 67 | * @notice Allow the delegate to act on your behalf for a specific contract 68 | * @param delegate The hotwallet to act on your behalf 69 | * @param contract_ The address for the contract you're delegating 70 | * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking 71 | */ 72 | function delegateForContract(address delegate, address contract_, bool value) external; 73 | 74 | /** 75 | * @notice Allow the delegate to act on your behalf for a specific token 76 | * @param delegate The hotwallet to act on your behalf 77 | * @param contract_ The address for the contract you're delegating 78 | * @param tokenId The token id for the token you're delegating 79 | * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking 80 | */ 81 | function delegateForToken(address delegate, address contract_, uint256 tokenId, bool value) external; 82 | 83 | /** 84 | * @notice Revoke all delegates 85 | */ 86 | function revokeAllDelegates() external; 87 | 88 | /** 89 | * @notice Revoke a specific delegate for all their permissions 90 | * @param delegate The hotwallet to revoke 91 | */ 92 | function revokeDelegate(address delegate) external; 93 | 94 | /** 95 | * @notice Remove yourself as a delegate for a specific vault 96 | * @param vault The vault which delegated to the msg.sender, and should be removed 97 | */ 98 | function revokeSelf(address vault) external; 99 | 100 | /** 101 | * ----------- READ ----------- 102 | */ 103 | 104 | /** 105 | * @notice Returns all active delegations a given delegate is able to claim on behalf of 106 | * @param delegate The delegate that you would like to retrieve delegations for 107 | * @return info Array of DelegationInfo structs 108 | */ 109 | function getDelegationsByDelegate(address delegate) external view returns (DelegationInfo[] memory); 110 | 111 | /** 112 | * @notice Returns an array of wallet-level delegates for a given vault 113 | * @param vault The cold wallet who issued the delegation 114 | * @return addresses Array of wallet-level delegates for a given vault 115 | */ 116 | function getDelegatesForAll(address vault) external view returns (address[] memory); 117 | 118 | /** 119 | * @notice Returns an array of contract-level delegates for a given vault and contract 120 | * @param vault The cold wallet who issued the delegation 121 | * @param contract_ The address for the contract you're delegating 122 | * @return addresses Array of contract-level delegates for a given vault and contract 123 | */ 124 | function getDelegatesForContract(address vault, address contract_) external view returns (address[] memory); 125 | 126 | /** 127 | * @notice Returns an array of contract-level delegates for a given vault's token 128 | * @param vault The cold wallet who issued the delegation 129 | * @param contract_ The address for the contract holding the token 130 | * @param tokenId The token id for the token you're delegating 131 | * @return addresses Array of contract-level delegates for a given vault's token 132 | */ 133 | function getDelegatesForToken( 134 | address vault, 135 | address contract_, 136 | uint256 tokenId 137 | ) external view returns (address[] memory); 138 | 139 | /** 140 | * @notice Returns all contract-level delegations for a given vault 141 | * @param vault The cold wallet who issued the delegations 142 | * @return delegations Array of ContractDelegation structs 143 | */ 144 | function getContractLevelDelegations(address vault) external view returns (ContractDelegation[] memory delegations); 145 | 146 | /** 147 | * @notice Returns all token-level delegations for a given vault 148 | * @param vault The cold wallet who issued the delegations 149 | * @return delegations Array of TokenDelegation structs 150 | */ 151 | function getTokenLevelDelegations(address vault) external view returns (TokenDelegation[] memory delegations); 152 | 153 | /** 154 | * @notice Returns true if the address is delegated to act on the entire vault 155 | * @param delegate The hotwallet to act on your behalf 156 | * @param vault The cold wallet who issued the delegation 157 | */ 158 | function checkDelegateForAll(address delegate, address vault) external view returns (bool); 159 | 160 | /** 161 | * @notice Returns true if the address is delegated to act on your behalf for a token contract or an entire vault 162 | * @param delegate The hotwallet to act on your behalf 163 | * @param contract_ The address for the contract you're delegating 164 | * @param vault The cold wallet who issued the delegation 165 | */ 166 | function checkDelegateForContract(address delegate, address vault, address contract_) external view returns (bool); 167 | 168 | /** 169 | * @notice Returns true if the address is delegated to act on your behalf for a specific token, the token's contract or an entire vault 170 | * @param delegate The hotwallet to act on your behalf 171 | * @param contract_ The address for the contract you're delegating 172 | * @param tokenId The token id for the token you're delegating 173 | * @param vault The cold wallet who issued the delegation 174 | */ 175 | function checkDelegateForToken( 176 | address delegate, 177 | address vault, 178 | address contract_, 179 | uint256 tokenId 180 | ) external view returns (bool); 181 | } 182 | -------------------------------------------------------------------------------- /contracts/strategy/DefaultWithdrawStrategy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BUSL-1.1 2 | pragma solidity 0.8.18; 3 | 4 | import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 5 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; 7 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 8 | 9 | import {IApeCoinStaking} from "../interfaces/IApeCoinStaking.sol"; 10 | import {INftVault} from "../interfaces/INftVault.sol"; 11 | import {ICoinPool} from "../interfaces/ICoinPool.sol"; 12 | import {INftPool} from "../interfaces/INftPool.sol"; 13 | import {IStakeManager} from "../interfaces/IStakeManager.sol"; 14 | import {IWithdrawStrategy} from "../interfaces/IWithdrawStrategy.sol"; 15 | 16 | import {ApeStakingLib} from "../libraries/ApeStakingLib.sol"; 17 | 18 | contract DefaultWithdrawStrategy is IWithdrawStrategy, ReentrancyGuard, Ownable { 19 | using ApeStakingLib for IApeCoinStaking; 20 | using SafeCast for uint256; 21 | using SafeCast for uint248; 22 | 23 | IApeCoinStaking public apeCoinStaking; 24 | IERC20 public wrapApeCoin; 25 | IStakeManager public staker; 26 | ICoinPool public coinPool; 27 | INftVault public nftVault; 28 | 29 | address public bayc; 30 | address public mayc; 31 | address public bakc; 32 | 33 | modifier onlyStaker() { 34 | require(msg.sender == address(staker), "DWS: caller is not staker"); 35 | _; 36 | } 37 | 38 | constructor( 39 | IApeCoinStaking apeCoinStaking_, 40 | INftVault nftVault_, 41 | ICoinPool coinPool_, 42 | IStakeManager staker_ 43 | ) Ownable() { 44 | apeCoinStaking = apeCoinStaking_; 45 | nftVault = nftVault_; 46 | coinPool = coinPool_; 47 | staker = staker_; 48 | 49 | wrapApeCoin = IERC20(coinPool.getWrapApeCoin()); 50 | bayc = address(apeCoinStaking.bayc()); 51 | mayc = address(apeCoinStaking.mayc()); 52 | bakc = address(apeCoinStaking.bakc()); 53 | } 54 | 55 | function setApeCoinStaking(address apeCoinStaking_) public onlyOwner { 56 | apeCoinStaking = IApeCoinStaking(apeCoinStaking_); 57 | 58 | bayc = address(apeCoinStaking.bayc()); 59 | mayc = address(apeCoinStaking.mayc()); 60 | bakc = address(apeCoinStaking.bakc()); 61 | } 62 | 63 | function initGlobalState() public onlyStaker { 64 | wrapApeCoin = IERC20(coinPool.getWrapApeCoin()); 65 | } 66 | 67 | struct WithdrawApeCoinVars { 68 | uint256 tokenId; 69 | uint256 stakedApeCoin; 70 | uint256 pendingRewards; 71 | uint256 unstakeNftSize; 72 | uint256 totalWithdrawn; 73 | } 74 | 75 | function withdrawApeCoin(uint256 required) external override onlyStaker returns (uint256 withdrawn) { 76 | require(address(wrapApeCoin) != address(0), "DWS: wrapApeCoin not set"); 77 | 78 | WithdrawApeCoinVars memory vars; 79 | 80 | // 1. withdraw refund 81 | 82 | // 2. claim ape coin pool 83 | 84 | // 3. unstake ape coin pool 85 | 86 | // 4. unstake bayc 87 | if (vars.totalWithdrawn < required) { 88 | vars.stakedApeCoin = staker.stakedApeCoin(ApeStakingLib.BAYC_POOL_ID); 89 | if (vars.stakedApeCoin > 0) { 90 | vars.tokenId = 0; 91 | vars.unstakeNftSize = 0; 92 | vars.stakedApeCoin = 0; 93 | vars.pendingRewards = 0; 94 | for (uint256 i = 0; i < nftVault.totalStakingNft(bayc, address(staker)); i++) { 95 | vars.tokenId = nftVault.stakingNftIdByIndex(bayc, address(staker), i); 96 | vars.stakedApeCoin = apeCoinStaking 97 | .nftPosition(ApeStakingLib.BAYC_POOL_ID, vars.tokenId) 98 | .stakedAmount; 99 | 100 | vars.pendingRewards = apeCoinStaking.pendingRewards(ApeStakingLib.BAYC_POOL_ID, vars.tokenId); 101 | vars.pendingRewards -= staker.calculateFee(vars.pendingRewards); 102 | 103 | vars.totalWithdrawn += vars.stakedApeCoin; 104 | vars.totalWithdrawn += vars.pendingRewards; 105 | vars.unstakeNftSize += 1; 106 | 107 | if (vars.totalWithdrawn >= required) { 108 | break; 109 | } 110 | } 111 | if (vars.unstakeNftSize > 0) { 112 | uint256[] memory tokenIds = new uint256[](vars.unstakeNftSize); 113 | for (uint256 i = 0; i < vars.unstakeNftSize; i++) { 114 | tokenIds[i] = nftVault.stakingNftIdByIndex(bayc, address(staker), i); 115 | } 116 | staker.unstakeBayc(tokenIds); 117 | } 118 | } 119 | } 120 | 121 | // 5. unstake mayc 122 | if (vars.totalWithdrawn < required) { 123 | vars.stakedApeCoin = staker.stakedApeCoin(ApeStakingLib.MAYC_POOL_ID); 124 | if (vars.stakedApeCoin > 0) { 125 | vars.tokenId = 0; 126 | vars.unstakeNftSize = 0; 127 | vars.stakedApeCoin = 0; 128 | vars.pendingRewards = 0; 129 | for (uint256 i = 0; i < nftVault.totalStakingNft(mayc, address(staker)); i++) { 130 | vars.tokenId = nftVault.stakingNftIdByIndex(mayc, address(staker), i); 131 | vars.stakedApeCoin = apeCoinStaking 132 | .nftPosition(ApeStakingLib.MAYC_POOL_ID, vars.tokenId) 133 | .stakedAmount; 134 | 135 | vars.pendingRewards = apeCoinStaking.pendingRewards(ApeStakingLib.MAYC_POOL_ID, vars.tokenId); 136 | vars.pendingRewards -= staker.calculateFee(vars.pendingRewards); 137 | 138 | vars.totalWithdrawn += vars.stakedApeCoin; 139 | vars.totalWithdrawn += vars.pendingRewards; 140 | 141 | vars.unstakeNftSize += 1; 142 | 143 | if (vars.totalWithdrawn >= required) { 144 | break; 145 | } 146 | } 147 | if (vars.unstakeNftSize > 0) { 148 | uint256[] memory tokenIds = new uint256[](vars.unstakeNftSize); 149 | for (uint256 i = 0; i < vars.unstakeNftSize; i++) { 150 | tokenIds[i] = nftVault.stakingNftIdByIndex(mayc, address(staker), i); 151 | } 152 | staker.unstakeMayc(tokenIds); 153 | } 154 | } 155 | } 156 | 157 | // 6. unstake bakc 158 | if (vars.totalWithdrawn < required) { 159 | vars.stakedApeCoin = staker.stakedApeCoin(ApeStakingLib.BAKC_POOL_ID); 160 | if (vars.stakedApeCoin > 0) { 161 | vars.tokenId = 0; 162 | vars.unstakeNftSize = 0; 163 | vars.stakedApeCoin = 0; 164 | vars.pendingRewards = 0; 165 | for (uint256 i = 0; i < nftVault.totalStakingNft(bakc, address(staker)); i++) { 166 | vars.tokenId = nftVault.stakingNftIdByIndex(bakc, address(staker), i); 167 | vars.stakedApeCoin = apeCoinStaking 168 | .nftPosition(ApeStakingLib.BAKC_POOL_ID, vars.tokenId) 169 | .stakedAmount; 170 | 171 | vars.pendingRewards = apeCoinStaking.pendingRewards(ApeStakingLib.BAKC_POOL_ID, vars.tokenId); 172 | vars.pendingRewards -= staker.calculateFee(vars.pendingRewards); 173 | 174 | vars.totalWithdrawn += vars.stakedApeCoin; 175 | vars.totalWithdrawn += vars.pendingRewards; 176 | vars.unstakeNftSize += 1; 177 | 178 | if (vars.totalWithdrawn >= required) { 179 | break; 180 | } 181 | } 182 | if (vars.unstakeNftSize > 0) { 183 | uint256[] memory tokenIds = new uint256[](vars.unstakeNftSize); 184 | for (uint256 i = 0; i < vars.unstakeNftSize; i++) { 185 | tokenIds[i] = nftVault.stakingNftIdByIndex(bakc, address(staker), i); 186 | } 187 | staker.unstakeBakc(tokenIds); 188 | } 189 | } 190 | } 191 | 192 | // Caution: unstake nfts are asynchronous, the balance in our pool maybe not changed 193 | withdrawn = vars.totalWithdrawn; 194 | } 195 | } 196 | --------------------------------------------------------------------------------