├── .husky ├── .gitignore └── commit-msg ├── .nvmrc ├── .gitattributes ├── .czrc ├── tasks ├── task-names.ts ├── accounts.ts └── clean.ts ├── .commitlintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .huskyrc ├── .solhintignore ├── audits ├── 2022-04-macro.pdf └── 2021-08-least-authority.pdf ├── .env.example ├── .mocharc.json ├── .prettierignore ├── types ├── index.ts └── augmentations.d.ts ├── .eslintignore ├── contracts ├── test │ ├── MockOpenVault.sol │ ├── Templates.sol │ ├── MockCallDelegator.sol │ ├── UserProxy.sol │ ├── MockERC20.sol │ ├── ERC721ReceiverMock.sol │ ├── MockLendingPool.sol │ ├── MockERC721.sol │ ├── MockERC1155.sol │ ├── WrappedPunks.sol │ ├── MockLoanCore.sol │ └── CryptoPunks.sol ├── interfaces │ ├── IFeeController.sol │ ├── IPromissoryNote.sol │ ├── ICallDelegator.sol │ ├── IPunks.sol │ ├── IRepaymentController.sol │ ├── IWrappedPunks.sol │ ├── IVaultFactory.sol │ ├── ICallWhitelist.sol │ ├── IOriginationController.sol │ ├── IERC721Permit.sol │ ├── ILoanCore.sol │ ├── IFlashRollover.sol │ └── IAssetVault.sol ├── vault │ ├── OwnableERC721.sol │ ├── CallWhitelist.sol │ ├── VaultFactory.sol │ └── AssetVault.sol ├── FeeController.sol ├── libraries │ └── LoanLibrary.sol ├── PunkRouter.sol ├── RepaymentController.sol ├── ERC721Permit.sol ├── OriginationController.sol ├── PromissoryNote.sol └── LoanCore.sol ├── scripts ├── constants.ts ├── bootstrap-state-no-loans.ts ├── deploy-with-punk-router.ts ├── bootstrap-state-with-loans.ts ├── deploy-flash-rollover.ts ├── transfer-ownership.ts ├── deploy.ts ├── redeploy-test-transfer.ts └── redeploy-loancore.ts ├── .editorconfig ├── .prettierrc ├── .gitignore ├── test ├── utils │ ├── time.ts │ ├── types.ts │ ├── contracts.ts │ ├── erc1155.ts │ ├── erc721.ts │ ├── erc20.ts │ └── eip712.ts ├── FeeController.ts ├── PunkRouter.ts └── RepaymentController.ts ├── .eslintrc.yaml ├── .solhint.json ├── tsconfig.json ├── .solcover.js ├── docs ├── FeeController.md ├── PunkRouter.md ├── RepaymentController.md ├── ERC721Permit.md ├── OriginationController.md ├── PromissoryNote.md ├── AssetWrapper.md ├── LoanCore.md └── FlashRollover.md ├── LICENSE.md ├── package.json ├── hardhat.config.ts └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v12.22.1 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://gitcoin.co/grants/1657/paulrberg-open-source-engineering"] 2 | -------------------------------------------------------------------------------- /.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/2022-04-macro.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/pawnfi-contracts/HEAD/audits/2022-04-macro.pdf -------------------------------------------------------------------------------- /audits/2021-08-least-authority.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arcadexyz/pawnfi-contracts/HEAD/audits/2021-08-least-authority.pdf -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INFURA_API_KEY=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 2 | MNEMONIC=here is where your twelve words mnemonic should be put my friend 3 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "recursive": "test", 4 | "require": ["hardhat/register"], 5 | "timeout": 20000 6 | } 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # folders 2 | artifacts/ 3 | build/ 4 | cache/ 5 | coverage/ 6 | dist/ 7 | lib/ 8 | node_modules/ 9 | typechain/ 10 | 11 | # files 12 | coverage.json 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /contracts/test/MockOpenVault.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract MockOpenVault { 4 | function withdrawEnabled() external pure returns (bool) { 5 | return false; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scripts/constants.ts: -------------------------------------------------------------------------------- 1 | export const ORIGINATOR_ROLE = "0x59abfac6520ec36a6556b2a4dd949cc40007459bcd5cd2507f1e5cc77b6bc97e"; 2 | export const REPAYER_ROLE = "0x9c60024347074fd9de2c1e36003080d22dbc76a41ef87444d21e361bcb39118e"; 3 | -------------------------------------------------------------------------------- /contracts/test/Templates.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 4 | import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol"; 5 | import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol"; 6 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/IFeeController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | interface IFeeController { 4 | /** 5 | * @dev Emitted when origination fee is updated 6 | */ 7 | event UpdateOriginationFee(uint256 _newFee); 8 | 9 | function setOriginationFee(uint256 _originationFee) external; 10 | 11 | function getOriginationFee() external view returns (uint256); 12 | } 13 | -------------------------------------------------------------------------------- /.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 | # files 15 | *.env 16 | *.log 17 | *.tsbuildinfo 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .DS_Store 23 | .vscode 24 | 25 | gas_report.log 26 | .env 27 | cargs.js -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/interfaces/IPromissoryNote.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; 4 | 5 | interface IPromissoryNote is IERC721Enumerable { 6 | // Getter for mapping: mapping(uint256 => uint256) public loanIdByNoteId; 7 | function loanIdByNoteId(uint256 noteId) external view returns (uint256); 8 | 9 | function mint(address to, uint256 loanId) external returns (uint256); 10 | 11 | function burn(uint256 tokenId) external; 12 | } 13 | -------------------------------------------------------------------------------- /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 | pragma solidity ^0.8.0; 3 | 4 | import "../interfaces/ICallDelegator.sol"; 5 | 6 | contract MockCallDelegator is ICallDelegator { 7 | bool private canCall; 8 | 9 | /** 10 | * @inheritdoc ICallDelegator 11 | */ 12 | function canCallOn(address caller, address vault) external view override returns (bool) { 13 | require(caller != vault, "Invalid vault"); 14 | return canCall; 15 | } 16 | 17 | function setCanCall(bool _canCall) external { 18 | canCall = _canCall; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "ethers"; 2 | 3 | export enum LoanState { 4 | DUMMY = 0, 5 | Created = 1, 6 | Active = 2, 7 | Repaid = 3, 8 | Defaulted = 4, 9 | } 10 | 11 | export interface LoanTerms { 12 | durationSecs: BigNumberish; 13 | principal: BigNumber; 14 | interest: BigNumber; 15 | collateralTokenId: BigNumber; 16 | payableCurrency: string; 17 | } 18 | 19 | export interface LoanData { 20 | terms: LoanTerms; 21 | borrowerNoteId: BigNumber; 22 | lenderNoteId: BigNumber; 23 | state: LoanState; 24 | dueDate: BigNumberish; 25 | } 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/ICallDelegator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Interface for a vault owner to delegate call ability to another entity 7 | * Useful in the case where a vault is being used as collateral for a loan 8 | * and the borrower wants to claim an airdrop 9 | */ 10 | interface ICallDelegator { 11 | /** 12 | * @dev Return true if the caller is allowed to call functions on the given vault 13 | * @param caller The user that wants to call a function 14 | * @param vault The vault that the caller wants to call a function on 15 | * @return true if allowed, else false 16 | */ 17 | function canCallOn(address caller, address vault) external view returns (bool); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/test/UserProxy.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | contract UserProxy { 4 | address private _owner; 5 | 6 | /** 7 | * @dev Initializes the contract settings 8 | */ 9 | constructor() { 10 | _owner = msg.sender; 11 | } 12 | 13 | /** 14 | * @dev Transfers punk to the smart contract owner 15 | */ 16 | function transfer(address punkContract, uint256 punkIndex) external returns (bool) { 17 | if (_owner != msg.sender) { 18 | return false; 19 | } 20 | 21 | // solhint-disable-next-line avoid-low-level-calls 22 | (bool result, ) = punkContract.call( 23 | abi.encodeWithSignature("transferPunk(address,uint256)", _owner, punkIndex) 24 | ); 25 | 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /contracts/interfaces/IPunks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Interface for a permittable ERC721 contract 7 | * See https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. 8 | * 9 | * Adds the {permit} method, which can be used to change an account's ERC72 allowance (see {IERC721-allowance}) by 10 | * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't 11 | * need to send a transaction, and thus is not required to hold Ether at all. 12 | */ 13 | interface IPunks { 14 | function punkIndexToAddress(uint256 punkIndex) external view returns (address owner); 15 | 16 | function buyPunk(uint256 punkIndex) external; 17 | 18 | function transferPunk(address to, uint256 punkIndex) external; 19 | } 20 | -------------------------------------------------------------------------------- /contracts/interfaces/IRepaymentController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | interface IRepaymentController { 6 | /** 7 | * @dev used to repay a currently active loan. 8 | * 9 | * The loan must be in the Active state, and the 10 | * payableCurrency must be approved for withdrawal by the 11 | * repayment controller. This call will withdraw tokens 12 | * from the caller's wallet. 13 | * 14 | */ 15 | function repay(uint256 borrowerNoteId) external; 16 | 17 | /** 18 | * @dev used to repay a currently active loan that is past due. 19 | * 20 | * The loan must be in the Active state, and the caller must 21 | * be the holder of the lender note. 22 | */ 23 | function claim(uint256 lenderNoteId) external; 24 | } 25 | -------------------------------------------------------------------------------- /.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 | mocha: { 24 | grep: "@skip-on-coverage", 25 | invert: true, 26 | }, 27 | configureYulOptimizer: true, 28 | }; 29 | -------------------------------------------------------------------------------- /contracts/vault/OwnableERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 5 | 6 | /* @title OwnableERC721 7 | * @notice Use ERC721 ownership for access control 8 | * Requires tokenId scheme must map to map uint256(contract address) 9 | */ 10 | abstract contract OwnableERC721 { 11 | address public ownershipToken; 12 | 13 | modifier onlyOwner() { 14 | require(owner() == msg.sender, "OwnableERC721: caller is not the owner"); 15 | _; 16 | } 17 | 18 | function _setNFT(address _ownershipToken) internal { 19 | ownershipToken = _ownershipToken; 20 | } 21 | 22 | function owner() public view virtual returns (address ownerAddress) { 23 | return IERC721(ownershipToken).ownerOf(uint256(uint160(address(this)))); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/FeeController.md: -------------------------------------------------------------------------------- 1 | # `FeeController` 2 | 3 | The FeeController is intended to be an upgradable component of the Pawn 4 | protocol where new fees can be added or modified based on different 5 | platform needs. 6 | 7 | Fees may be assessed based on the following attributes: 8 | 9 | - Type/size of loan being requested 10 | - Amount of tokens being staked, etc 11 | - Due dates/penalty payments 12 | 13 | ## API 14 | 15 | ### `setOriginationFee(uint256 _originationFee)` _(external)_ 16 | 17 | Set the origination fee to the given value. Can only be called by contract owner. 18 | 19 | Emits `UpdateOriginationFee`. 20 | 21 | ### `getOriginationFee() → uint256` _(public)_ 22 | 23 | Get the current origination fee in bps. 24 | 25 | ## Events 26 | 27 | ### `UpdateOriginationFee(uint256 _newFee)` 28 | 29 | Emitted when the origination fee is changed. All fees are expressed in bps. 30 | -------------------------------------------------------------------------------- /contracts/interfaces/IWrappedPunks.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | /** 8 | * @dev Interface for a permittable ERC721 contract 9 | * See https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. 10 | * 11 | * Adds the {permit} method, which can be used to change an account's ERC72 allowance (see {IERC721-allowance}) by 12 | * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't 13 | * need to send a transaction, and thus is not required to hold Ether at all. 14 | */ 15 | interface IWrappedPunks is IERC721 { 16 | function mint(uint256 punkIndex) external; 17 | 18 | function burn(uint256 punkIndex) external; 19 | 20 | function registerProxy() external; 21 | 22 | function proxyInfo(address user) external returns (address proxy); 23 | } 24 | -------------------------------------------------------------------------------- /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 | 13 | const tx = await token.mint(address, amount); 14 | const receipt = await tx.wait(); 15 | 16 | if (receipt && receipt.events && receipt.events.length === 1 && receipt.events[0].args) { 17 | return receipt.events[0].args.id; 18 | } else { 19 | throw new Error("Unable to initialize bundle"); 20 | } 21 | }; 22 | 23 | /** 24 | * approve `amount` tokens for `to` from `from` 25 | */ 26 | export const approve = async (token: MockERC1155, sender: Signer, toAddress: string): Promise => { 27 | const senderAddress = await sender.getAddress(); 28 | 29 | await expect(token.connect(sender).setApprovalForAll(toAddress, true)) 30 | .to.emit(token, "ApprovalForAll") 31 | .withArgs(senderAddress, toAddress, true); 32 | }; 33 | -------------------------------------------------------------------------------- /contracts/test/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/utils/Context.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol"; 6 | 7 | contract MockERC20 is Context, ERC20Burnable { 8 | /** 9 | * @dev Initializes ERC20 token 10 | */ 11 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {} 12 | 13 | /** 14 | * @dev Creates `amount` new tokens for `to`. Public for any test to call. 15 | * 16 | * See {ERC20-_mint}. 17 | */ 18 | function mint(address to, uint256 amount) public virtual { 19 | _mint(to, amount); 20 | } 21 | } 22 | 23 | contract MockERC20WithDecimals is ERC20PresetMinterPauser { 24 | uint8 private _decimals; 25 | 26 | /** 27 | * @dev Initializes ERC20 token 28 | */ 29 | constructor( 30 | string memory name, 31 | string memory symbol, 32 | uint8 decimals 33 | ) ERC20PresetMinterPauser(name, symbol) { 34 | _decimals = decimals; 35 | } 36 | 37 | function decimals() public view virtual override returns (uint8) { 38 | return _decimals; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contracts/test/ERC721ReceiverMock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 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 | -------------------------------------------------------------------------------- /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 tx = await token.mint(await to.getAddress()); 12 | const receipt = await tx.wait(); 13 | 14 | if (receipt && receipt.events && receipt.events.length === 1 && receipt.events[0].args) { 15 | return receipt.events[0].args.tokenId; 16 | } else { 17 | throw new Error("Unable to initialize bundle"); 18 | } 19 | }; 20 | 21 | /** 22 | * approve `amount` tokens for `to` from `from` 23 | */ 24 | export const approve = async ( 25 | token: MockERC721, 26 | sender: Signer, 27 | toAddress: string, 28 | tokenId: BigNumber, 29 | ): Promise => { 30 | const senderAddress = await sender.getAddress(); 31 | expect(await token.getApproved(tokenId)).to.not.equal(toAddress); 32 | 33 | await expect(token.connect(sender).approve(toAddress, tokenId)) 34 | .to.emit(token, "Approval") 35 | .withArgs(senderAddress, toAddress, tokenId); 36 | 37 | expect(await token.getApproved(tokenId)).to.equal(toAddress); 38 | }; 39 | -------------------------------------------------------------------------------- /contracts/interfaces/IVaultFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Interface for a vault factory contract 7 | */ 8 | interface IVaultFactory { 9 | /** 10 | * @dev Emitted when a new vault is created 11 | */ 12 | event VaultCreated(address vault, address to); 13 | 14 | /** 15 | * @dev Return true if the given address is a vault instance created by this factory, else false 16 | * @param instance The address to check 17 | */ 18 | function isInstance(address instance) external view returns (bool validity); 19 | 20 | /** 21 | * @dev Return the number of instances created by this factory 22 | */ 23 | function instanceCount() external view returns (uint256); 24 | 25 | /** 26 | * @dev Return the instance at the given index 27 | * @dev allows for enumeration over all instances 28 | * @param index the index to return instance at 29 | */ 30 | function instanceAt(uint256 index) external view returns (address); 31 | 32 | /** 33 | * @dev Creates a new asset vault bundle, returning the bundle tokenId 34 | * @dev note that the vault tokenId is a uint256 cast of the vault address 35 | * @param to The recipient of the newly created bundle 36 | */ 37 | function initializeBundle(address to) external returns (uint256); 38 | } 39 | -------------------------------------------------------------------------------- /test/utils/erc20.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Signer, BigNumber } 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: BigNumber): 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 | -------------------------------------------------------------------------------- /docs/PunkRouter.md: -------------------------------------------------------------------------------- 1 | # `PunkRouter` 2 | 3 | The PunkRouter is a shim for the `AssetWrapper` that allows it work with CryptoPunks, 4 | which do not fit the ERC721 standard. The PunkRouter's deposit methods use the 5 | [Wrapped Punks](https://wrappedpunks.com/) contract to convert a punk to ERC721 6 | before deposit. 7 | 8 | ### API 9 | 10 | ### `constructor(IAssetWrapper _assetWrapper, IWrappedPunks _wrappedPunks, IPunks _punks)` 11 | 12 | Deploys the contract with references to the specified `AssetWrapper`, wrapped punks contract, 13 | and original CryptoPunks contract. 14 | 15 | ### `function depositPunk(uint256 punkIndex, uint256 bundleId) external` 16 | 17 | Wrap and deposit an original cryptopunk into an AssetWrapper bundle. The `punkIndex` is the 18 | punk ID, and the `bundleId` is the token ID of the bundle within the `AssetWrapper` 19 | contract. 20 | 21 | Requirements: 22 | 23 | - The CryptoPunk at `punkIndex` must be offered for sale to this address for 0 ETH. This 24 | is equivalent to an approval for normal ERC721s - see the [CryptoPunks smart contract](https://github.com/larvalabs/cryptopunks/blob/master/contracts/CryptoPunksMarket.sol#L148) for more information. 25 | - `msg.sender` must be the owner of the punk at `punkIndex`. 26 | 27 | ### `withdrawPunk(uint256 punkIndex, address to) external` 28 | 29 | Withdraw a punk that is accidentally held by the PunkRouter contract, 30 | maybe due to a mistaken send. Can only be called by admin. Transfers 31 | the punk to `to`. 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Non-fungible Technologies, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /contracts/interfaces/ICallWhitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | /** 6 | * @dev Interface for a call whitelist contract which 7 | * whitelists certain functions on certain contracts to call 8 | */ 9 | interface ICallWhitelist { 10 | /** 11 | * @dev Emitted when a new call is whitelisted 12 | */ 13 | event CallAdded(address operator, address callee, bytes4 selector); 14 | 15 | /** 16 | * @dev Emitted when a call is removed from the whitelist 17 | */ 18 | event CallRemoved(address operator, address callee, bytes4 selector); 19 | 20 | /** 21 | * @dev Return true if the given function on the given callee is whitelisted 22 | * @param callee The contract that is intended to be called 23 | * @param selector The function selector that is intended to be called 24 | * @return true if whitelisted, else false 25 | */ 26 | function isWhitelisted(address callee, bytes4 selector) external view returns (bool); 27 | 28 | /** 29 | * @dev Add the given callee and selector to the whitelist 30 | * @param callee The contract to whitelist 31 | * @param selector The function selector to whitelist 32 | */ 33 | function add(address callee, bytes4 selector) external; 34 | 35 | /** 36 | * @dev Remove the given callee and selector from the whitelist 37 | * @param callee The contract to whitelist 38 | * @param selector The function selector to whitelist 39 | */ 40 | function remove(address callee, bytes4 selector) external; 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow will install Deno and run tests across stable and nightly builds on Windows, Ubuntu and macOS. 7 | # For more information see: https://github.com/denolib/setup-deno 8 | 9 | name: Test 10 | 11 | on: 12 | push: 13 | branches: [main] 14 | pull_request: 15 | branches: [main] 16 | 17 | jobs: 18 | test: 19 | runs-on: ${{ matrix.os }} # runs a test on Ubuntu, Windows and macOS 20 | 21 | strategy: 22 | matrix: 23 | node-version: [12.x] 24 | os: [macOS-latest, windows-latest, ubuntu-latest] 25 | 26 | steps: 27 | - name: Setup Repo 28 | uses: actions/checkout@v2 29 | 30 | - name: Uses node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} # tests across multiple Deno versions 34 | 35 | - name: Install 36 | run: yarn install --frozen-lockfile 37 | 38 | - name: Lint 39 | run: yarn lint 40 | 41 | - name: Compile 42 | run: yarn compile 43 | 44 | - name: Generate Typechain 45 | run: yarn typechain 46 | 47 | - name: Test 48 | env: 49 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 50 | run: yarn test 51 | 52 | - name: Coverage 53 | env: 54 | ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }} 55 | run: yarn coverage 56 | -------------------------------------------------------------------------------- /docs/RepaymentController.md: -------------------------------------------------------------------------------- 1 | # `RepaymentController` 2 | 3 | The RepaymentController is a periphery-style contract that allows interactions 4 | with `LoanCore` for the purposes of repaying or claiming defaulted loans. 5 | 6 | While `LoanCore` maintains the invariants of owed tokens and collateral for 7 | valid loan state, `RepaymentController` is responsible for checking that tokens 8 | have been approved for withdrawal and repayment before `LoanCore` operations. 9 | 10 | ### API 11 | 12 | ### `constructor(ILoanCore _loanCore, IPromissoryNote _borrowerNote, IPromissoryNote _lenderNote)` 13 | 14 | Deploys the contract with references to the specified `LoanCore` and `PromissoryNote` 15 | contracts. 16 | 17 | ### `repay(uint256 borrowerNoteId) external` 18 | 19 | Called by the borrower to repay a currently active loan. Withdraws 20 | repayment tokens and delegates logic to `LoanCore-repay`. Caller sends 21 | `borrowerNoteId` to reference the loan, which is then dereferenced to a loan ID. 22 | 23 | Requirements: 24 | 25 | - The loan must be in the `Active` state. 26 | - The repayment amount must be approved for withdrawal by the `RepaymentController`. 27 | 28 | ### `claim(uint256 lenderNoteId) external` 29 | 30 | Used by the lender to claim collateral for a loan that is in default. Caller sends 31 | `lenderNoteId` to reference the loan, which is then dereferenced to a loan ID. 32 | Sends the associated collateral token back to the holder of the `LenderNote.` 33 | 34 | Requirements: 35 | 36 | - The loan must be in the `Active` state. 37 | - The current time must be greater than the loan's due date. 38 | - The caller must be the owner of the associated `LenderNote`. 39 | -------------------------------------------------------------------------------- /contracts/FeeController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "./interfaces/IFeeController.sol"; 6 | 7 | /** 8 | * Fee Controller is intended to be an upgradable component of Pawnfi 9 | * where new fees can be added or modified based on different user attributes 10 | * 11 | * Type/size of loan being requested 12 | * Amount of tokens being staked, etc 13 | * 14 | * Version 1 (as of 4/21/2021) Capabilities: 15 | * 16 | * FeeController will be called once after a loan has been matched so that we can 17 | * create an origination fee (2% credited to PawnFi) 18 | * @dev support for floating point originationFee should be discussed 19 | */ 20 | 21 | contract FeeController is AccessControlEnumerable, IFeeController, Ownable { 22 | // initial fee is 3% 23 | uint256 private originationFee = 300; 24 | 25 | constructor() {} 26 | 27 | /** 28 | * @dev Set the origination fee to the given value 29 | * 30 | * @param _originationFee the new origination fee, in bps 31 | * 32 | * Requirements: 33 | * 34 | * - The caller must be the owner of the contract 35 | */ 36 | function setOriginationFee(uint256 _originationFee) external override onlyOwner { 37 | originationFee = _originationFee; 38 | emit UpdateOriginationFee(_originationFee); 39 | } 40 | 41 | /** 42 | * @dev Get the current origination fee in bps 43 | * 44 | */ 45 | function getOriginationFee() public view override returns (uint256) { 46 | return originationFee; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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"; 6 | import { main as flashRolloverDeploy } from "./deploy-flash-rollover"; 7 | import { deployNFTs, mintAndDistribute, SECTION_SEPARATOR } from "./bootstrap-tools"; 8 | 9 | export async function main(): Promise { 10 | // Bootstrap five accounts only. 11 | // Skip the first account, since the 12 | // first signer will be the deployer. 13 | const [, ...signers] = (await ethers.getSigners()).slice(0, 6); 14 | 15 | console.log(SECTION_SEPARATOR); 16 | console.log("Deploying resources...\n"); 17 | 18 | // Deploy the smart contracts 19 | const { loanCore } = await deploy(); 20 | console.log(SECTION_SEPARATOR); 21 | const { mockAddressProvider } = await flashRolloverDeploy(loanCore.address); 22 | const lendingPool = await mockAddressProvider.getLendingPool(); 23 | 24 | // Mint some NFTs 25 | console.log(SECTION_SEPARATOR); 26 | const { punks, art, beats, weth, pawnToken, usd } = await deployNFTs(); 27 | 28 | // Distribute NFTs and ERC20s 29 | console.log(SECTION_SEPARATOR); 30 | console.log("Distributing assets...\n"); 31 | await mintAndDistribute(signers, weth, pawnToken, usd, punks, art, beats, lendingPool); 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 | } 44 | -------------------------------------------------------------------------------- /contracts/test/MockLendingPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 6 | 7 | import "../interfaces/IFlashRollover.sol"; 8 | 9 | /* solhint-disable no-unused-vars */ 10 | contract MockAddressesProvider { 11 | address public lendingPool; 12 | 13 | constructor(address _lendingPool) { 14 | lendingPool = _lendingPool; 15 | } 16 | 17 | function getLendingPool() external view returns (address) { 18 | return lendingPool; 19 | } 20 | } 21 | 22 | contract MockLendingPool { 23 | uint256 private loanFeeBps = 9; 24 | 25 | event FlashLoan(uint256 amount, uint256 fee); 26 | 27 | function flashLoan( 28 | address receiverAddress, 29 | address[] calldata assets, 30 | uint256[] calldata amounts, 31 | uint256[] calldata modes, 32 | address onBehalfOf, 33 | bytes calldata params, 34 | uint16 referralCode 35 | ) external { 36 | uint256 startBalance = IERC20(assets[0]).balanceOf(address(this)); 37 | uint256 premium = (amounts[0] * loanFeeBps) / 10_000; 38 | uint256[] memory premiums = new uint256[](1); 39 | premiums[0] = premium; 40 | 41 | // Send assets - only supports one asset 42 | IERC20(assets[0]).transfer(receiverAddress, amounts[0]); 43 | 44 | // Call the callback operation 45 | IFlashLoanReceiver(receiverAddress).executeOperation(assets, amounts, premiums, msg.sender, params); 46 | 47 | emit FlashLoan(amounts[0], premium); 48 | // Require repayment plus premium 49 | require( 50 | IERC20(assets[0]).transferFrom(receiverAddress, address(this), amounts[0] + premiums[0]), 51 | "Failed repayment" 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/deploy-with-punk-router.ts: -------------------------------------------------------------------------------- 1 | import { Contract } from "ethers"; 2 | import { ethers } from "hardhat"; 3 | 4 | import { main as deployMain, DeployedResources } from "./deploy"; 5 | 6 | import { ORIGINATOR_ROLE as DEFAULT_ORIGINATOR_ROLE, REPAYER_ROLE as DEFAULT_REPAYER_ROLE } from "./constants"; 7 | export interface DeployedResourcesWithPunks extends DeployedResources { 8 | punkRouter: Contract; 9 | } 10 | 11 | export async function main( 12 | ORIGINATOR_ROLE = DEFAULT_ORIGINATOR_ROLE, 13 | REPAYER_ROLE = DEFAULT_REPAYER_ROLE, 14 | WRAPPED_PUNKS = "0xb7F7F6C52F2e2fdb1963Eab30438024864c313F6", 15 | CRYPTO_PUNKS = "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", 16 | ): Promise { 17 | const { 18 | assetVault, 19 | feeController, 20 | loanCore, 21 | borrowerNote, 22 | lenderNote, 23 | repaymentController, 24 | originationController, 25 | } = await deployMain(ORIGINATOR_ROLE, REPAYER_ROLE); 26 | 27 | const PunkRouter = await ethers.getContractFactory("PunkRouter"); 28 | const punkRouter = await PunkRouter.deploy(assetVault.address, WRAPPED_PUNKS, CRYPTO_PUNKS); 29 | await punkRouter.deployed(); 30 | 31 | console.log("PunkRouter deployed to:", punkRouter.address); 32 | 33 | return { 34 | assetVault, 35 | feeController, 36 | loanCore, 37 | borrowerNote, 38 | lenderNote, 39 | repaymentController, 40 | originationController, 41 | punkRouter, 42 | }; 43 | } 44 | 45 | // We recommend this pattern to be able to use async/await everywhere 46 | // and properly handle errors. 47 | if (require.main === module) { 48 | main() 49 | .then(() => process.exit(0)) 50 | .catch((error: Error) => { 51 | console.error(error); 52 | process.exit(1); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /contracts/interfaces/IOriginationController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../libraries/LoanLibrary.sol"; 6 | 7 | /** 8 | * @dev Interface for the OriginationController contracts 9 | */ 10 | interface IOriginationController { 11 | /** 12 | * @dev initializes loan from loan core 13 | * Requirements: 14 | * - The caller must be a borrower or lender 15 | * - The external signer must not be msg.sender 16 | * - The external signer must be a borrower or lender 17 | * @param loanTerms - struct containing specifics of loan made between lender and borrower 18 | * @param borrower - address of borrowerPromissory note 19 | * @param lender - address of lenderPromissory note 20 | * @param v, r, s - signature from erc20 21 | */ 22 | function initializeLoan( 23 | LoanLibrary.LoanTerms calldata loanTerms, 24 | address borrower, 25 | address lender, 26 | uint8 v, 27 | bytes32 r, 28 | bytes32 s 29 | ) external returns (uint256 loanId); 30 | 31 | /** 32 | * @dev creates a new loan, with permit attached 33 | * @param loanTerms - struct containing specifics of loan made between lender and borrower 34 | * @param borrower - address of borrowerPromissory note 35 | * @param lender - address of lenderPromissory note 36 | * @param v, r, s - signature from erc20 37 | * @param collateralV, collateralR, collateralS - signature from collateral 38 | * @param permitDeadline - timestamp at which the collateral signature becomes invalid 39 | */ 40 | function initializeLoanWithCollateralPermit( 41 | LoanLibrary.LoanTerms calldata loanTerms, 42 | address borrower, 43 | address lender, 44 | uint8 v, 45 | bytes32 r, 46 | bytes32 s, 47 | uint8 collateralV, 48 | bytes32 collateralR, 49 | bytes32 collateralS, 50 | uint256 permitDeadline 51 | ) external returns (uint256 loanId); 52 | } 53 | -------------------------------------------------------------------------------- /contracts/libraries/LoanLibrary.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | library LoanLibrary { 4 | /** 5 | * @dev Enum describing the current state of a loan 6 | * State change flow: 7 | * Created -> Active -> Repaid 8 | * -> Defaulted 9 | */ 10 | enum LoanState { 11 | // We need a default that is not 'Created' - this is the zero value 12 | DUMMY_DO_NOT_USE, 13 | // The loan data is stored, but not initiated yet. 14 | Created, 15 | // The loan has been initialized, funds have been delivered to the borrower and the collateral is held. 16 | Active, 17 | // The loan has been repaid, and the collateral has been returned to the borrower. This is a terminal state. 18 | Repaid, 19 | // The loan was delinquent and collateral claimed by the lender. This is a terminal state. 20 | Defaulted 21 | } 22 | 23 | /** 24 | * @dev The raw terms of a loan 25 | */ 26 | struct LoanTerms { 27 | // The number of seconds representing relative due date of the loan 28 | uint256 durationSecs; 29 | // The amount of principal in terms of the payableCurrency 30 | uint256 principal; 31 | // The amount of interest in terms of the payableCurrency 32 | uint256 interest; 33 | // The tokenID of the collateral bundle 34 | uint256 collateralTokenId; 35 | // The payable currency for the loan principal and interest 36 | address payableCurrency; 37 | } 38 | 39 | /** 40 | * @dev The data of a loan. This is stored once the loan is Active 41 | */ 42 | struct LoanData { 43 | // The tokenId of the borrower note 44 | uint256 borrowerNoteId; 45 | // The tokenId of the lender note 46 | uint256 lenderNoteId; 47 | // The raw terms of the loan 48 | LoanTerms terms; 49 | // The current state of the loan 50 | LoanState state; 51 | // Timestamp representing absolute due date date of the loan 52 | uint256 dueDate; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /contracts/test/MockERC721.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/utils/Context.sol"; 5 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | 8 | contract MockERC721 is Context, ERC721Enumerable { 9 | using Counters for Counters.Counter; 10 | Counters.Counter private _tokenIdTracker; 11 | 12 | /** 13 | * @dev Initializes ERC721 token 14 | */ 15 | constructor(string memory name, string memory symbol) ERC721(name, symbol) {} 16 | 17 | /** 18 | * @dev Creates a new token for `to`. Public for any test to call. 19 | * 20 | * See {ERC721-_mint}. 21 | */ 22 | function mint(address to) external returns (uint256 tokenId) { 23 | tokenId = _tokenIdTracker.current(); 24 | _mint(to, uint256(uint160(address(this))) + tokenId); 25 | _tokenIdTracker.increment(); 26 | } 27 | 28 | /** 29 | * @dev Burn the given token, can be called by anyone 30 | */ 31 | function burn(uint256 tokenId) external { 32 | _burn(tokenId); 33 | } 34 | } 35 | 36 | contract MockERC721Metadata is MockERC721 { 37 | using Counters for Counters.Counter; 38 | Counters.Counter private _tokenIdTracker; 39 | 40 | mapping(uint256 => string) public tokenURIs; 41 | 42 | constructor(string memory name, string memory symbol) MockERC721(name, symbol) {} 43 | 44 | function tokenURI(uint256 tokenId) public view override returns (string memory) { 45 | require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token"); 46 | return tokenURIs[tokenId]; 47 | } 48 | 49 | /** 50 | * @dev Creates a new token for `to`. Public for any test to call. 51 | * 52 | * See {ERC721-_mint}. 53 | */ 54 | function mint(address to, string memory tokenUri) external returns (uint256 tokenId) { 55 | tokenId = _tokenIdTracker.current(); 56 | _mint(to, tokenId); 57 | _tokenIdTracker.increment(); 58 | _setTokenURI(tokenId, tokenUri); 59 | } 60 | 61 | function _setTokenURI(uint256 tokenId, string memory tokenUri) internal { 62 | tokenURIs[tokenId] = tokenUri; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /contracts/test/MockERC1155.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/utils/Context.sol"; 5 | import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; 6 | import "@openzeppelin/contracts/utils/Counters.sol"; 7 | 8 | contract MockERC1155 is Context, ERC1155 { 9 | using Counters for Counters.Counter; 10 | Counters.Counter private _tokenIdTracker; 11 | 12 | /** 13 | * @dev Initializes ERC1155 token 14 | */ 15 | constructor() ERC1155("") {} 16 | 17 | /** 18 | * @dev Creates `amount` tokens of token type `id`, and assigns them to `account`. 19 | * 20 | * Emits a {TransferSingle} event. 21 | * 22 | * Requirements: 23 | * 24 | * - `account` cannot be the zero address. 25 | * - If `account` refers to a smart contract, it must implement {IERC1155Receiver-onERC1155Received} and return the 26 | * acceptance magic value. 27 | */ 28 | function mint(address to, uint256 amount) public virtual { 29 | _mint(to, _tokenIdTracker.current(), amount, ""); 30 | _tokenIdTracker.increment(); 31 | } 32 | } 33 | 34 | contract MockERC1155Metadata is MockERC1155 { 35 | using Counters for Counters.Counter; 36 | Counters.Counter private _tokenIdTracker; 37 | 38 | mapping(uint256 => string) public tokenURIs; 39 | 40 | constructor() MockERC1155() {} 41 | 42 | function mint( 43 | address to, 44 | uint256 amount, 45 | string memory tokenUri 46 | ) public virtual { 47 | uint256 tokenId = _tokenIdTracker.current(); 48 | _mint(to, tokenId, amount, ""); 49 | _tokenIdTracker.increment(); 50 | _setTokenURI(tokenId, tokenUri); 51 | } 52 | 53 | function mintBatch( 54 | address to, 55 | uint256[] memory ids, 56 | uint256[] memory amounts, 57 | string[] memory tokenUris, 58 | bytes memory data 59 | ) public virtual { 60 | super._mintBatch(to, ids, amounts, data); 61 | 62 | for (uint256 i = 0; i < ids.length; i++) { 63 | _setTokenURI(ids[i], tokenUris[i]); 64 | } 65 | } 66 | 67 | function _setTokenURI(uint256 tokenId, string memory tokenUri) internal { 68 | tokenURIs[tokenId] = tokenUri; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /contracts/interfaces/IERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 6 | 7 | /** 8 | * @dev Interface for a permittable ERC721 contract 9 | * See https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. 10 | * 11 | * Adds the {permit} method, which can be used to change an account's ERC72 allowance (see {IERC721-allowance}) by 12 | * presenting a message signed by the account. By not relying on {IERC721-approve}, the token holder account doesn't 13 | * need to send a transaction, and thus is not required to hold Ether at all. 14 | */ 15 | interface IERC721Permit is IERC721 { 16 | /** 17 | * @dev Allows `spender` to spend `tokenID` which is owned by`owner`, 18 | * given ``owner``'s signed approval. 19 | * 20 | * Emits an {Approval} event. 21 | * 22 | * Requirements: 23 | * 24 | * - `spender` cannot be the zero address. 25 | * - `owner` must be the owner of `tokenId`. 26 | * - `deadline` must be a timestamp in the future. 27 | * - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` 28 | * over the EIP712-formatted function arguments. 29 | * - the signature must use ``owner``'s current nonce (see {nonces}). 30 | * 31 | * For more information on the signature format, see the 32 | * https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP 33 | * section]. 34 | */ 35 | function permit( 36 | address owner, 37 | address spender, 38 | uint256 tokenId, 39 | uint256 deadline, 40 | uint8 v, 41 | bytes32 r, 42 | bytes32 s 43 | ) external; 44 | 45 | /** 46 | * @dev Returns the current nonce for `owner`. This value must be 47 | * included whenever a signature is generated for {permit}. 48 | * 49 | * Every successful call to {permit} increases ``owner``'s nonce by one. This 50 | * prevents a signature from being used multiple times. 51 | */ 52 | function nonces(address owner) external view returns (uint256); 53 | 54 | /** 55 | * @dev Returns the domain separator used in the encoding of the signature for {permit}, as defined by {EIP712}. 56 | */ 57 | // solhint-disable-next-line func-name-mixedcase 58 | function DOMAIN_SEPARATOR() external view returns (bytes32); 59 | } 60 | -------------------------------------------------------------------------------- /contracts/test/WrappedPunks.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 4 | import "../interfaces/IPunks.sol"; 5 | import "./UserProxy.sol"; 6 | 7 | contract WrappedPunk is ERC721 { 8 | event ProxyRegistered(address user, address proxy); 9 | 10 | // Instance of cryptopunk smart contract 11 | IPunks private _punkContract; 12 | 13 | // Mapping from user address to proxy address 14 | mapping(address => address) private _proxies; 15 | 16 | /** 17 | * @dev Initializes the contract settings 18 | */ 19 | constructor(address punkContract_) ERC721("Wrapped Cryptopunks", "WPUNKS") { 20 | _punkContract = IPunks(punkContract_); 21 | } 22 | 23 | /** 24 | * @dev Gets address of cryptopunk smart contract 25 | */ 26 | function punkContract() public view returns (address) { 27 | return address(_punkContract); 28 | } 29 | 30 | /** 31 | * @dev Registers proxy 32 | */ 33 | function registerProxy() public { 34 | address sender = _msgSender(); 35 | 36 | require(_proxies[sender] == address(0), "PunkWrapper: caller has registered the proxy"); 37 | 38 | address proxy = address(new UserProxy()); 39 | 40 | _proxies[sender] = proxy; 41 | 42 | emit ProxyRegistered(sender, proxy); 43 | } 44 | 45 | /** 46 | * @dev Gets proxy address 47 | */ 48 | function proxyInfo(address user) public view returns (address) { 49 | return _proxies[user]; 50 | } 51 | 52 | /** 53 | * @dev Mints a wrapped punk 54 | */ 55 | function mint(uint256 punkIndex) public { 56 | address sender = _msgSender(); 57 | 58 | UserProxy proxy = UserProxy(_proxies[sender]); 59 | 60 | require(proxy.transfer(address(_punkContract), punkIndex), "PunkWrapper: transfer fail"); 61 | 62 | _mint(sender, punkIndex); 63 | } 64 | 65 | /** 66 | * @dev Burns a specific wrapped punk 67 | */ 68 | function burn(uint256 punkIndex) public { 69 | address sender = _msgSender(); 70 | 71 | require(_isApprovedOrOwner(sender, punkIndex), "PunkWrapper: caller is not owner nor approved"); 72 | 73 | _burn(punkIndex); 74 | 75 | // Transfers ownership of punk on original cryptopunk smart contract to caller 76 | _punkContract.transferPunk(sender, punkIndex); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /contracts/PunkRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "./interfaces/IWrappedPunks.sol"; 7 | import "./interfaces/IPunks.sol"; 8 | 9 | /** 10 | * @dev {ERC721} Router contract allowing users to automatically 11 | * wrap and deposit original cryptopunks into the AssetWrapper 12 | */ 13 | contract PunkRouter is ERC721Holder, Ownable { 14 | address public immutable assetWrapper; 15 | IPunks public immutable punks; 16 | address public immutable proxy; 17 | IWrappedPunks public wrappedPunks; 18 | 19 | constructor( 20 | address _assetWrapper, 21 | IWrappedPunks _wrappedPunks, 22 | IPunks _punks 23 | ) { 24 | assetWrapper = _assetWrapper; 25 | punks = _punks; 26 | wrappedPunks = _wrappedPunks; 27 | wrappedPunks.registerProxy(); 28 | proxy = wrappedPunks.proxyInfo(address(this)); 29 | } 30 | 31 | /** 32 | * @dev Wrap and deposit an original cryptopunk into an AssetWrapper bundle 33 | * 34 | * @param punkIndex The index of the CryptoPunk to deposit 35 | * @param bundleId The id of the wNFT to deposit into 36 | * 37 | * Requirements: 38 | * 39 | * - CryptoPunk punkIndex must be offered for sale to this address for 0 ETH 40 | * Equivalent to an approval for normal ERC721s 41 | * - msg.sender must be the owner of punkIndex 42 | */ 43 | function depositPunk(uint256 punkIndex, uint256 bundleId) external { 44 | IWrappedPunks _wrappedPunks = wrappedPunks; 45 | address punkOwner = punks.punkIndexToAddress(punkIndex); 46 | require(punkOwner == msg.sender, "PunkRouter: not owner"); 47 | punks.buyPunk(punkIndex); 48 | punks.transferPunk(proxy, punkIndex); 49 | 50 | _wrappedPunks.mint(punkIndex); 51 | _wrappedPunks.safeTransferFrom(address(this), address(uint160(bundleId)), punkIndex); 52 | } 53 | 54 | /** 55 | * @dev Withdraw the crypto punk that is accidentally held by the PunkRouter contract 56 | * 57 | * @param punkIndex The index of the CryptoPunk to withdraw 58 | * @param to The address of the new owner 59 | */ 60 | function withdrawPunk(uint256 punkIndex, address to) external onlyOwner { 61 | punks.transferPunk(to, punkIndex); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /docs/ERC721Permit.md: -------------------------------------------------------------------------------- 1 | # `ERC721Permit` 2 | 3 | Implementation of the ERC721 Permit extension allowing approvals to be made 4 | via signatures, as defined in [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612). 5 | 6 | See the [EIP-712 spec](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol). 7 | 8 | This contract [IERC721](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#IERC721) by adding 9 | the `permit` method, which can be used to change an account's ERC721 allowance (see `IERC721-allowance`) 10 | by presenting a message signed by the account. By not relying on `IERC721-approve`, the token holder 11 | account doesn't need to send a transaction, and thus is not required to hold Ether at all. 12 | 13 | ## API 14 | 15 | ### `constructor(string name)` 16 | 17 | Initializes the `EIP712` domain separator using the `name` parameter, and setting `version` to `"1"`. `name` should be the same 18 | as the `ERC721` token name. 19 | 20 | ### `permit(address owner, address spender, uint256 tokenId, uint256 deadline, uint8 v, bytes32 r, bytes32 s)` _(public)_ 21 | 22 | Allows `spender` to spend `tokenID` which is owned by`owner`, given the signed approval of `owner`. 23 | 24 | Requirements: 25 | 26 | - `spender` cannot be the zero address. 27 | - `owner` must be the owner of `tokenId`. 28 | - `deadline` must be a timestamp in the future. 29 | - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner` 30 | over the EIP712-formatted function arguments. 31 | - the signature must use `owner`'s current nonce (see {nonces}). 32 | 33 | For more information on the signature format, see the 34 | [relevant EIP section](https://eips.ethereum.org/EIPS/eip-2612#specification). 35 | 36 | ### `nonces(address owner) → uint256` _(public)_ 37 | 38 | Returns the current nonce for `owner`. This value must be 39 | included whenever a signature is generated for `permit`. 40 | Every successful call to `permit` increases `owner`'s nonce by one. This 41 | prevents a signature from being used multiple times. 42 | 43 | ### `DOMAIN_SEPARATOR() → bytes32` _(external)_ 44 | 45 | Returns the domain separator used in the encoding of the signature for `permit`, as defined by [EIP712](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol). 46 | 47 | ### `_useNonce(address owner) → uint256 current` _(internal)_ 48 | 49 | Consumes a nonce: return the current nonce value for the owner and and increments it. 50 | -------------------------------------------------------------------------------- /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"; 6 | import { main as flashRolloverDeploy } from "./deploy-flash-rollover"; 7 | import { deployNFTs, mintAndDistribute, SECTION_SEPARATOR, wrapAssetsAndMakeLoans } from "./bootstrap-tools"; 8 | 9 | export async function main(): Promise { 10 | // Bootstrap five accounts only. 11 | // Skip the first account, since the 12 | // first signer will be the deployer. 13 | const [, ...signers] = (await ethers.getSigners()).slice(0, 6); 14 | 15 | console.log(SECTION_SEPARATOR); 16 | console.log("Deploying resources...\n"); 17 | 18 | // Deploy the smart contracts 19 | const { assetVault, originationController, repaymentController, borrowerNote, loanCore } = await deploy(); 20 | const { mockAddressProvider } = await flashRolloverDeploy(loanCore.address); 21 | const lendingPool = await mockAddressProvider.getLendingPool(); 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, lendingPool); 31 | 32 | // Wrap some assets 33 | console.log(SECTION_SEPARATOR); 34 | console.log("Wrapping assets...\n"); 35 | await wrapAssetsAndMakeLoans( 36 | signers, 37 | assetVault, 38 | originationController, 39 | borrowerNote, 40 | repaymentController, 41 | punks, 42 | usd, 43 | beats, 44 | weth, 45 | art, 46 | pawnToken, 47 | ); 48 | 49 | // End state: 50 | // 0 is clean (but has a bunch of tokens and NFTs) 51 | // 1 has 2 bundles and 1 open borrow, one closed borrow 52 | // 2 has two open lends and one closed lend 53 | // 3 has 3 bundles, two open borrows, one closed borrow, and one closed lend 54 | // 4 has 1 bundle, an unused bundle, one open lend and one open borrow 55 | } 56 | 57 | // We recommend this pattern to be able to use async/await everywhere 58 | // and properly handle errors. 59 | if (require.main === module) { 60 | main() 61 | .then(() => process.exit(0)) 62 | .catch((error: Error) => { 63 | console.error(error); 64 | process.exit(1); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /scripts/deploy-flash-rollover.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import { FlashRollover, MockLendingPool, MockAddressesProvider } from "../typechain"; 4 | 5 | import { SECTION_SEPARATOR } from "./bootstrap-tools"; 6 | 7 | export interface DeployedResources { 8 | flashRollover: FlashRollover; 9 | mockAddressProvider: MockAddressesProvider; 10 | } 11 | 12 | // TODO: Set arguments once a new loan core is deployed. 13 | export async function main( 14 | ADDRESSES_PROVIDER_ADDRESS = "0xB53C1a33016B2DC2fF3653530bfF1848a515c8c5", 15 | ): Promise { 16 | // Hardhat always runs the compile task when running scripts through it. 17 | // If this runs in a standalone fashion you may want to call compile manually 18 | // to make sure everything is compiled 19 | // await run("compile"); 20 | 21 | console.log(SECTION_SEPARATOR); 22 | const signers = await ethers.getSigners(); 23 | console.log("Deployer address: ", signers[0].address); 24 | console.log("Deployer balance: ", (await signers[0].getBalance()).toString()); 25 | console.log(SECTION_SEPARATOR); 26 | 27 | const MockLendingPoolFactory = await ethers.getContractFactory("MockLendingPool"); 28 | const mockLendingPool = await MockLendingPoolFactory.deploy(); 29 | await mockLendingPool.deployed(); 30 | 31 | console.log("MockLendingPool deployed to:", mockLendingPool.address); 32 | 33 | const MockAddressProviderFactory = await ethers.getContractFactory("MockAddressesProvider"); 34 | const mockAddressProvider = await MockAddressProviderFactory.deploy(mockLendingPool.address); 35 | await mockAddressProvider.deployed(); 36 | 37 | console.log("MockAddressProvider deployed to:", mockAddressProvider.address); 38 | 39 | const FlashRolloverFactory = await ethers.getContractFactory("FlashRollover"); 40 | // console.log("deploying ", ADDRESSES_PROVIDER_ADDRESS); 41 | const flashRollover = await FlashRolloverFactory.deploy(mockAddressProvider.address); 42 | 43 | await flashRollover.deployed(); 44 | 45 | console.log("FlashRollover deployed to:", flashRollover.address); 46 | 47 | return { flashRollover, mockAddressProvider }; 48 | } 49 | 50 | // We recommend this pattern to be able to use async/await everywhere 51 | // and properly handle errors. 52 | if (require.main === module) { 53 | main() 54 | .then(() => process.exit(0)) 55 | .catch((error: Error) => { 56 | console.error(error); 57 | process.exit(1); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /contracts/RepaymentController.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 9 | 10 | import "./libraries/LoanLibrary.sol"; 11 | import "./interfaces/IPromissoryNote.sol"; 12 | import "./interfaces/ILoanCore.sol"; 13 | import "./interfaces/IRepaymentController.sol"; 14 | 15 | contract RepaymentController is IRepaymentController { 16 | using SafeMath for uint256; 17 | using SafeERC20 for IERC20; 18 | 19 | ILoanCore private loanCore; 20 | IPromissoryNote private borrowerNote; 21 | IPromissoryNote private lenderNote; 22 | 23 | constructor( 24 | ILoanCore _loanCore, 25 | IPromissoryNote _borrowerNote, 26 | IPromissoryNote _lenderNote 27 | ) { 28 | loanCore = _loanCore; 29 | borrowerNote = _borrowerNote; 30 | lenderNote = _lenderNote; 31 | } 32 | 33 | /** 34 | * @inheritdoc IRepaymentController 35 | */ 36 | function repay(uint256 borrowerNoteId) external override { 37 | // get loan from borrower note 38 | uint256 loanId = borrowerNote.loanIdByNoteId(borrowerNoteId); 39 | 40 | require(loanId != 0, "RepaymentController: repay could not dereference loan"); 41 | 42 | LoanLibrary.LoanTerms memory terms = loanCore.getLoan(loanId).terms; 43 | 44 | // withdraw principal plus interest from borrower and send to loan core 45 | 46 | IERC20(terms.payableCurrency).safeTransferFrom(msg.sender, address(this), terms.principal.add(terms.interest)); 47 | IERC20(terms.payableCurrency).approve(address(loanCore), terms.principal.add(terms.interest)); 48 | 49 | // call repay function in loan core 50 | loanCore.repay(loanId); 51 | } 52 | 53 | /** 54 | * @inheritdoc IRepaymentController 55 | */ 56 | function claim(uint256 lenderNoteId) external override { 57 | // make sure that caller owns lender note 58 | address lender = lenderNote.ownerOf(lenderNoteId); 59 | require(lender == msg.sender, "RepaymentController: not owner of lender note"); 60 | 61 | // get loan from lender note 62 | uint256 loanId = lenderNote.loanIdByNoteId(lenderNoteId); 63 | require(loanId != 0, "RepaymentController: claim could not dereference loan"); 64 | 65 | // call claim function in loan core 66 | loanCore.claim(loanId); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/OriginationController.md: -------------------------------------------------------------------------------- 1 | # `OriginationController` 2 | 3 | The OriginationController is a periphery-style contract that allows interactions 4 | with `LoanCore` for the purposes of initializing loans. 5 | 6 | While `LoanCore` maintains the invariants of owed tokens and collateral for 7 | valid loan state, `OriginationController` is responsible for checking mutual 8 | loan consent by verifying the required signatures for loan creation. 9 | 10 | ### API 11 | 12 | ### `constructor(address _loanCore, address _assetWrapper)` 13 | 14 | Deploys the contract with references to the specified `LoanCore` and `AssetWrapper` 15 | contracts. Also initializes a domain separator and version for `EIP712` signatures 16 | for collateral permits. 17 | 18 | ### `intializeLoan` 19 | 20 | ``` 21 | function initializeLoan( 22 | LoanLibrary.LoanTerms calldata loanTerms, 23 | address borrower, 24 | address lender, 25 | uint8 v, 26 | bytes32 r, 27 | bytes32 s 28 | ) external; 29 | ``` 30 | 31 | Initializes a loan with `LoanCore`. See the `LoanCore` documentation for the `LoanTerms` 32 | data type. Validates the signature against the submitted terms, then withdraws principal 33 | from the lender, and the collateral from the borrower. Approves `LoanCore` to then 34 | withdraw the associated principal and collateral, and calls the loan initiation functions 35 | in `LoanCore`. 36 | 37 | Requirements: 38 | 39 | - The caller must be the borrower or lender. 40 | - The external signer must not be `msg.sender`. 41 | - The external signer must be the borrower or lender. 42 | - The collateral must be approved with withdrawal by the `OriginationController`. 43 | 44 | ### `initializeLoanWithCollateralPermit` 45 | 46 | ``` 47 | function initializeLoanWithCollateralPermit( 48 | LoanLibrary.LoanTerms calldata loanTerms, 49 | address borrower, 50 | address lender, 51 | uint8 v, 52 | bytes32 r, 53 | bytes32 s, 54 | uint8 collateralV, 55 | bytes32 collateralR, 56 | bytes32 collateralS, 57 | uint256 permitDeadline 58 | ) external; 59 | ``` 60 | 61 | Calls `ERC721-permit` on the `AssetWrapper` using the collateral permit signature 62 | to approve collateral withdrawal. Does not require on-chain pre-approval. 63 | 64 | After permission for the collateral withdrawal is validated, logic is delegated 65 | to `initializeLoan`. 66 | 67 | Requirements: 68 | 69 | - The caller must be the borrower or lender. 70 | - The external signer must not be `msg.sender`. 71 | - The external signer must be the borrower or lender. 72 | - The collateral signature must match the specified `collateralTokenId` and be from the borrower. 73 | -------------------------------------------------------------------------------- /scripts/transfer-ownership.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | const ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; 4 | const FEE_CLAIMER_ROLE = "0x8dd046eb6fe22791cf064df41dbfc76ef240a563550f519aac88255bd8c2d3bb"; 5 | 6 | export async function main( 7 | LOAN_CORE_ADDRESS = process.env.LOAN_CORE_ADDRESS, 8 | ADMIN_ADDRESS = process.env.ADMIN_ADDRESS, 9 | FEE_CONTROLLER_ADDRESS = process.env.FEE_CONTROLLER_ADDRESS, 10 | ): Promise { 11 | if (!LOAN_CORE_ADDRESS) { 12 | throw new Error("Must specify LOAN_CORE_ADDRESS in environment!"); 13 | } 14 | 15 | if (!ADMIN_ADDRESS) { 16 | throw new Error("Must specify ADMIN_ADDRESS in environment!"); 17 | } 18 | 19 | const [deployer] = await ethers.getSigners(); 20 | console.log(`Deployer address: ${await deployer.getAddress()}`); 21 | console.log(`Admin address: ${ADMIN_ADDRESS}`); 22 | console.log(`Loan core address: ${LOAN_CORE_ADDRESS}`); 23 | if (FEE_CONTROLLER_ADDRESS) { 24 | console.log(`Fee controller address: ${FEE_CONTROLLER_ADDRESS}`); 25 | } 26 | 27 | const loanCore = await ethers.getContractAt("LoanCore", LOAN_CORE_ADDRESS); 28 | // set LoanCore admin and fee claimer 29 | const updateLoanCoreFeeClaimer = await loanCore.grantRole(FEE_CLAIMER_ROLE, ADMIN_ADDRESS); 30 | await updateLoanCoreFeeClaimer.wait(); 31 | const updateLoanCoreAdmin = await loanCore.grantRole(ADMIN_ROLE, ADMIN_ADDRESS); 32 | await updateLoanCoreAdmin.wait(); 33 | 34 | // renounce ownership from deployer 35 | const renounceAdmin = await loanCore.renounceRole(ADMIN_ROLE, await deployer.getAddress()); 36 | await renounceAdmin.wait(); 37 | // renounce ability to claim fees 38 | const renounceFeeClaimer = await loanCore.renounceRole(FEE_CLAIMER_ROLE, await deployer.getAddress()); 39 | await renounceFeeClaimer.wait(); 40 | 41 | if (FEE_CONTROLLER_ADDRESS) { 42 | // set FeeController admin 43 | const feeController = await ethers.getContractAt("FeeController", FEE_CONTROLLER_ADDRESS); 44 | const updateFeeControllerAdmin = await feeController.transferOwnership(ADMIN_ADDRESS); 45 | await updateFeeControllerAdmin.wait(); 46 | } 47 | 48 | console.log("Transferred all ownership.\n"); 49 | } 50 | 51 | // We recommend this pattern to be able to use async/await everywhere 52 | // and properly handle errors. 53 | if (require.main === module) { 54 | main() 55 | .then(() => process.exit(0)) 56 | .catch((error: Error) => { 57 | console.error(error); 58 | process.exit(1); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /test/FeeController.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import hre, { waffle } from "hardhat"; 3 | const { loadFixture } = waffle; 4 | import { Signer } from "ethers"; 5 | import { FeeController } from "../typechain"; 6 | import { deploy } from "./utils/contracts"; 7 | 8 | interface TestContext { 9 | feeController: FeeController; 10 | user: Signer; 11 | other: Signer; 12 | signers: Signer[]; 13 | } 14 | 15 | describe("FeeController", () => { 16 | const fixture = async (): Promise => { 17 | const signers: Signer[] = await hre.ethers.getSigners(); 18 | const feeController = await deploy("FeeController", signers[0], []); 19 | 20 | return { 21 | feeController, 22 | user: signers[0], 23 | other: signers[1], 24 | signers: signers.slice(2), 25 | }; 26 | }; 27 | 28 | describe("constructor", () => { 29 | it("creates Fee Controller", async () => { 30 | const signers: Signer[] = await hre.ethers.getSigners(); 31 | expect(await deploy("FeeController", signers[0], [])); 32 | }); 33 | 34 | describe("setOriginationFee", () => { 35 | it("reverts if sender does not have admin role", async () => { 36 | const { feeController, other } = await loadFixture(fixture); 37 | await expect(feeController.connect(other).setOriginationFee(1234)).to.be.reverted; 38 | }); 39 | 40 | it("sets origination fee", async () => { 41 | const { feeController, user } = await loadFixture(fixture); 42 | await expect(feeController.connect(user).setOriginationFee(1234)) 43 | .to.emit(feeController, "UpdateOriginationFee") 44 | .withArgs(1234); 45 | }); 46 | }); 47 | 48 | describe("getOriginationFee", () => { 49 | it("initially returns 3%", async () => { 50 | const { feeController, user } = await loadFixture(fixture); 51 | const originationFee = await feeController.connect(user).getOriginationFee(); 52 | expect(originationFee).to.equal(300); 53 | }); 54 | 55 | it("returns updated origination fee after set", async () => { 56 | const { feeController, user } = await loadFixture(fixture); 57 | const newFee = 200; 58 | 59 | await feeController.connect(user).setOriginationFee(newFee); 60 | 61 | const originationFee = await feeController.connect(user).getOriginationFee(); 62 | expect(originationFee).to.equal(newFee); 63 | }); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /docs/PromissoryNote.md: -------------------------------------------------------------------------------- 1 | # `PromissoryNote` 2 | 3 | Built off Openzeppelin's [ERC721PresetMinterPauserAutoId](https://docs.openzeppelin.com/contracts/3.x/api/presets#ERC721PresetMinterPauserAutoId). 4 | 5 | ERC721 token, including: 6 | 7 | - ability for holders to burn (destroy) their tokens 8 | - a minter role that allows for token minting (creation) 9 | - token ID and URI autogeneration 10 | 11 | This contract uses [AccessControl](https://docs.openzeppelin.com/contracts/4.x/api/access#AccessControl) 12 | to lock permissioned functions using the different roles - head to its documentation for details. 13 | 14 | The account that deploys the contract will be granted the minter and pauser 15 | roles, as well as the default admin role, which will let it grant both minter 16 | and pauser roles to other accounts. 17 | 18 | `PromissoryNote` instances for both the lender and borrower notes should be deployed 19 | when an instance of `LoanCore` is deployed. 20 | 21 | ## API 22 | 23 | ### `constructor(string name, string symbol)` 24 | 25 | Creates the borrowor note contract linked to a specific `LoanCore` instance. 26 | The loan core reference is non-upgradeable. Passes `name` and `symbol` to the 27 | `ERC721` constructor. 28 | 29 | Grants `PAUSER_ROLE`, `MINTER_ROLE`, and `BURNER_ROLE` to the sender 30 | contract, provided it is an instance of LoanCore. 31 | 32 | ### `mint(address to)` _(external)_ 33 | 34 | Creates a new token for `to`. Its token ID will be automatically 35 | assigned (and available on the emitted `IERC721-Transfer` event), and the token 36 | URI autogenerated based on the base URI passed at construction. 37 | 38 | See [ERC721-\_mint](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#ERC721-_mint-address-uint256-). 39 | 40 | Requirements: 41 | 42 | - the caller must have the `MINTER_ROLE`. 43 | 44 | ### `burn(uint256 tokenId)` _(external)_ 45 | 46 | Burns `tokenId`. See [ERC721-\_burn](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#ERC721-_burn-uint256-). 47 | 48 | Requirements: 49 | 50 | - The caller must own `tokenId` or be an approved operator. 51 | The loan core contract can only burn a loan that is finished: 52 | either repaid or claimed. 53 | 54 | ### `supportsInterface(bytes4 interfaceId) → bool` _(public)_ 55 | 56 | Override of `supportsInterface` for `AccessControlEnumerable`, `ERC721`, `ERC721Enumerable`. 57 | 58 | See [IERC165-supportsInterface](https://docs.openzeppelin.com/contracts/4.x/api/utils#IERC165-supportsInterface-bytes4-). 59 | 60 | ### `_beforeTokenTransfer(address from, address to, uint256 amount)` _(internal)_ 61 | 62 | override of `_beforeTokenTransfer` for `ERC721`, `ERC721Enumerable`, `ERC721Pausable`. 63 | 64 | See [IERC721-\_beforeTokenTransfer](https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#ERC721-_beforeTokenTransfer-address-address-uint256-). 65 | -------------------------------------------------------------------------------- /contracts/interfaces/ILoanCore.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 "../libraries/LoanLibrary.sol"; 8 | 9 | import "./IPromissoryNote.sol"; 10 | import "./IFeeController.sol"; 11 | import "./ILoanCore.sol"; 12 | 13 | /** 14 | * @dev Interface for the LoanCore contract 15 | */ 16 | interface ILoanCore { 17 | /** 18 | * @dev Emitted when a loan is initially created 19 | */ 20 | event LoanCreated(LoanLibrary.LoanTerms terms, uint256 loanId); 21 | 22 | /** 23 | * @dev Emitted when a loan is started and principal is distributed to the borrower. 24 | */ 25 | event LoanStarted(uint256 loanId, address lender, address borrower); 26 | 27 | /** 28 | * @dev Emitted when a loan is repaid by the borrower 29 | */ 30 | event LoanRepaid(uint256 loanId); 31 | 32 | /** 33 | * @dev Emitted when a loan collateral is claimed by the lender 34 | */ 35 | event LoanClaimed(uint256 loanId); 36 | 37 | /** 38 | * @dev Emitted when fees are claimed by admin 39 | */ 40 | event FeesClaimed(address token, address to, uint256 amount); 41 | 42 | /** 43 | * @dev Get LoanData by loanId 44 | */ 45 | function getLoan(uint256 loanId) external view returns (LoanLibrary.LoanData calldata loanData); 46 | 47 | /** 48 | * @dev Create store a loan object with some given terms 49 | */ 50 | function createLoan(LoanLibrary.LoanTerms calldata terms) external returns (uint256 loanId); 51 | 52 | /** 53 | * @dev Start a loan with the given borrower and lender 54 | * Distributes the principal less the protocol fee to the borrower 55 | * 56 | * Requirements: 57 | * - This function can only be called by a whitelisted OriginationController 58 | * - The proper principal and collateral must have been sent to this contract before calling. 59 | */ 60 | function startLoan( 61 | address lender, 62 | address borrower, 63 | uint256 loanId 64 | ) external; 65 | 66 | /** 67 | * @dev Repay the given loan 68 | * 69 | * Requirements: 70 | * - The caller must be a holder of the borrowerNote 71 | * - The caller must send in principal + interest 72 | * - The loan must be in state Active 73 | */ 74 | function repay(uint256 loanId) external; 75 | 76 | /** 77 | * @dev Claim the collateral of the given delinquent loan 78 | * 79 | * Requirements: 80 | * - The caller must be a holder of the lenderNote 81 | * - The loan must be in state Active 82 | * - The current time must be beyond the dueDate 83 | */ 84 | function claim(uint256 loanId) external; 85 | 86 | /** 87 | * @dev Getters for integrated contracts 88 | * 89 | */ 90 | function borrowerNote() external returns (IPromissoryNote); 91 | 92 | function lenderNote() external returns (IPromissoryNote); 93 | 94 | function collateralToken() external returns (IERC721); 95 | 96 | function feeController() external returns (IFeeController); 97 | } 98 | -------------------------------------------------------------------------------- /contracts/interfaces/IFlashRollover.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 4 | 5 | import "../external/interfaces/ILendingPool.sol"; 6 | import "./ILoanCore.sol"; 7 | import "./IOriginationController.sol"; 8 | import "./IRepaymentController.sol"; 9 | 10 | interface IFlashLoanReceiver { 11 | function executeOperation( 12 | address[] calldata assets, 13 | uint256[] calldata amounts, 14 | uint256[] calldata premiums, 15 | address initiator, 16 | bytes calldata params 17 | ) external returns (bool); 18 | 19 | // Function names defined by AAVE 20 | /* solhint-disable func-name-mixedcase */ 21 | function ADDRESSES_PROVIDER() external view returns (ILendingPoolAddressesProvider); 22 | 23 | function LENDING_POOL() external view returns (ILendingPool); 24 | /* solhint-enable func-name-mixedcase */ 25 | } 26 | 27 | interface IFlashRollover is IFlashLoanReceiver { 28 | event Rollover(address indexed lender, address indexed borrower, uint256 collateralTokenId, uint256 newLoanId); 29 | 30 | event Migration(address indexed oldLoanCore, address indexed newLoanCore, uint256 newLoanId); 31 | 32 | event SetOwner(address owner); 33 | 34 | /** 35 | * The contract references needed to roll 36 | * over the loan. Other dependent contracts 37 | * (asset wrapper, promissory notes) can 38 | * be fetched from the relevant LoanCore 39 | * contracts. 40 | */ 41 | struct RolloverContractParams { 42 | ILoanCore sourceLoanCore; 43 | ILoanCore targetLoanCore; 44 | IRepaymentController sourceRepaymentController; 45 | IOriginationController targetOriginationController; 46 | } 47 | 48 | /** 49 | * Holds parameters passed through flash loan 50 | * control flow that dictate terms of the new loan. 51 | * Contains a signature by lender for same terms. 52 | * isLegacy determines which loanCore to look for the 53 | * old loan in. 54 | */ 55 | struct OperationData { 56 | RolloverContractParams contracts; 57 | uint256 loanId; 58 | LoanLibrary.LoanTerms newLoanTerms; 59 | uint8 v; 60 | bytes32 r; 61 | bytes32 s; 62 | } 63 | 64 | /** 65 | * Defines the contracts that should be used for a 66 | * flash loan operation. May change based on if the 67 | * old loan is on the current loanCore or legacy (in 68 | * which case it requires migration). 69 | */ 70 | struct OperationContracts { 71 | ILoanCore loanCore; 72 | IERC721 borrowerNote; 73 | IERC721 lenderNote; 74 | IFeeController feeController; 75 | IERC721 assetWrapper; 76 | IRepaymentController repaymentController; 77 | IOriginationController originationController; 78 | ILoanCore targetLoanCore; 79 | IERC721 targetBorrowerNote; 80 | } 81 | 82 | function rolloverLoan( 83 | RolloverContractParams calldata contracts, 84 | uint256 loanId, 85 | LoanLibrary.LoanTerms calldata newLoanTerms, 86 | uint8 v, 87 | bytes32 r, 88 | bytes32 s 89 | ) external; 90 | 91 | function setOwner(address _owner) external; 92 | 93 | function flushToken(IERC20 token, address to) external; 94 | } 95 | -------------------------------------------------------------------------------- /contracts/vault/CallWhitelist.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "../interfaces/ICallWhitelist.sol"; 6 | 7 | /** 8 | * @title CallWhitelist 9 | * @notice Whitelist for calls that can be made from a vault 10 | * @dev This is intended to allow for "claim" functions to be called 11 | * while vault is being held in escrow as collateral. 12 | * @dev Note this contract has admin permissions, which grant the admin 13 | * the ability to add and remove contracts and functions from the whitelist 14 | */ 15 | contract CallWhitelist is Ownable, ICallWhitelist { 16 | // add some function selectors to the global blacklist 17 | // as-in clearly we shouldn't be able to just raw transfer assets out of the vault 18 | // without going through the normal process 19 | bytes4 private constant ERC20_TRANSFER = 0xa9059cbb; 20 | bytes4 private constant ERC20_ERC721_APPROVE = 0x095ea7b3; 21 | bytes4 private constant ERC20_ERC721_TRANSFER_FROM = 0x23b872dd; 22 | 23 | bytes4 private constant ERC721_SAFE_TRANSFER_FROM = 0x42842e0e; 24 | bytes4 private constant ERC721_SAFE_TRANSFER_FROM_DATA = 0xb88d4fde; 25 | bytes4 private constant ERC721_ERC1155_SET_APPROVAL = 0xa22cb465; 26 | 27 | bytes4 private constant ERC1155_SAFE_TRANSFER_FROM = 0xf242432a; 28 | bytes4 private constant ERC1155_SAFE_BATCH_TRANSFER_FROM = 0x2eb2c2d6; 29 | 30 | /** 31 | * @notice whitelist of callable functions on contracts 32 | * Maps address that can be called to function selectors which can be called on it 33 | * I.e. if we want to call 0x0000 on contract at 0x1111, mapping will have 34 | * whitelist[0x1111][0x0000] = true 35 | */ 36 | mapping(address => mapping(bytes4 => bool)) private whitelist; 37 | 38 | /** 39 | * @inheritdoc ICallWhitelist 40 | */ 41 | function isWhitelisted(address callee, bytes4 selector) external view override returns (bool) { 42 | return !isBlacklisted(selector) && whitelist[callee][selector]; 43 | } 44 | 45 | /** 46 | * @inheritdoc ICallWhitelist 47 | */ 48 | function add(address callee, bytes4 selector) external override onlyOwner { 49 | whitelist[callee][selector] = true; 50 | emit CallAdded(msg.sender, callee, selector); 51 | } 52 | 53 | /** 54 | * @inheritdoc ICallWhitelist 55 | */ 56 | function remove(address callee, bytes4 selector) external override onlyOwner { 57 | whitelist[callee][selector] = false; 58 | emit CallRemoved(msg.sender, callee, selector); 59 | } 60 | 61 | /** 62 | * Returns true if the given function selector is on the global blacklist, else false 63 | * @param selector the selector to check 64 | * @return true if blacklisted, else false 65 | */ 66 | function isBlacklisted(bytes4 selector) internal pure returns (bool) { 67 | return 68 | selector == ERC20_TRANSFER || 69 | selector == ERC20_ERC721_APPROVE || 70 | selector == ERC20_ERC721_TRANSFER_FROM || 71 | selector == ERC721_SAFE_TRANSFER_FROM || 72 | selector == ERC721_SAFE_TRANSFER_FROM_DATA || 73 | selector == ERC721_ERC1155_SET_APPROVAL || 74 | selector == ERC1155_SAFE_TRANSFER_FROM || 75 | selector == ERC1155_SAFE_BATCH_TRANSFER_FROM; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /contracts/vault/VaultFactory.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 5 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/proxy/Clones.sol"; 8 | 9 | import "../interfaces/IAssetVault.sol"; 10 | import "../interfaces/IVaultFactory.sol"; 11 | import "../ERC721Permit.sol"; 12 | 13 | /** @title VaultFactory 14 | * Factory for creating and registering AssetVaults 15 | * Note: TokenId is simply a uint representation of the vault address 16 | * To enable simple lookups from vault <-> tokenId 17 | */ 18 | contract VaultFactory is ERC721Enumerable, ERC721Permit, IVaultFactory { 19 | address public immutable template; 20 | address public immutable whitelist; 21 | 22 | constructor(address _template, address _whitelist) 23 | ERC721("Asset Wrapper V2", "AW-V2") 24 | ERC721Permit("Asset Wrapper V2") 25 | { 26 | require(_template != address(0), "VaultFactory: invalid template"); 27 | template = _template; 28 | whitelist = _whitelist; 29 | } 30 | 31 | /** 32 | * @inheritdoc IVaultFactory 33 | */ 34 | function isInstance(address instance) external view override returns (bool validity) { 35 | return _exists(uint256(uint160(instance))); 36 | } 37 | 38 | /** 39 | * @inheritdoc IVaultFactory 40 | */ 41 | function instanceCount() external view override returns (uint256 count) { 42 | return totalSupply(); 43 | } 44 | 45 | /** 46 | * @inheritdoc IVaultFactory 47 | */ 48 | function instanceAt(uint256 index) external view override returns (address instance) { 49 | return address(uint160(tokenByIndex(index))); 50 | } 51 | 52 | /** 53 | * @dev Creates a new bundle token for `to`. Its token ID will be 54 | * automatically assigned (and available on the emitted {IERC721-Transfer} event) 55 | * 56 | * See {ERC721-_mint}. 57 | */ 58 | function initializeBundle(address to) external override returns (uint256) { 59 | address vault = _create(); 60 | 61 | _mint(to, uint256(uint160(vault))); 62 | 63 | emit VaultCreated(vault, to); 64 | return uint256(uint160(vault)); 65 | } 66 | 67 | /** 68 | * @dev Creates and initializes a minimal proxy vault instance 69 | */ 70 | function _create() internal returns (address vault) { 71 | vault = Clones.clone(template); 72 | IAssetVault(vault).initialize(whitelist); 73 | return vault; 74 | } 75 | 76 | /** 77 | * @dev Hook that is called before any token transfer 78 | * @dev note this notifies the vault contract about the ownership transfer 79 | */ 80 | function _beforeTokenTransfer( 81 | address from, 82 | address to, 83 | uint256 tokenId 84 | ) internal virtual override(ERC721, ERC721Enumerable) { 85 | super._beforeTokenTransfer(from, to, tokenId); 86 | } 87 | 88 | /** 89 | * @dev See {IERC165-supportsInterface}. 90 | */ 91 | function supportsInterface(bytes4 interfaceId) 92 | public 93 | view 94 | virtual 95 | override(ERC721, ERC721Enumerable) 96 | returns (bool) 97 | { 98 | return super.supportsInterface(interfaceId); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /contracts/ERC721Permit.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity ^0.8.0; 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 | /** 12 | * @dev Implementation of the ERC721 Permit extension allowing approvals to be made via signatures, as defined in 13 | * https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. 14 | * 15 | * See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol 16 | * 17 | * Adds the {permit} method, which can be used to change an account's ERC721 allowance (see {IERC721-allowance}) by 18 | * presenting a message signed by the account. By not relying on `{IERC721-approve}`, the token holder account doesn't 19 | * need to send a transaction, and thus is not required to hold Ether at all. 20 | * 21 | * _Available since v3.4._ 22 | */ 23 | abstract contract ERC721Permit is ERC721, IERC721Permit, EIP712 { 24 | using Counters for Counters.Counter; 25 | 26 | mapping(address => Counters.Counter) private _nonces; 27 | 28 | // solhint-disable-next-line var-name-mixedcase 29 | bytes32 private immutable _PERMIT_TYPEHASH = 30 | keccak256("Permit(address owner,address spender,uint256 tokenId,uint256 nonce,uint256 deadline)"); 31 | 32 | /** 33 | * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. 34 | * 35 | * It's a good idea to use the same `name` that is defined as the ERC721 token name. 36 | */ 37 | constructor(string memory name) EIP712(name, "1") {} 38 | 39 | /** 40 | * @dev See {IERC721-permit}. 41 | */ 42 | function permit( 43 | address owner, 44 | address spender, 45 | uint256 tokenId, 46 | uint256 deadline, 47 | uint8 v, 48 | bytes32 r, 49 | bytes32 s 50 | ) public virtual override { 51 | require(block.timestamp <= deadline, "ERC721Permit: expired deadline"); 52 | require(owner == ERC721.ownerOf(tokenId), "ERC721Permit: not owner"); 53 | 54 | bytes32 structHash = keccak256( 55 | abi.encode(_PERMIT_TYPEHASH, owner, spender, tokenId, _useNonce(owner), deadline) 56 | ); 57 | 58 | bytes32 hash = _hashTypedDataV4(structHash); 59 | 60 | address signer = ECDSA.recover(hash, v, r, s); 61 | require(signer == owner, "ERC721Permit: invalid signature"); 62 | 63 | _approve(spender, tokenId); 64 | } 65 | 66 | /** 67 | * @dev See {IERC721Permit-nonces}. 68 | */ 69 | function nonces(address owner) public view virtual override returns (uint256) { 70 | return _nonces[owner].current(); 71 | } 72 | 73 | /** 74 | * @dev See {IERC721Permit-DOMAIN_SEPARATOR}. 75 | */ 76 | // solhint-disable-next-line func-name-mixedcase 77 | function DOMAIN_SEPARATOR() external view override returns (bytes32) { 78 | return _domainSeparatorV4(); 79 | } 80 | 81 | /** 82 | * @dev "Consume a nonce": return the current value and increment. 83 | * 84 | * _Available since v4.1._ 85 | */ 86 | function _useNonce(address owner) internal virtual returns (uint256 current) { 87 | Counters.Counter storage nonce = _nonces[owner]; 88 | current = nonce.current(); 89 | nonce.increment(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/utils/eip712.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 3 | import { BigNumberish } from "ethers"; 4 | import { LoanTerms } from "./types"; 5 | import { fromRpcSig, ECDSASignature } from "ethereumjs-util"; 6 | 7 | interface TypeData { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | types: any; 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | primaryType: any; 12 | } 13 | 14 | export interface PermitData { 15 | owner: string; 16 | spender: string; 17 | tokenId: BigNumberish; 18 | nonce: number; 19 | deadline: BigNumberish; 20 | } 21 | 22 | const typedPermitData: TypeData = { 23 | types: { 24 | Permit: [ 25 | { name: "owner", type: "address" }, 26 | { name: "spender", type: "address" }, 27 | { name: "tokenId", type: "uint256" }, 28 | { name: "nonce", type: "uint256" }, 29 | { name: "deadline", type: "uint256" }, 30 | ], 31 | }, 32 | primaryType: "Permit" as const, 33 | }; 34 | 35 | const typedLoanTermsData: TypeData = { 36 | types: { 37 | LoanTerms: [ 38 | { name: "durationSecs", type: "uint256" }, 39 | { name: "principal", type: "uint256" }, 40 | { name: "interest", type: "uint256" }, 41 | { name: "collateralTokenId", type: "uint256" }, 42 | { name: "payableCurrency", type: "address" }, 43 | ], 44 | }, 45 | primaryType: "LoanTerms" as const, 46 | }; 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 | const buildData = (verifyingContract: string, name: string, version: string, message: any, typeData: TypeData) => { 50 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 51 | const chainId = hre.network.config.chainId!; 52 | return Object.assign({}, typeData, { 53 | domain: { 54 | name, 55 | version, 56 | chainId, 57 | verifyingContract, 58 | }, 59 | message, 60 | }); 61 | }; 62 | 63 | /** 64 | * Create an EIP712 signature for loan terms 65 | * @param verifyingContract The address of the contract that will be verifying this signature 66 | * @param name The name of the contract that will be verifying this signature 67 | * @param terms the LoanTerms object to sign 68 | * @param signer The EOA to create the signature 69 | */ 70 | export async function createLoanTermsSignature( 71 | verifyingContract: string, 72 | name: string, 73 | terms: LoanTerms, 74 | signer: SignerWithAddress, 75 | ): Promise { 76 | const data = buildData(verifyingContract, name, "1", terms, typedLoanTermsData); 77 | 78 | const signature = await signer._signTypedData(data.domain, data.types, data.message); 79 | return fromRpcSig(signature); 80 | } 81 | 82 | /** 83 | * Create an EIP712 signature for ERC721 permit 84 | * @param verifyingContract The address of the contract that will be verifying this signature 85 | * @param name The name of the contract that will be verifying this signature 86 | * @param permitData the data of the permit to sign 87 | * @param signer The EOA to create the signature 88 | */ 89 | export async function createPermitSignature( 90 | verifyingContract: string, 91 | name: string, 92 | permitData: PermitData, 93 | signer: SignerWithAddress, 94 | ): Promise { 95 | const data = buildData(verifyingContract, name, "1", permitData, typedPermitData); 96 | 97 | const signature = await signer._signTypedData(data.domain, data.types, data.message); 98 | return fromRpcSig(signature); 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pawn-contracts", 3 | "description": "Smart contracts for the Pawn Finance protocol", 4 | "version": "1.0.0", 5 | "devDependencies": { 6 | "@aave/protocol-v2": "^1.0.1", 7 | "@commitlint/cli": "^9.1.2", 8 | "@commitlint/config-conventional": "^9.1.2", 9 | "@ethersproject/abstract-signer": "^5.0.6", 10 | "@ethersproject/bignumber": "^5.0.8", 11 | "@nomiclabs/hardhat-ethers": "^2.0.1", 12 | "@nomiclabs/hardhat-etherscan": "^2.1.7", 13 | "@nomiclabs/hardhat-waffle": "^2.0.1", 14 | "@openzeppelin/contracts": "^4.0.0", 15 | "@typechain/ethers-v5": "^5.0.0", 16 | "@typechain/hardhat": "^1.0.1", 17 | "@types/chai": "^4.2.13", 18 | "@types/fs-extra": "^9.0.1", 19 | "@types/mocha": "^7.0.2", 20 | "@types/node": "^14.11.8", 21 | "@typescript-eslint/eslint-plugin": "^3.10.1", 22 | "@typescript-eslint/parser": "^3.10.1", 23 | "chai": "^4.2.0", 24 | "commitizen": "^4.2.1", 25 | "cz-conventional-changelog": "^3.3.0", 26 | "dotenv": "^8.2.0", 27 | "eslint": "^7.11.0", 28 | "eslint-config-prettier": "^6.12.0", 29 | "ethereum-waffle": "^3.4.0", 30 | "ethereumjs-util": "^7.0.10", 31 | "ethers": "^5.0.32", 32 | "fs-extra": "^9.0.1", 33 | "hardhat": "^2.0.10", 34 | "hardhat-gas-reporter": "^1.0.4", 35 | "husky": "^4.3.0", 36 | "mocha": "^8.1.3", 37 | "prettier": "^2.1.2", 38 | "prettier-plugin-solidity": "^1.0.0-beta.1", 39 | "shelljs": "^0.8.4", 40 | "solc-0.8": "npm:solc@^0.8.5", 41 | "solhint": "^3.2.1", 42 | "solhint-plugin-prettier": "^0.0.5", 43 | "solidity-coverage": "^0.7.12", 44 | "solidity-docgen": "^0.5.13", 45 | "ts-generator": "^0.1.1", 46 | "ts-node": "^8.10.2", 47 | "typechain": "^4.0.1", 48 | "typescript": "<4.1.0" 49 | }, 50 | "resolutions": { 51 | "dot-prop": ">4.2.1", 52 | "elliptic": ">=6.5.4", 53 | "lodash": ">=4.17.21", 54 | "set-value": ">4.0.1", 55 | "underscore": ">=1.12.1", 56 | "yargs-parser": ">=5.0.1" 57 | }, 58 | "files": [ 59 | "/contracts" 60 | ], 61 | "keywords": [ 62 | "blockchain", 63 | "ethereum", 64 | "hardhat", 65 | "smart-contracts", 66 | "solidity" 67 | ], 68 | "license": "WTFPL", 69 | "publishConfig": { 70 | "access": "public" 71 | }, 72 | "scripts": { 73 | "clean": "hardhat clean", 74 | "commit": "git-cz", 75 | "compile": "hardhat compile", 76 | "coverage": "hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"./test/**/*.ts\"", 77 | "gendocs": "solidity-docgen -i ./contracts --solc-module solc-0.8", 78 | "lint": "yarn run lint:sol && yarn run lint:ts && yarn run prettier:list-different", 79 | "lint:fix": "yarn run prettier && yarn run lint:sol:fix && yarn run lint:ts:fix", 80 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 81 | "lint:sol:fix": "solhint --config ./.solhint.json --fix --max-warnings 0 \"contracts/**/*.sol\"", 82 | "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", 83 | "lint:ts:fix": "eslint --config ./.eslintrc.yaml --fix --ignore-path ./.eslintignore --ext .js,.ts .", 84 | "prettier": "prettier --config .prettierrc --write \"**/*.{js,json,md,sol,ts}\"", 85 | "prettier:list-different": "prettier --config .prettierrc --list-different \"**/*.{js,json,md,sol,ts}\"", 86 | "solc-0.8": "npm:solc@^0.8.0", 87 | "solidity-docgen": "^0.5.13", 88 | "test": "hardhat test", 89 | "typechain": "hardhat typechain", 90 | "bootstrap-with-loans": "npx hardhat --network localhost run scripts/bootstrap-state-with-loans.ts", 91 | "bootstrap-no-loans": "npx hardhat --network localhost run scripts/bootstrap-state-no-loans.ts" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /contracts/test/MockLoanCore.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/utils/Counters.sol"; 4 | 5 | import "../interfaces/ILoanCore.sol"; 6 | import "../interfaces/IPromissoryNote.sol"; 7 | 8 | import "../PromissoryNote.sol"; 9 | 10 | /** 11 | * @dev Interface for the LoanCore contract 12 | */ 13 | contract MockLoanCore is ILoanCore { 14 | using Counters for Counters.Counter; 15 | Counters.Counter private loanIdTracker; 16 | 17 | IPromissoryNote public override borrowerNote; 18 | IPromissoryNote public override lenderNote; 19 | IERC721 public override collateralToken; 20 | IFeeController public override feeController; 21 | 22 | mapping(uint256 => LoanLibrary.LoanData) public loans; 23 | 24 | constructor() { 25 | borrowerNote = new PromissoryNote("Mock BorrowerNote", "MB"); 26 | lenderNote = new PromissoryNote("Mock LenderNote", "ML"); 27 | 28 | // Avoid having loanId = 0 29 | loanIdTracker.increment(); 30 | } 31 | 32 | /** 33 | * @dev Get LoanData by loanId 34 | */ 35 | function getLoan(uint256 loanId) public view override returns (LoanLibrary.LoanData memory _loanData) { 36 | return loans[loanId]; 37 | } 38 | 39 | /** 40 | * @dev Create store a loan object with some given terms 41 | */ 42 | function createLoan(LoanLibrary.LoanTerms calldata terms) external override returns (uint256 loanId) { 43 | LoanLibrary.LoanTerms memory _loanTerms = LoanLibrary.LoanTerms( 44 | terms.durationSecs, 45 | terms.principal, 46 | terms.interest, 47 | terms.collateralTokenId, 48 | terms.payableCurrency 49 | ); 50 | 51 | LoanLibrary.LoanData memory _loanData = LoanLibrary.LoanData( 52 | 0, 53 | 0, 54 | _loanTerms, 55 | LoanLibrary.LoanState.Created, 56 | terms.durationSecs 57 | ); 58 | 59 | loanId = loanIdTracker.current(); 60 | loanIdTracker.increment(); 61 | 62 | loans[loanId] = _loanData; 63 | 64 | emit LoanCreated(terms, loanId); 65 | 66 | return loanId; 67 | } 68 | 69 | /** 70 | * @dev Start a loan with the given borrower and lender 71 | * Distributes the principal less the protocol fee to the borrower 72 | * 73 | * Requirements: 74 | * - This function can only be called by a whitelisted OriginationController 75 | * - The proper principal and collateral must have been sent to this contract before calling. 76 | */ 77 | function startLoan( 78 | address lender, 79 | address borrower, 80 | uint256 loanId 81 | ) public override { 82 | uint256 borrowerNoteId = borrowerNote.mint(borrower, loanId); 83 | uint256 lenderNoteId = lenderNote.mint(lender, loanId); 84 | 85 | LoanLibrary.LoanData memory data = loans[loanId]; 86 | loans[loanId] = LoanLibrary.LoanData( 87 | borrowerNoteId, 88 | lenderNoteId, 89 | data.terms, 90 | LoanLibrary.LoanState.Active, 91 | data.dueDate 92 | ); 93 | 94 | emit LoanStarted(loanId, lender, borrower); 95 | } 96 | 97 | /** 98 | * @dev Repay the given loan 99 | * 100 | * Requirements: 101 | * - The caller must be a holder of the borrowerNote 102 | * - The caller must send in principal + interest 103 | * - The loan must be in state Active 104 | */ 105 | function repay(uint256 loanId) public override { 106 | loans[loanId].state = LoanLibrary.LoanState.Repaid; 107 | emit LoanRepaid(loanId); 108 | } 109 | 110 | /** 111 | * @dev Claim the collateral of the given delinquent loan 112 | * 113 | * Requirements: 114 | * - The caller must be a holder of the lenderNote 115 | * - The loan must be in state Active 116 | * - The current time must be beyond the dueDate 117 | */ 118 | function claim(uint256 loanId) public override {} 119 | } 120 | -------------------------------------------------------------------------------- /docs/AssetWrapper.md: -------------------------------------------------------------------------------- 1 | # `AssetWrapper` 2 | 3 | The AssetWrapper contract is a generalized bundle 4 | mechanism for ERC20, ERC721, and ERC1155 assets. 5 | 6 | Users can create new bundles, which grants them an NFT to 7 | reclaim all assets stored in the bundle. They can then 8 | store various types of assets in that bundle. The bundle NFT 9 | can then be used or traded as an asset in its own right. 10 | At any time, the holder of the bundle NFT can redeem it for the 11 | underlying assets. 12 | 13 | ## API 14 | 15 | ### `initializeBundle(address to) →` _(external)_ 16 | 17 | Creates a new bundle token for `to`. Its token ID will be 18 | automatically assigned returned, and available on the emitted `Transfer` event. 19 | 20 | See [ERC721-\_safeMint](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721-_safeMint-address-uint256-). 21 | 22 | ### `depositERC20(address tokenAddress, uint256 amount, uint256 bundleId)` _(external)_ 23 | 24 | Deposit ERC20 tokens into a given bundle. 25 | 26 | Requirements: 27 | 28 | - The bundle with ID `bundleId` must have been initialized with `initializeBundle`. 29 | - The tokens for deposit must be approved for withdrawal by the 30 | `AssetWrapper` contract. 31 | 32 | Emits a `DepositERC20` event. 33 | 34 | ### `depositERC721(address tokenAddress, uint256 tokenId, uint256 bundleId)` _(external)_ 35 | 36 | Deposit an ERC721 token into a given bundle. 37 | 38 | Requirements: 39 | 40 | - The bundle with ID `bundleId` must have been initialized with `initializeBundle`. 41 | - The NFT for deposit must be approved for withdrawal by the 42 | `AssetWrapper` contract. 43 | 44 | Emits a `DepositERC721` event. 45 | 46 | ### `depositERC1155(address tokenAddress, uint256 tokenId, uint256 amount, uint256 bundleId)` _(external)_ 47 | 48 | Deposit an ERC1155 token into a given bundle. 49 | 50 | Requirements: 51 | 52 | - The bundle with ID `bundleId` must have been initialized with `initializeBundle`. 53 | - The NFT for deposit must be approved for withdrawal of `amount` by the 54 | `AssetWrapper` contract. 55 | 56 | Emits a `DepositERC1155` event. 57 | 58 | ### `depositETH(uint256 bundleId)` _(external)_ 59 | 60 | Deposit ETH into a given bundle. ETH should be sent in `msg.value`. 61 | 62 | Requirements: 63 | 64 | - The bundle with ID `bundleId` must have been initialized with `initializeBundle`. 65 | 66 | Emits a `DepositETH` event. 67 | 68 | ### `withdraw(uint256 bundleId)` _(external)_ 69 | 70 | Withdraw all assets in the given bundle, returning them to `msg.sender`. 71 | 72 | Requirements: 73 | 74 | - The bundle with ID `bundleId` must have been initialized with `initializeBundle`. 75 | - The bundle with ID `bundleId` must be owned by or approved to `msg.sender`. 76 | 77 | Emits a `Withdraw` event. 78 | 79 | ### `_beforeTokenTransfer(address from, address to, uint256 tokenId)` _(internal)_ 80 | 81 | Hook that is called before any token transfer. 82 | 83 | See [IERC721-\_beforeTokenTransfer](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721-_beforeTokenTransfer-address-address-uint256-). 84 | 85 | ### `supportsInterface(bytes4 interfaceId) → bool` _(public)_ 86 | 87 | See [IERC165-supportsInterface](https://docs.openzeppelin.com/contracts/3.x/api/introspection#IERC165-supportsInterface-bytes4-). 88 | 89 | ## Events 90 | 91 | ### `DepositERC20(address indexed depositor, uint256 indexed bundleId, address tokenAddress, uint256 amount)` 92 | 93 | Emitted when an ERC20 token is deposited to the specified `bundleId`. 94 | 95 | ### `DepositERC721(address indexed depositor, uint256 indexed bundleId, address tokenAddress, uint256 tokenId)` 96 | 97 | Emitted when an ERC721 token is deposited to the specified `bundleId`. 98 | 99 | ### `DepositERC1155(address indexed depositor, uint256 indexed bundleId, address tokenAddress, uint256 tokenId, uint256 amount)` 100 | 101 | Emitted when an ERC1155 token is deposited to the specified `bundleId`. 102 | 103 | ### `DepositETH(address indexed depositor, uint256 indexed bundleId, uint256 amount)` 104 | 105 | Emitted when ETH is deposited to the specified `bundleId`. 106 | 107 | ### `Withdraw(address indexed withdrawer, uint256 indexed bundleId)` 108 | 109 | Emitted when a bundle is unwrapped, transferring all bundled assets back to the owner. 110 | -------------------------------------------------------------------------------- /scripts/deploy.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import { ORIGINATOR_ROLE as DEFAULT_ORIGINATOR_ROLE, REPAYER_ROLE as DEFAULT_REPAYER_ROLE } from "./constants"; 4 | 5 | import { 6 | AssetVault, 7 | FeeController, 8 | LoanCore, 9 | PromissoryNote, 10 | RepaymentController, 11 | OriginationController, 12 | } from "../typechain"; 13 | export interface DeployedResources { 14 | assetVault: AssetVault; 15 | feeController: FeeController; 16 | loanCore: LoanCore; 17 | borrowerNote: PromissoryNote; 18 | lenderNote: PromissoryNote; 19 | repaymentController: RepaymentController; 20 | originationController: OriginationController; 21 | } 22 | 23 | export async function main( 24 | ORIGINATOR_ROLE = DEFAULT_ORIGINATOR_ROLE, 25 | REPAYER_ROLE = DEFAULT_REPAYER_ROLE, 26 | ): Promise { 27 | // Hardhat always runs the compile task when running scripts through it. 28 | // If this runs in a standalone fashion you may want to call compile manually 29 | // to make sure everything is compiled 30 | // await run("compile"); 31 | 32 | // We get the contract to deploy 33 | const AssetVaultFactory = await ethers.getContractFactory("AssetVault"); 34 | const assetVault = await AssetVaultFactory.deploy(); 35 | await assetVault.deployed(); 36 | 37 | console.log("AssetVault deployed to:", assetVault.address); 38 | 39 | const FeeControllerFactory = await ethers.getContractFactory("FeeController"); 40 | const feeController = await FeeControllerFactory.deploy(); 41 | await feeController.deployed(); 42 | 43 | console.log("FeeController deployed to: ", feeController.address); 44 | 45 | const LoanCoreFactory = await ethers.getContractFactory("LoanCore"); 46 | const loanCore = await LoanCoreFactory.deploy(assetVault.address, feeController.address); 47 | await loanCore.deployed(); 48 | 49 | const promissoryNoteFactory = await ethers.getContractFactory("PromissoryNote"); 50 | const borrowerNoteAddr = await loanCore.borrowerNote(); 51 | const borrowerNote = await promissoryNoteFactory.attach(borrowerNoteAddr); 52 | const lenderNoteAddr = await loanCore.lenderNote(); 53 | const lenderNote = await promissoryNoteFactory.attach(lenderNoteAddr); 54 | 55 | console.log("LoanCore deployed to:", loanCore.address); 56 | console.log("BorrowerNote deployed to:", borrowerNoteAddr); 57 | console.log("LenderNote deployed to:", lenderNoteAddr); 58 | 59 | const RepaymentControllerFactory = await ethers.getContractFactory("RepaymentController"); 60 | const repaymentController = ( 61 | await RepaymentControllerFactory.deploy(loanCore.address, borrowerNoteAddr, lenderNoteAddr) 62 | ); 63 | await repaymentController.deployed(); 64 | const updateRepaymentControllerPermissions = await loanCore.grantRole(REPAYER_ROLE, repaymentController.address); 65 | await updateRepaymentControllerPermissions.wait(); 66 | 67 | console.log("RepaymentController deployed to:", repaymentController.address); 68 | 69 | const OriginationControllerFactory = await ethers.getContractFactory("OriginationController"); 70 | const originationController = ( 71 | await OriginationControllerFactory.deploy(loanCore.address, assetVault.address) 72 | ); 73 | await originationController.deployed(); 74 | const updateOriginationControllerPermissions = await loanCore.grantRole( 75 | ORIGINATOR_ROLE, 76 | originationController.address, 77 | ); 78 | await updateOriginationControllerPermissions.wait(); 79 | 80 | console.log("OriginationController deployed to:", originationController.address); 81 | 82 | return { 83 | assetVault, 84 | feeController, 85 | loanCore, 86 | borrowerNote, 87 | lenderNote, 88 | repaymentController, 89 | originationController, 90 | }; 91 | } 92 | 93 | // We recommend this pattern to be able to use async/await everywhere 94 | // and properly handle errors. 95 | if (require.main === module) { 96 | main() 97 | .then(() => process.exit(0)) 98 | .catch((error: Error) => { 99 | console.error(error); 100 | process.exit(1); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /contracts/OriginationController.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/utils/Context.sol"; 4 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 5 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 7 | import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 8 | 9 | import "./interfaces/IOriginationController.sol"; 10 | import "./interfaces/ILoanCore.sol"; 11 | import "./interfaces/IERC721Permit.sol"; 12 | import "./interfaces/IAssetVault.sol"; 13 | import "./interfaces/IVaultFactory.sol"; 14 | 15 | contract OriginationController is Context, IOriginationController, EIP712 { 16 | using SafeERC20 for IERC20; 17 | address public immutable loanCore; 18 | address public immutable vaultFactory; 19 | 20 | // solhint-disable-next-line var-name-mixedcase 21 | bytes32 private immutable _LOAN_TERMS_TYPEHASH = 22 | keccak256( 23 | // solhint-disable-next-line max-line-length 24 | "LoanTerms(uint256 durationSecs,uint256 principal,uint256 interest,uint256 collateralTokenId,address payableCurrency)" 25 | ); 26 | 27 | constructor(address _loanCore, address _vaultFactory) EIP712("OriginationController", "1") { 28 | require(_loanCore != address(0), "Origination: loanCore not defined"); 29 | loanCore = _loanCore; 30 | vaultFactory = _vaultFactory; 31 | } 32 | 33 | /** 34 | * @inheritdoc IOriginationController 35 | */ 36 | function initializeLoan( 37 | LoanLibrary.LoanTerms calldata loanTerms, 38 | address borrower, 39 | address lender, 40 | uint8 v, 41 | bytes32 r, 42 | bytes32 s 43 | ) public override returns (uint256 loanId) { 44 | require(_msgSender() == lender || _msgSender() == borrower, "Origination: sender not participant"); 45 | // vault must be in withdraw-disabled state, 46 | // otherwise its unsafe as assets could have been withdrawn to frontrun this call 47 | require( 48 | !IAssetVault(address(uint160(loanTerms.collateralTokenId))).withdrawEnabled(), 49 | "Origination: withdraws enabled" 50 | ); 51 | 52 | bytes32 loanHash = keccak256( 53 | abi.encode( 54 | _LOAN_TERMS_TYPEHASH, 55 | loanTerms.durationSecs, 56 | loanTerms.principal, 57 | loanTerms.interest, 58 | loanTerms.collateralTokenId, 59 | loanTerms.payableCurrency 60 | ) 61 | ); 62 | bytes32 typedLoanHash = _hashTypedDataV4(loanHash); 63 | address externalSigner = ECDSA.recover(typedLoanHash, v, r, s); 64 | 65 | require(externalSigner == lender || externalSigner == borrower, "Origination: signer not participant"); 66 | require(externalSigner != _msgSender(), "Origination: approved own loan"); 67 | 68 | IERC20(loanTerms.payableCurrency).safeTransferFrom(lender, address(this), loanTerms.principal); 69 | IERC20(loanTerms.payableCurrency).approve(loanCore, loanTerms.principal); 70 | IERC721(vaultFactory).transferFrom(borrower, address(this), loanTerms.collateralTokenId); 71 | IERC721(vaultFactory).approve(loanCore, loanTerms.collateralTokenId); 72 | 73 | loanId = ILoanCore(loanCore).createLoan(loanTerms); 74 | ILoanCore(loanCore).startLoan(lender, borrower, loanId); 75 | } 76 | 77 | /** 78 | * @inheritdoc IOriginationController 79 | */ 80 | function initializeLoanWithCollateralPermit( 81 | LoanLibrary.LoanTerms calldata loanTerms, 82 | address borrower, 83 | address lender, 84 | uint8 v, 85 | bytes32 r, 86 | bytes32 s, 87 | uint8 collateralV, 88 | bytes32 collateralR, 89 | bytes32 collateralS, 90 | uint256 permitDeadline 91 | ) external override returns (uint256 loanId) { 92 | IERC721Permit(vaultFactory).permit( 93 | borrower, 94 | address(this), 95 | loanTerms.collateralTokenId, 96 | permitDeadline, 97 | collateralV, 98 | collateralR, 99 | collateralS 100 | ); 101 | 102 | loanId = initializeLoan(loanTerms, borrower, lender, v, r, s); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /scripts/redeploy-test-transfer.ts: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | import hre, { ethers } from "hardhat"; 4 | 5 | import { main as deploy } from "./redeploy-loancore"; 6 | import { main as transferOwnership } from "./transfer-ownership"; 7 | import { main as flashRolloverDeploy } from "./deploy-flash-rollover"; 8 | import { 9 | getBalance, 10 | deployNFTs, 11 | mintAndDistribute, 12 | SECTION_SEPARATOR, 13 | wrapAssetsAndMakeLoans, 14 | } from "./bootstrap-tools"; 15 | 16 | export async function main(): Promise { 17 | // Bootstrap five accounts only. 18 | // Skip the first account, since the 19 | // first signer will be the deployer. 20 | const allSigners = await ethers.getSigners(); 21 | const [deployer, ...signers] = allSigners.slice(0, 6); 22 | const adminAddress = process.env.ADMIN_ADDRESS || allSigners[10].address; 23 | 24 | console.log(SECTION_SEPARATOR); 25 | console.log("Deploying resources...\n"); 26 | 27 | // Deploy the smart contracts 28 | const { assetWrapper, originationController, repaymentController, borrowerNote, loanCore } = await deploy(); 29 | const { mockAddressProvider } = await flashRolloverDeploy(loanCore.address); 30 | const lendingPool = await mockAddressProvider.getLendingPool(); 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, lendingPool); 40 | 41 | // Wrap some assets 42 | console.log(SECTION_SEPARATOR); 43 | console.log("Wrapping assets...\n"); 44 | await wrapAssetsAndMakeLoans( 45 | signers, 46 | assetWrapper, 47 | originationController, 48 | borrowerNote, 49 | repaymentController, 50 | punks, 51 | usd, 52 | beats, 53 | weth, 54 | art, 55 | pawnToken, 56 | ); 57 | 58 | console.log("Transferring ownership...\n"); 59 | 60 | // Transfer ownership and try to withdraw fees 61 | await transferOwnership(loanCore.address, adminAddress); 62 | 63 | console.log("Testing permissions...\n"); 64 | 65 | // Try to have deployer withdraw 66 | try { 67 | await loanCore.connect(deployer.address).claimFees(weth.address); 68 | throw new Error(" Deployer fee claim did not revert!"); 69 | } catch (e) { 70 | if ((e as Error).message.includes("")) throw e; 71 | console.log("Deployer fee claim reverted."); 72 | } 73 | 74 | // Try to have deployer pause 75 | try { 76 | await loanCore.connect(deployer.address).pause(); 77 | throw new Error(" Deployer pause did not revert!"); 78 | } catch (e) { 79 | if ((e as Error).message.includes("")) throw e; 80 | console.log("Deployer pause reverted."); 81 | } 82 | 83 | // Have admin withdraw 84 | await deployer.sendTransaction({ 85 | value: ethers.utils.parseEther("1"), 86 | to: adminAddress, 87 | }); 88 | 89 | await hre.network.provider.request({ 90 | method: "hardhat_impersonateAccount", 91 | params: [adminAddress], 92 | }); 93 | const adminSigner = await ethers.getSigner(adminAddress); 94 | 95 | console.log(`Loan core balance pre-withdraw: ${await getBalance(weth, loanCore.address)}`); 96 | console.log(`Fee claimer balance pre-withdraw: ${await getBalance(weth, adminAddress)}`); 97 | await loanCore.connect(adminSigner).claimFees(weth.address); 98 | console.log(`Loan core balance post-withdraw: ${await getBalance(weth, loanCore.address)}`); 99 | console.log(`Fee claimer balance post-withdraw: ${await getBalance(weth, adminAddress)}`); 100 | console.log(`Admin successfully withdrew fees.`); 101 | 102 | // Have admin pause 103 | await loanCore.connect(adminSigner).pause(); 104 | await loanCore.connect(adminSigner).unpause(); 105 | console.log(`Admin successfully paused and unpaused contract.`); 106 | 107 | console.log(SECTION_SEPARATOR); 108 | console.log("Ownership transfer complete!"); 109 | console.log(SECTION_SEPARATOR); 110 | } 111 | 112 | // We recommend this pattern to be able to use async/await everywhere 113 | // and properly handle errors. 114 | if (require.main === module) { 115 | main() 116 | .then(() => process.exit(0)) 117 | .catch((error: Error) => { 118 | console.error(error); 119 | process.exit(1); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /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 | 7 | import "./tasks/accounts"; 8 | import "./tasks/clean"; 9 | 10 | import { resolve } from "path"; 11 | 12 | import { config as dotenvConfig } from "dotenv"; 13 | import { HardhatUserConfig } from "hardhat/config"; 14 | import { NetworkUserConfig, HardhatNetworkUserConfig } from "hardhat/types"; 15 | 16 | dotenvConfig({ path: resolve(__dirname, "./.env") }); 17 | 18 | const chainIds = { 19 | ganache: 1337, 20 | goerli: 5, 21 | hardhat: 1337, 22 | localhost: 31337, 23 | kovan: 42, 24 | mainnet: 1, 25 | rinkeby: 4, 26 | ropsten: 3, 27 | }; 28 | 29 | // Ensure that we have all the environment variables we need. 30 | let mnemonic: string; 31 | if (!process.env.MNEMONIC) { 32 | mnemonic = "test test test test test test test test test test test junk"; 33 | } else { 34 | mnemonic = process.env.MNEMONIC; 35 | } 36 | 37 | const forkMainnet = process.env.FORK_MAINNET === "true"; 38 | 39 | let alchemyApiKey: string | undefined; 40 | if (forkMainnet && !process.env.ALCHEMY_API_KEY) { 41 | throw new Error("Please set process.env.ALCHEMY_API_KEY"); 42 | } else { 43 | alchemyApiKey = process.env.ALCHEMY_API_KEY; 44 | } 45 | 46 | function createTestnetConfig(network: keyof typeof chainIds): NetworkUserConfig { 47 | const url = `https://eth-${network}.alchemyapi.io/v2/${alchemyApiKey}`; 48 | return { 49 | accounts: { 50 | count: 10, 51 | initialIndex: 0, 52 | mnemonic, 53 | path: "m/44'/60'/0'/0", 54 | }, 55 | chainId: chainIds[network], 56 | url, 57 | }; 58 | } 59 | 60 | function createHardhatConfig(): HardhatNetworkUserConfig { 61 | const config = { 62 | accounts: { 63 | mnemonic, 64 | }, 65 | chainId: chainIds.hardhat, 66 | }; 67 | 68 | if (forkMainnet) { 69 | return Object.assign(config, { 70 | forking: { 71 | url: `https://eth-mainnet.alchemyapi.io/v2/${alchemyApiKey}`, 72 | }, 73 | }); 74 | } 75 | 76 | return config; 77 | } 78 | 79 | function createMainnetConfig(): NetworkUserConfig { 80 | return { 81 | accounts: { 82 | mnemonic, 83 | }, 84 | chainId: chainIds.mainnet, 85 | url: `https://eth-mainnet.alchemyapi.io/v2/${alchemyApiKey}`, 86 | }; 87 | } 88 | 89 | const optimizerEnabled = process.env.DISABLE_OPTIMIZER ? false : true; 90 | 91 | const config: HardhatUserConfig = { 92 | defaultNetwork: "hardhat", 93 | gasReporter: { 94 | currency: "USD", 95 | enabled: process.env.REPORT_GAS ? true : false, 96 | excludeContracts: [], 97 | src: "./contracts", 98 | coinmarketcap: process.env.COINMARKETCAP_API_KEY, 99 | outputFile: process.env.REPORT_GAS_OUTPUT, 100 | }, 101 | networks: { 102 | mainnet: createMainnetConfig(), 103 | hardhat: createHardhatConfig(), 104 | goerli: createTestnetConfig("goerli"), 105 | kovan: createTestnetConfig("kovan"), 106 | rinkeby: createTestnetConfig("rinkeby"), 107 | ropsten: createTestnetConfig("ropsten"), 108 | localhost: { 109 | accounts: { 110 | mnemonic, 111 | }, 112 | chainId: chainIds.hardhat, 113 | gasMultiplier: 10, 114 | }, 115 | }, 116 | paths: { 117 | artifacts: "./artifacts", 118 | cache: "./cache", 119 | sources: "./contracts", 120 | tests: "./test", 121 | }, 122 | solidity: { 123 | compilers: [ 124 | { 125 | version: "0.8.5", 126 | settings: { 127 | metadata: { 128 | // Not including the metadata hash 129 | // https://github.com/paulrberg/solidity-template/issues/31 130 | bytecodeHash: "none", 131 | }, 132 | // You should disable the optimizer when debugging 133 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 134 | optimizer: { 135 | enabled: optimizerEnabled, 136 | runs: 999999, 137 | }, 138 | }, 139 | }, 140 | { 141 | version: "0.4.12", 142 | }, 143 | ], 144 | }, 145 | typechain: { 146 | outDir: "typechain", 147 | target: "ethers-v5", 148 | }, 149 | etherscan: { 150 | apiKey: process.env.ETHERSCAN_API_KEY, 151 | }, 152 | }; 153 | 154 | export default config; 155 | -------------------------------------------------------------------------------- /contracts/PromissoryNote.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.8.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol"; 4 | import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol"; 5 | import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; 6 | import "@openzeppelin/contracts/utils/Context.sol"; 7 | import "@openzeppelin/contracts/utils/Counters.sol"; 8 | import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; 9 | 10 | import "./ERC721Permit.sol"; 11 | import "./interfaces/ILoanCore.sol"; 12 | import "./interfaces/IPromissoryNote.sol"; 13 | 14 | /** 15 | * Built off Openzeppelin's ERC721PresetMinterPauserAutoId. 16 | * 17 | * @dev {ERC721} token, including: 18 | * 19 | * - ability for holders to burn (destroy) their tokens 20 | * - a minter role that allows for token minting (creation) 21 | * - token ID and URI autogeneration 22 | * 23 | * This contract uses {AccessControl} to lock permissioned functions using the 24 | * different roles - head to its documentation for details. 25 | * 26 | * The account that deploys the contract will be granted the minter and pauser 27 | * roles, as well as the default admin role, which will let it grant both minter 28 | * and pauser roles to other accounts. 29 | */ 30 | contract PromissoryNote is 31 | Context, 32 | AccessControlEnumerable, 33 | ERC721Enumerable, 34 | ERC721Pausable, 35 | ERC721Permit, 36 | IPromissoryNote 37 | { 38 | using Counters for Counters.Counter; 39 | 40 | bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); 41 | bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); 42 | bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); 43 | Counters.Counter private _tokenIdTracker; 44 | 45 | mapping(uint256 => uint256) public override loanIdByNoteId; 46 | 47 | /** 48 | * @dev Creates the borrowor note contract linked to a specific loan core 49 | * The loan core reference is non-upgradeable 50 | * See (_setURI). 51 | * Grants `PAUSER_ROLE`, `MINTER_ROLE`, and `BURNER_ROLE` to the sender 52 | * contract, provided it is an instance of LoanCore. 53 | * 54 | * Grants `DEFAULT_ADMIN_ROLE` to the account that deploys the contract. Admins 55 | 56 | */ 57 | 58 | constructor(string memory name, string memory symbol) ERC721(name, symbol) ERC721Permit(name) { 59 | _setupRole(BURNER_ROLE, _msgSender()); 60 | _setupRole(MINTER_ROLE, _msgSender()); 61 | _setupRole(PAUSER_ROLE, _msgSender()); 62 | 63 | // We don't want token IDs of 0 64 | _tokenIdTracker.increment(); 65 | } 66 | 67 | /** 68 | * @dev Creates a new token for `to`. Its token ID will be automatically 69 | * assigned (and available on the emitted {IERC721-Transfer} event), and the token 70 | * URI autogenerated based on the base URI passed at construction. 71 | * 72 | * See {ERC721-_mint}. 73 | * 74 | * Requirements: 75 | * 76 | * - the caller must have the `MINTER_ROLE`. 77 | */ 78 | function mint(address to, uint256 loanId) external override returns (uint256) { 79 | require(hasRole(MINTER_ROLE, _msgSender()), "ERC721PresetMinter: sending does have proper role"); 80 | 81 | uint256 currentTokenId = _tokenIdTracker.current(); 82 | _mint(to, currentTokenId); 83 | loanIdByNoteId[currentTokenId] = loanId; 84 | 85 | _tokenIdTracker.increment(); 86 | 87 | return currentTokenId; 88 | } 89 | 90 | /** 91 | * @dev Burns `tokenId`. See {ERC721-_burn}. 92 | * 93 | * Requirements: 94 | * 95 | * - The caller must own `tokenId` or be an approved operator. 96 | * The loan core contract can only burn a loan that is finished: 97 | * either repaid or claimed. 98 | * 99 | * 100 | */ 101 | function burn(uint256 tokenId) external override { 102 | require(hasRole(BURNER_ROLE, _msgSender()), "PromissoryNote: callers is not owner nor approved"); 103 | _burn(tokenId); 104 | loanIdByNoteId[tokenId] = 0; 105 | } 106 | 107 | /** 108 | * @dev override of supportsInterface for AccessControlEnumerable, ERC721, ERC721Enumerable 109 | */ 110 | function supportsInterface(bytes4 interfaceId) 111 | public 112 | view 113 | virtual 114 | override(AccessControlEnumerable, ERC721, ERC721Enumerable, IERC165) 115 | returns (bool) 116 | { 117 | return super.supportsInterface(interfaceId); 118 | } 119 | 120 | /** 121 | * @dev override of supportsInterface for ERC721, ERC721Enumerable, ERC721Pausable 122 | */ 123 | function _beforeTokenTransfer( 124 | address from, 125 | address to, 126 | uint256 amount 127 | ) internal virtual override(ERC721, ERC721Enumerable, ERC721Pausable) { 128 | super._beforeTokenTransfer(from, to, amount); 129 | 130 | require(!paused(), "ERC20Pausable: token transfer while paused"); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /scripts/redeploy-loancore.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "hardhat"; 2 | 3 | import { 4 | AssetVault, 5 | FeeController, 6 | LoanCore, 7 | PromissoryNote, 8 | RepaymentController, 9 | OriginationController, 10 | } from "../typechain"; 11 | 12 | import { ORIGINATOR_ROLE as DEFAULT_ORIGINATOR_ROLE, REPAYER_ROLE as DEFAULT_REPAYER_ROLE } from "./constants"; 13 | 14 | import { SECTION_SEPARATOR } from "./bootstrap-tools"; 15 | 16 | /** 17 | * October 2021: LoanCore Redeploy 18 | * This deploy addresses the issue of AssetWrapper re-use. 19 | * The following contracts need to be re-deployed for any LoanCore change: 20 | * - LoanCore 21 | * - BorrowerNote (implicit) 22 | * - LenderNote (implicit) 23 | * - OriginationController (LoanCore address is immutable) 24 | * - RepaymentController (LoanCore address is immutable) 25 | * 26 | */ 27 | 28 | export interface DeployedResources { 29 | assetWrapper: AssetVault; 30 | feeController: FeeController; 31 | loanCore: LoanCore; 32 | borrowerNote: PromissoryNote; 33 | lenderNote: PromissoryNote; 34 | repaymentController: RepaymentController; 35 | originationController: OriginationController; 36 | } 37 | 38 | export async function main( 39 | ORIGINATOR_ROLE = DEFAULT_ORIGINATOR_ROLE, 40 | REPAYER_ROLE = DEFAULT_REPAYER_ROLE, 41 | ASSET_WRAPPER_ADDRESS = "0x1F563CDd688ad47b75E474FDe74E87C643d129b7", 42 | FEE_CONTROLLER_ADDRESS = "0xfc2b8D5C60c8E8BbF8d6dc685F03193472e39587", 43 | ): Promise { 44 | console.log(SECTION_SEPARATOR); 45 | const signers = await ethers.getSigners(); 46 | console.log("Deployer address: ", signers[0].address); 47 | console.log("Deployer balance: ", (await signers[0].getBalance()).toString()); 48 | console.log(SECTION_SEPARATOR); 49 | 50 | // Hardhat always runs the compile task when running scripts through it. 51 | // If this runs in a standalone fashion you may want to call compile manually 52 | // to make sure everything is compiled 53 | // await run("compile"); 54 | 55 | // Attach to existing contracts 56 | const AssetWrapperFactory = await ethers.getContractFactory("AssetWrapper"); 57 | const assetWrapper = await AssetWrapperFactory.attach(ASSET_WRAPPER_ADDRESS); 58 | 59 | const FeeControllerFactory = await ethers.getContractFactory("FeeController"); 60 | const feeController = await FeeControllerFactory.attach(FEE_CONTROLLER_ADDRESS); 61 | 62 | // Start deploying new contracts 63 | const LoanCoreFactory = await ethers.getContractFactory("LoanCore"); 64 | const loanCore = await LoanCoreFactory.deploy(ASSET_WRAPPER_ADDRESS, FEE_CONTROLLER_ADDRESS); 65 | await loanCore.deployed(); 66 | 67 | const promissoryNoteFactory = await ethers.getContractFactory("PromissoryNote"); 68 | const borrowerNoteAddr = await loanCore.borrowerNote(); 69 | const borrowerNote = await promissoryNoteFactory.attach(borrowerNoteAddr); 70 | const lenderNoteAddr = await loanCore.lenderNote(); 71 | const lenderNote = await promissoryNoteFactory.attach(lenderNoteAddr); 72 | 73 | console.log("LoanCore deployed to:", loanCore.address); 74 | console.log("BorrowerNote deployed to:", borrowerNoteAddr); 75 | console.log("LenderNote deployed to:", lenderNoteAddr); 76 | 77 | const RepaymentControllerFactory = await ethers.getContractFactory("RepaymentController"); 78 | const repaymentController = ( 79 | await RepaymentControllerFactory.deploy(loanCore.address, borrowerNoteAddr, lenderNoteAddr) 80 | ); 81 | await repaymentController.deployed(); 82 | const updateRepaymentControllerPermissions = await loanCore.grantRole(REPAYER_ROLE, repaymentController.address); 83 | await updateRepaymentControllerPermissions.wait(); 84 | 85 | console.log("RepaymentController deployed to:", repaymentController.address); 86 | 87 | const OriginationControllerFactory = await ethers.getContractFactory("OriginationController"); 88 | const originationController = ( 89 | await OriginationControllerFactory.deploy(loanCore.address, ASSET_WRAPPER_ADDRESS) 90 | ); 91 | await originationController.deployed(); 92 | const updateOriginationControllerPermissions = await loanCore.grantRole( 93 | ORIGINATOR_ROLE, 94 | originationController.address, 95 | ); 96 | await updateOriginationControllerPermissions.wait(); 97 | 98 | console.log("OriginationController deployed to:", originationController.address); 99 | 100 | return { 101 | assetWrapper, 102 | feeController, 103 | loanCore, 104 | borrowerNote, 105 | lenderNote, 106 | repaymentController, 107 | originationController, 108 | }; 109 | } 110 | 111 | // We recommend this pattern to be able to use async/await everywhere 112 | // and properly handle errors. 113 | if (require.main === module) { 114 | main() 115 | .then(() => process.exit(0)) 116 | .catch((error: Error) => { 117 | console.error(error); 118 | process.exit(1); 119 | }); 120 | } 121 | -------------------------------------------------------------------------------- /contracts/interfaces/IAssetVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | import "./ICallWhitelist.sol"; 3 | 4 | pragma solidity ^0.8.0; 5 | 6 | /** 7 | * @dev Interface for an AssetVault contract 8 | */ 9 | interface IAssetVault { 10 | /** 11 | * @dev Emitted when withdraws are enabled on the vault 12 | */ 13 | event WithdrawEnabled(address operator); 14 | 15 | /** 16 | * @dev Emitted when an ERC20 token is withdrawn 17 | */ 18 | event WithdrawERC20(address indexed operator, address indexed token, address recipient, uint256 amount); 19 | 20 | /** 21 | * @dev Emitted when an ERC721 token is withdrawn 22 | */ 23 | event WithdrawERC721(address indexed operator, address indexed token, address recipient, uint256 tokenId); 24 | 25 | /** 26 | * @dev Emitted when an ERC1155 token is withdrawn 27 | */ 28 | event WithdrawERC1155( 29 | address indexed operator, 30 | address indexed token, 31 | address recipient, 32 | uint256 tokenId, 33 | uint256 amount 34 | ); 35 | 36 | /** 37 | * @dev Emitted when ETH is withdrawn 38 | */ 39 | event WithdrawETH(address indexed operator, address indexed recipient, uint256 amount); 40 | 41 | /** 42 | * @dev Emitted when an external call is made from the vault 43 | */ 44 | event Call(address indexed operator, address indexed to, bytes data); 45 | 46 | /** 47 | * @dev Sets up the vault 48 | * @param _whitelist The whitelist contract which decides what external calls are valid 49 | */ 50 | function initialize(address _whitelist) external; 51 | 52 | /** 53 | * @dev Return true if withdrawing is enabled on the vault 54 | * @dev if false, the vault can only receive deposits, else it can also withdraw 55 | * @dev Any integration should be aware that a vault with withdraw enabled is not safe to use as collateral 56 | * as the held assets may be withdrawn without notice, i.e. to frontrun a deposit 57 | */ 58 | function withdrawEnabled() external view returns (bool); 59 | 60 | /** 61 | * @dev Return the contract being used to whitelist function calls 62 | */ 63 | function whitelist() external view returns (ICallWhitelist); 64 | 65 | /** 66 | * @dev Enables withdrawals on the vault 67 | * @dev Any integration should be aware that a withdraw-enabled vault is not safe to use as collateral 68 | * as the held assets may be withdrawn without notice, i.e. to frontrun a deposit 69 | * 70 | * 71 | * Requirements: 72 | * 73 | * - Caller must be the owner of the tracking NFT 74 | */ 75 | function enableWithdraw() external; 76 | 77 | /** 78 | * @dev Withdraw entire balance of a given ERC20 token from the vault 79 | * @param token The ERC20 token to withdraw 80 | * @param to the recipient of the withdrawn funds 81 | * 82 | * Requirements: 83 | * 84 | * - The vault must be in closed state 85 | * - The caller must be the owner 86 | */ 87 | function withdrawERC20(address token, address to) external; 88 | 89 | /** 90 | * @dev Withdraw an ERC721 token from the vault 91 | * @param token The token to withdraw 92 | * @param tokenId The id of the NFT to withdraw 93 | * @param to The recipient of the withdrawn token 94 | * 95 | * Requirements: 96 | * 97 | * - The vault must be in closed state 98 | * - The caller must be the owner 99 | * - token must exist and be owned by this contract 100 | */ 101 | function withdrawERC721( 102 | address token, 103 | uint256 tokenId, 104 | address to 105 | ) external; 106 | 107 | /** 108 | * @dev Withdraw entire balance of an ERC1155 token from the vault 109 | * @param token The token to withdraw 110 | * @param tokenId The id of the token to withdraw 111 | * @param to The recipient of the withdrawn token 112 | * 113 | * Requirements: 114 | * 115 | * - The vault must be in closed state 116 | * - The caller must be the owner 117 | */ 118 | function withdrawERC1155( 119 | address token, 120 | uint256 tokenId, 121 | address to 122 | ) external; 123 | 124 | /** 125 | * @dev Withdraw entire balance of ETH from the vault 126 | * 127 | * Requirements: 128 | * 129 | * - The vault must be in closed state 130 | * - The caller must be the owner 131 | */ 132 | function withdrawETH(address to) external; 133 | 134 | /** 135 | * @dev Call a function on an external contract 136 | * @dev This is intended for claiming airdrops while the vault is being used as collateral 137 | * @param to The contract address to call 138 | * @param data The data to call the contract with 139 | * 140 | * Requirements: 141 | * 142 | * - The vault must be in closed state 143 | * - The caller must either be the owner, or the owner must have explicitly 144 | * delegated this ability to the caller through ICallDelegator interface 145 | * - The call must be in the whitelist 146 | */ 147 | function call(address to, bytes memory data) external; 148 | } 149 | -------------------------------------------------------------------------------- /contracts/vault/AssetVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; 6 | import "@openzeppelin/contracts/utils/Address.sol"; 7 | import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; 8 | import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; 9 | import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; 10 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 11 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; 12 | import "../interfaces/ICallWhitelist.sol"; 13 | import "../interfaces/ICallDelegator.sol"; 14 | import "../interfaces/IAssetVault.sol"; 15 | import "./OwnableERC721.sol"; 16 | 17 | /// @title AssetVault 18 | /// @notice Vault for isolated storage of collateral tokens 19 | /// @dev Note this is a one-time use vault. 20 | /// It starts in a deposit-only state. Funds cannot be withdrawn at this point 21 | /// When the owner calls "enableWithdraw()", the state is set to a withdrawEnabled state 22 | /// Withdraws cannot be disabled once enabled 23 | /// This restriction protects integrations and purchasers of AssetVaults from unexpected withdrawal 24 | /// I.e. Someone buys an AV assuming it contains token X, but I withdraw token X right before the sale concludes 25 | /// @dev note that there is an arbitrary external call which can be made by either: 26 | /// - the current owner of the vault 27 | /// - someone who the current owner "delegates" through the ICallDelegator interface 28 | /// This is to enable airdrop claims by borrowers during loans. 29 | contract AssetVault is IAssetVault, OwnableERC721, Initializable, ERC1155Holder, ERC721Holder, ReentrancyGuard { 30 | using Address for address; 31 | using Address for address payable; 32 | using SafeERC20 for IERC20; 33 | 34 | // True if withdrawals are allowed out of this vault 35 | // Note once set to true, it cannot be reverted back to false 36 | bool public override withdrawEnabled; 37 | 38 | // Whitelist contract to determine if a given external call is allowed 39 | ICallWhitelist public override whitelist; 40 | 41 | modifier onlyWithdrawEnabled() { 42 | require(withdrawEnabled, "AssetVault: withdraws disabled"); 43 | _; 44 | } 45 | 46 | modifier onlyWithdrawDisabled() { 47 | require(!withdrawEnabled, "AssetVault: withdraws enabled"); 48 | _; 49 | } 50 | 51 | /** 52 | * @dev initialize values so initialize cannot be called on template 53 | */ 54 | constructor() { 55 | withdrawEnabled = true; 56 | OwnableERC721._setNFT(msg.sender); 57 | } 58 | 59 | /** 60 | * @dev Function to initialize the contract 61 | */ 62 | function initialize(address _whitelist) external override initializer { 63 | require(!withdrawEnabled && ownershipToken == address(0), "AssetVault: Already initialized"); 64 | // set ownership to inherit from the factory who deployed us 65 | // The factory should have a tokenId == uint256(address(this)) 66 | // whose owner has ownership control over this contract 67 | OwnableERC721._setNFT(msg.sender); 68 | whitelist = ICallWhitelist(_whitelist); 69 | } 70 | 71 | receive() external payable {} 72 | 73 | /** 74 | * @inheritdoc OwnableERC721 75 | */ 76 | function owner() public view override returns (address ownerAddress) { 77 | return OwnableERC721.owner(); 78 | } 79 | 80 | /** 81 | * @inheritdoc IAssetVault 82 | */ 83 | function enableWithdraw() external override onlyOwner onlyWithdrawDisabled { 84 | withdrawEnabled = true; 85 | emit WithdrawEnabled(msg.sender); 86 | } 87 | 88 | /** Withdrawal functions */ 89 | 90 | /** 91 | * @inheritdoc IAssetVault 92 | */ 93 | function withdrawERC20(address token, address to) external override onlyOwner onlyWithdrawEnabled { 94 | uint256 balance = IERC20(token).balanceOf(address(this)); 95 | IERC20(token).safeTransfer(to, balance); 96 | emit WithdrawERC20(msg.sender, token, to, balance); 97 | } 98 | 99 | /** 100 | * @inheritdoc IAssetVault 101 | */ 102 | function withdrawERC721( 103 | address token, 104 | uint256 tokenId, 105 | address to 106 | ) external override onlyOwner onlyWithdrawEnabled { 107 | IERC721(token).safeTransferFrom(address(this), to, tokenId); 108 | emit WithdrawERC721(msg.sender, token, to, tokenId); 109 | } 110 | 111 | /** 112 | * @inheritdoc IAssetVault 113 | */ 114 | function withdrawERC1155( 115 | address token, 116 | uint256 tokenId, 117 | address to 118 | ) external override onlyOwner onlyWithdrawEnabled { 119 | uint256 balance = IERC1155(token).balanceOf(address(this), tokenId); 120 | IERC1155(token).safeTransferFrom(address(this), to, tokenId, balance, ""); 121 | emit WithdrawERC1155(msg.sender, token, to, tokenId, balance); 122 | } 123 | 124 | /** 125 | * @inheritdoc IAssetVault 126 | */ 127 | function withdrawETH(address to) external override onlyOwner onlyWithdrawEnabled nonReentrant { 128 | // perform transfer 129 | uint256 balance = address(this).balance; 130 | payable(to).sendValue(balance); 131 | emit WithdrawETH(msg.sender, to, balance); 132 | } 133 | 134 | /** 135 | * @inheritdoc IAssetVault 136 | */ 137 | function call(address to, bytes calldata data) external override onlyWithdrawDisabled nonReentrant { 138 | require( 139 | msg.sender == owner() || ICallDelegator(owner()).canCallOn(msg.sender, address(this)), 140 | "AssetVault: call disallowed" 141 | ); 142 | require(whitelist.isWhitelisted(to, bytes4(data[:4])), "AssetVault: non-whitelisted call"); 143 | 144 | to.functionCall(data); 145 | emit Call(msg.sender, to, data); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /docs/LoanCore.md: -------------------------------------------------------------------------------- 1 | # `LoanCore` 2 | 3 | LoanCore contract - core contract for creating, repaying, and claiming collateral for PawnFi loans. 4 | 5 | ## Data Types 6 | 7 | ``` 8 | /** 9 | * Enum describing the current state of a loan. 10 | * State change flow: 11 | * Created -> Active -> Repaid 12 | * -> Defaulted 13 | */ 14 | enum LoanState { 15 | // We need a default that is not 'Created' - this is the zero value 16 | DUMMY_DO_NOT_USE, 17 | // The loan data is stored, but not initiated yet. 18 | Created, 19 | // The loan has been initialized, funds have been delivered to the borrower and the collateral is held. 20 | Active, 21 | // The loan has been repaid, and the collateral has been returned to the borrower. This is a terminal state. 22 | Repaid, 23 | // The loan was delinquent and collateral claimed by the lender. This is a terminal state. 24 | Defaulted 25 | } 26 | 27 | /** 28 | * The raw terms of a loan 29 | */ 30 | struct LoanTerms { 31 | // The number of seconds representing relative due date of the loan 32 | uint256 durationSecs; 33 | // The amount of principal in terms of the payableCurrency 34 | uint256 principal; 35 | // The amount of interest in terms of the payableCurrency 36 | uint256 interest; 37 | // The tokenID of the collateral bundle 38 | uint256 collateralTokenId; 39 | // The payable currency for the loan principal and interest 40 | address payableCurrency; 41 | } 42 | 43 | /** 44 | * The data of a loan. This is stored once the loan is Active 45 | */ 46 | struct LoanData { 47 | // The tokenId of the borrower note 48 | uint256 borrowerNoteId; 49 | // The tokenId of the lender note 50 | uint256 lenderNoteId; 51 | // The raw terms of the loan 52 | LoanTerms terms; 53 | // The current state of the loan 54 | LoanState state; 55 | // Timestamp representing absolute due date date of the loan 56 | uint256 dueDate; 57 | } 58 | ``` 59 | 60 | ## API 61 | 62 | ### `constructor(contract IERC721 _collateralToken, contract IFeeController _feeController)` 63 | 64 | Create the `LoanCore` contract. Requires references to `_collateralToken` (an instance of `AssetWrapper`) 65 | and `_feeController` (an instance of `FeeController`). 66 | 67 | The constructor will grant `DEFAULT_ADMIN_ROLE` and `FEE_CLAIMER_ROLE` to the deployer. It will also 68 | deploy two instances of `PromissoryNote` - one for the `borrowerNote` and one for the `lenderNote`. 69 | 70 | ### `getLoan(uint256 loanId) → struct LoanData loanData` _(external)_ 71 | 72 | Get LoanData by loanId (see `LoanData` struct definitiona above). 73 | 74 | ### `createLoan(struct LoanTerms terms) → uint256 loanId` _(external)_ 75 | 76 | Create a loan object with the given terms. This function created a loan record 77 | in memory and reserves the collateral so it cannot be used by other loans, but 78 | does not start the loan or issue principal. 79 | 80 | Can only be called by `ORIGINATOR_ROLE` (should be an instance of `OriginationController`). 81 | 82 | Emits a `LoanCreated` event. 83 | 84 | ### `startLoan(address lender, address borrower, uint256 loanId)` _(external)_ 85 | 86 | Start a loan with the given borrower and lender, using the terms of the 87 | `loanId` already instantiated in `createLoan`. 88 | 89 | `LoanCore` will withdraw the `collateralToken` and `principal` from the caller, 90 | who should already have collected those assets from borrower and lender, 91 | and approved `LoanCore` for withdrawal. 92 | 93 | Distributes the principal less the protocol fee to the borrower. 94 | 95 | Requirements: 96 | 97 | - Can only be called by `ORIGINATOR_ROLE` (should be an instance of `OriginationController`). 98 | 99 | Emits a `LoanStarted` event. 100 | 101 | ### `repay(uint256 loanId)` _(external)_ 102 | 103 | Repay the given loan for the specified `loanId`. 104 | 105 | `LoanCore` will withdraw the repayment amount (principal + interest) from 106 | the caller, which should already have collected those assets from the borrower. 107 | 108 | The repaid tokens will be distributed to the lender, and the collateral token 109 | redistributed the borrower. 110 | 111 | On a completed loan repayment the corresponding `LenderNote` and `BorrowerNote` 112 | tokens for the loan are burned. 113 | 114 | Requirements: 115 | 116 | - Can only be called by `REPAYER_ROLE` (should be an instance of `RepaymentController`). 117 | - The loan must be in state `Active`. 118 | - The caller must have approved `LoanCore` to withdraw tokens. 119 | 120 | Emits a `LoanRepaid` event. 121 | 122 | ### `claim(uint256 loanId)` _(external)_ 123 | 124 | Claim the collateral of the given loan, as long as the loan has not been repaid by the 125 | due date. 126 | 127 | The collateral token will be distributed to the lender, and the `LenderNote` and 128 | `BorrowerNote` burned. 129 | 130 | Requirements: 131 | 132 | - Can only be called by `REPAYER_ROLE` (should be an instance of `RepaymentController`). 133 | - The loan must be in state `Active`. 134 | - The current time must be beyond the `dueDate` of the loan. 135 | 136 | Emits a `LoanClaimed` event. 137 | 138 | ### `getPrincipalLessFees(uint256 principal) → uint256` _(internal)_ 139 | 140 | Take a principal value and return the amount less protocol fees. Reads from 141 | `FeeController` to get the current origination fee value. 142 | 143 | ### `setFeeController(contract IFeeController _newController)` _(external)_ 144 | 145 | Set the fee controller to a new value. The new argument must support 146 | the `FeeController` interface. 147 | 148 | Requirements: 149 | 150 | - Can only be called by `FEE_CLAIMER_ROLE`. 151 | 152 | ### `claimFees(contract IERC20 token)` _(external)_ 153 | 154 | Claim the protocol fees for the given token. All fees will be withdrawn 155 | to the caller. 156 | 157 | Requirements: 158 | 159 | - Can only be called by `FEE_CLAIMER_ROLE`. 160 | 161 | Emits a `FeesClaimed` event. 162 | 163 | ## Events 164 | 165 | ### `Initialized(address collateralToken, address borrowerNote, address lenderNote)` 166 | 167 | Emitted on contract deployment to expose location of dependent contracts. 168 | 169 | ### `LoanCreated(LoanLibrary.LoanTerms terms, uint256 loanId)` 170 | 171 | Emitted when a loan is created, but not yet started. Exposes terms and unique ID of loan. 172 | 173 | ### `LoanStarted(uint256 loanId, address lender, address borrower)` 174 | 175 | Emitted when a loan is started and principal is distributed to the borrower. 176 | 177 | ### `LoanRepaid(uint256 loanId)` 178 | 179 | Emitted when a loan is repaid by the borrower. 180 | 181 | ### `LoanClaimed(uint256 loanId)` 182 | 183 | Emitted when a loan in default has collateral claimed by the lender. 184 | 185 | ### `FeesClaimed(address token, address to, uint256 amount)` 186 | 187 | Emitted when protocol fees are withdrawn from the contract. 188 | -------------------------------------------------------------------------------- /test/PunkRouter.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import hre from "hardhat"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 4 | import { BigNumber } from "ethers"; 5 | 6 | import { VaultFactory, CallWhitelist, AssetVault, PunkRouter, CryptoPunksMarket, WrappedPunk } from "../typechain"; 7 | import { deploy } from "./utils/contracts"; 8 | 9 | type Signer = SignerWithAddress; 10 | 11 | interface TestContext { 12 | assetWrapper: VaultFactory; 13 | punkRouter: PunkRouter; 14 | punks: CryptoPunksMarket; 15 | wrappedPunks: WrappedPunk; 16 | user: Signer; 17 | other: Signer; 18 | signers: Signer[]; 19 | } 20 | 21 | interface TestContextForDepositStuck { 22 | owner: Signer; 23 | other: Signer; 24 | punks: CryptoPunksMarket; 25 | punkIndex: number; 26 | punkRouter: PunkRouter; 27 | } 28 | 29 | describe("PunkRouter", () => { 30 | /** 31 | * Sets up a test context, deploying new contracts and returning them for use in a test 32 | */ 33 | const setupTestContext = async (): Promise => { 34 | const signers: Signer[] = await hre.ethers.getSigners(); 35 | const punks = await deploy("CryptoPunksMarket", signers[0], []); 36 | const wrappedPunks = await deploy("WrappedPunk", signers[0], [punks.address]); 37 | const whitelist = await deploy("CallWhitelist", signers[0], []); 38 | const vaultTemplate = await deploy("AssetVault", signers[0], []); 39 | const assetWrapper = ( 40 | await deploy("VaultFactory", signers[0], [vaultTemplate.address, whitelist.address]) 41 | ); 42 | const punkRouter = ( 43 | await deploy("PunkRouter", signers[0], [assetWrapper.address, wrappedPunks.address, punks.address]) 44 | ); 45 | 46 | return { 47 | assetWrapper, 48 | punks, 49 | wrappedPunks, 50 | punkRouter, 51 | user: signers[0], 52 | other: signers[1], 53 | signers: signers.slice(2), 54 | }; 55 | }; 56 | 57 | const setupTestContextForDepositStuck = async (): Promise => { 58 | const { punks, punkRouter, user, other } = await setupTestContext(); 59 | const punkIndex = 1234; 60 | // claim ownership of punk 61 | await punks.setInitialOwner(await user.getAddress(), punkIndex); 62 | await punks.allInitialOwnersAssigned(); 63 | // simulate depositPunk and stucked after buyPunk 64 | await punks.connect(user).transferPunk(punkRouter.address, punkIndex); 65 | return { 66 | owner: user, 67 | other, 68 | punkIndex, 69 | punks, 70 | punkRouter, 71 | }; 72 | }; 73 | 74 | /** 75 | * Initialize a new bundle, returning the bundleId 76 | */ 77 | const initializeBundle = async (assetWrapper: VaultFactory, user: Signer): Promise => { 78 | const tx = await assetWrapper.initializeBundle(await user.getAddress()); 79 | const receipt = await tx.wait(); 80 | if (receipt && receipt.events) { 81 | for (const event of receipt.events) { 82 | if (event.event && event.event === "VaultCreated" && event.args && event.args.vault) { 83 | return event.args.vault; 84 | } 85 | } 86 | throw new Error("Unable to initialize bundle"); 87 | } else { 88 | throw new Error("Unable to initialize bundle"); 89 | } 90 | }; 91 | 92 | describe("Deposit CryptoPunk", function () { 93 | it("should successfully deposit a cryptopunk into bundle", async () => { 94 | const { assetWrapper, punks, wrappedPunks, punkRouter, user } = await setupTestContext(); 95 | const punkIndex = 1234; 96 | // claim ownership of punk 97 | await punks.setInitialOwner(await user.getAddress(), punkIndex); 98 | await punks.allInitialOwnersAssigned(); 99 | // "approve" the punk to the router 100 | await punks.offerPunkForSaleToAddress(punkIndex, 0, punkRouter.address); 101 | 102 | const bundleId = await initializeBundle(assetWrapper, user); 103 | await expect(punkRouter.depositPunk(punkIndex, bundleId)) 104 | .to.emit(wrappedPunks, "Transfer") 105 | .withArgs(punkRouter.address, bundleId, punkIndex); 106 | 107 | expect(await wrappedPunks.ownerOf(punkIndex)).to.equal(bundleId); 108 | }); 109 | 110 | it("should fail if not approved", async () => { 111 | const { assetWrapper, punks, punkRouter, user } = await setupTestContext(); 112 | const punkIndex = 1234; 113 | // claim ownership of punk 114 | await punks.setInitialOwner(await user.getAddress(), punkIndex); 115 | await punks.allInitialOwnersAssigned(); 116 | // skip "approving" the punk to the router 117 | 118 | const bundleId = await initializeBundle(assetWrapper, user); 119 | await expect(punkRouter.depositPunk(punkIndex, bundleId)).to.be.reverted; 120 | }); 121 | 122 | it("should fail if not owner", async () => { 123 | const { assetWrapper, punks, punkRouter, user, other } = await setupTestContext(); 124 | const punkIndex = 1234; 125 | // claim ownership of punk 126 | await punks.setInitialOwner(await user.getAddress(), punkIndex); 127 | await punks.allInitialOwnersAssigned(); 128 | // "approve" the punk to the router 129 | await punks.offerPunkForSaleToAddress(punkIndex, 0, punkRouter.address); 130 | 131 | const bundleId = await initializeBundle(assetWrapper, user); 132 | await expect(punkRouter.connect(other).depositPunk(punkIndex, bundleId)).to.be.revertedWith( 133 | "PunkRouter: not owner", 134 | ); 135 | }); 136 | }); 137 | 138 | describe("Withdraw CryptoPunk held by PunkRouter", function () { 139 | it("should successfully withdraw punk", async () => { 140 | const { punks, punkRouter, other, punkIndex } = await setupTestContextForDepositStuck(); 141 | await expect(punkRouter.withdrawPunk(punkIndex, other.address)) 142 | .to.emit(punks, "Transfer") 143 | .withArgs(punkRouter.address, other.address, 1) 144 | .to.emit(punks, "PunkTransfer") 145 | .withArgs(punkRouter.address, other.address, punkIndex); 146 | }); 147 | 148 | it("should fail if not designated admin", async () => { 149 | const { punkRouter, owner, other, punkIndex } = await setupTestContextForDepositStuck(); 150 | await expect(punkRouter.connect(other).withdrawPunk(punkIndex, owner.address)).to.be.revertedWith( 151 | "Ownable: caller is not the owner", 152 | ); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The [Pawn](https://docs.arcade.xyz/docs/faq) Protocol facilitates trustless borrowing, lending, and escrow of NFT assets on EVM blockchains. This repository contains the core contracts that power the protocol, written in Solidity. 2 | 3 | # Relevant Links 4 | 5 | - 🌐 [Website](https://www.arcade.xyz) - Our app website, with a high-level overview of the project. 6 | - 📝 [Usage Documentation](https://docs.arcade.xyz) - Our user-facing documentation for Arcade and the Pawn Protocol. 7 | - 💬 [Discord](https://discord.gg/uNrDStEb) - Join the Arcade community! Great for further technical discussion and real-time support. 8 | - 🔔 [Twitter](https://twitter.com/arcade_xyz) - Follow us on Twitter for alerts and announcements. 9 | 10 | If you are interested in being whitelisted for the Pawn private beta, contact us on Discord. Public launch coming soon! 11 | 12 | # Local Setup 13 | 14 | This repo uses a fork of [Paul Berg's excellent Solidity template](https://github.com/paulrberg/solidity-template). General usage instructions for the repo can be found there. We use a very normal TypeScript/Yarn/Hardhat toolchain. 15 | 16 | ## Deploying 17 | 18 | In order to deploy the contracts to a local hardhat instance run the deploy script. 19 | 20 | `yarn hardhat run scripts/deploy.ts` 21 | 22 | The same can be done for non-local instances like Ropsten or Mainnet, but a private key for the address to deploy from must be supplied in `hardhat.config.ts` as specified in [the Hardhat documentation](https://hardhat.org/config/). 23 | 24 | ## Local Development 25 | 26 | In one window, start a node. Wait for it to load. This is a local Ethereum node forked from the current mainnet Ethereum state. 27 | 28 | `npx hardhat node` 29 | 30 | In another window run the bootrap script with or without loans created. 31 | 32 | `yarn bootstrap-with-loans` 33 | or 34 | `yarn bootstrap-no-loans` 35 | 36 | Both will deploy our smart contracts, create a collection of ERC20 and ERC721/ERC1155 NFTs, and distribute them amongst the first 5 signers, skipping the first one since it deploys the smart contract. The second target will also wrap assets, and create loans. 37 | 38 | # Overview of Contracts 39 | 40 | ## Version 1 41 | 42 | The Version 1 of the Pawn protocol uses the contracts described below for its operation. These contracts are currently deployed on the Ethereum mainnet and the Rinkeby testnet. [The addresses of our deployed can be found in our documentation](https://docs.pawn.fi/docs/contract-addresses). All contracts are verified on [Etherscan](https://etherscan.io/). [Audit reports](https://docs.pawn.fi/docs/audit-reports) are also available. 43 | 44 | ### AssetWrapper 45 | 46 | This contract holds ERC20, ERC721, and ERC1155 assets on behalf of another address. The Pawn protocol interacts with asset wrapped bundles, but bundles have no coupling to the Pawn protocol and can be used for other uses. Any collateral used in the Pawn protocol takes the form of an `AssetWrapper` bundle. 47 | 48 | [AssetWrapper API Specification](docs/AssetWrapper.md) 49 | 50 | ### BorrowerNote 51 | 52 | The BorrowerNote is an ERC721 asset that represents the borrower's obligation for a specific loan in the Pawn protocol. The asset can be transferred like a normal ERC721 NFT, which transfers the borrowing obligation to the recipient of the transfer. Holding the `BorrowerNote` attached to a specific loan gives the holder the right to reclaim the collateral bundle when the loan is repaid. 53 | 54 | `BorrowerNote` and `LenderNote` are both instantiations of `PromissoryNote`, a generalized NFT contract that implements [ERC721Burnable](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721Burnable). 55 | 56 | [PromissoryNote API Specification](docs/PromissoryNote.md) 57 | 58 | ### LenderNote 59 | 60 | The LenderNote is an ERC721 asset that represents the lender's rights for a specific loan in the Pawn protocol. The asset can be transferred like a normal ERC721 NFT, which transfers the rights of the lender to the recipient of the transfer. Holding the `LenderNote` attached to a specific loan gives the holder the right to any funds from loan repayments, and the right to claim a collateral bundle for a defaulted loan. 61 | 62 | `BorrowerNote` and `LenderNote` are both instantiations of `PromissoryNote`, a generalized NFT contract that implements [ERC721Burnable](https://docs.openzeppelin.com/contracts/3.x/api/token/erc721#ERC721Burnable). 63 | 64 | [PromissoryNote API Specification](docs/PromissoryNote.md) 65 | 66 | ### LoanCore 67 | 68 | The core invariants of the Pawn protocol are maintained here. `LoanCore` tracks all active loans, the associated `AssetWrapper` collateral, and `PromissoryNote` obligations. Any execution logic arond loan origination, repayment, or default is contained within `LoanCore`. When a loan is in progress, collateral is held by `LoanCore`, and `LoanCore` contains relevant information about loan terms and due dates. 69 | 70 | This contract also contains admin functionality where operators of the protocol can withdraw any accrued revenue from assessed protocol fees. 71 | 72 | [LoanCore API Specification](docs/LoanCore.md) 73 | 74 | ### OriginationController 75 | 76 | This is an external-facing periphery contract that manages loan origination interactions with `LoanCore`. The `OriginationController` takes responsibility for transferring collateral assets from the borrower to `LoanCore`. This controller also checks the validity of origination signatures against the specified parties and loan terms. 77 | 78 | [OriginationController API Specification](docs/OriginationController.md) 79 | 80 | ### RepaymentController 81 | 82 | This is an external-facing periphery contract that manages interactions with `LoanCore` that end the loan lifecycle. The `RepaymentController` takes responsibility for transferring repaid principal + interest from the borrower to `LoanCore` for disbursal to the lender, and returning collateral assets from `LoanCore` back to the borrower on a successful repayment. This controller also handles lender claims in case of default, and ensures ownership of the lender note before allowing a claim. 83 | 84 | [RepaymentController API Specification](docs/RepaymentController.md) 85 | 86 | ## PunkRouter 87 | 88 | [CryptoPunks](https://www.larvalabs.com/cryptopunks) serve as valuable collateral within the NFT ecosystem, but they do not conform to the ERC721 standard. The `PunkRouter` uilizes the [Wrapped Punks](https://wrappedpunks.com/) contract to enable users to deposit CryptoPunks into `AssetWrapper` collateral bundles. This allows wrapping and depositing to a bundle to be an atomic operation. 89 | 90 | [PunkRouter API Specification](docs/PunkRouter.md) 91 | 92 | ## FlashRollover 93 | 94 | This contract allows borrowers with a currently-active loan to roll over their collateral to a new loan, without needing to pay back the entire principal + interest. The contract uses an [AAVE Flash Loan](https://docs.aave.com/faq/flash-loans) to borrow enough tokens to repay the loan with interest. Once the original loan is repaid, a new loan is issued with the lender's signature, with the principal of the new loan repaying the flash loan plus the flash loan fee (0.09%). This allows borrowers to extend their loan term without having to move any deployed capital from loan proceeds. Note: if the principal of the new loan less fees is smaller than the old loan's principal + interest + flash loan fee, the contract will attempt to withdraw the balance from the borrower's wallet. If the new loan's principal is larger than the old loan's principal + interest + flash loan fee, the leftover loan proceeds will be sent to the borrower, making this like a refinance. 95 | 96 | [FlashRollover API Specification](docs/FlashRollover.md) 97 | 98 | ## Version 2 99 | 100 | Version 2 of the Pawn protocol is currently in development. More details will be added to this section as the protocol progresses towards release. 101 | -------------------------------------------------------------------------------- /test/RepaymentController.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import hre, { ethers, waffle } from "hardhat"; 3 | const { loadFixture } = waffle; 4 | import { utils, Signer, BigNumber } from "ethers"; 5 | 6 | import { MockLoanCore, MockERC20, MockERC721, RepaymentController } from "../typechain"; 7 | import { deploy } from "./utils/contracts"; 8 | 9 | interface TestContext { 10 | loanId: string; 11 | loanData: { 12 | borrowerNoteId: BigNumber; 13 | lenderNoteId: BigNumber; 14 | }; 15 | repaymentController: RepaymentController; 16 | mockERC20: MockERC20; 17 | mockLoanCore: MockLoanCore; 18 | borrower: Signer; 19 | lender: Signer; 20 | otherParty: Signer; 21 | signers: Signer[]; 22 | } 23 | 24 | describe("RepaymentController", () => { 25 | const TEST_LOAN_PRINCIPAL = 10; 26 | const TEST_LOAN_INTEREST = 1; 27 | let context: TestContext; 28 | 29 | /** 30 | * Sets up a test context, deploying new contracts and returning them for use in a test 31 | */ 32 | const fixture = async (): Promise => { 33 | const signers: Signer[] = await hre.ethers.getSigners(); 34 | const [deployer, borrower, lender, otherParty] = signers; 35 | 36 | const mockCollateral = await deploy("MockERC721", deployer, ["Mock Collateral", "MwNFT"]); 37 | const mockLoanCore = await deploy("MockLoanCore", deployer, []); 38 | 39 | const borrowerNoteAddress = await mockLoanCore.borrowerNote(); 40 | const lenderNoteAddress = await mockLoanCore.lenderNote(); 41 | 42 | const mockERC20 = await deploy("MockERC20", deployer, ["Mock ERC20", "MOCK"]); 43 | await mockERC20.mint( 44 | await borrower.getAddress(), 45 | utils.parseEther((TEST_LOAN_PRINCIPAL + TEST_LOAN_INTEREST).toString()), 46 | ); 47 | await mockERC20.mint( 48 | await otherParty.getAddress(), 49 | utils.parseEther((TEST_LOAN_PRINCIPAL + TEST_LOAN_INTEREST).toString()), 50 | ); 51 | 52 | const repaymentController = ( 53 | await deploy("RepaymentController", deployer, [ 54 | mockLoanCore.address, 55 | borrowerNoteAddress, 56 | lenderNoteAddress, 57 | ]) 58 | ); 59 | 60 | // Mint collateral token from asset wrapper 61 | const collateralMintTx = await mockCollateral.mint(await borrower.getAddress()); 62 | await collateralMintTx.wait(); 63 | 64 | // token Id is 0 since it's the first one minted 65 | const collateralTokenId = 0; 66 | 67 | const durationSecs = 60 * 60 * 24 * 14; 68 | const terms = { 69 | durationSecs: durationSecs, 70 | principal: utils.parseEther(TEST_LOAN_PRINCIPAL.toString()), 71 | interest: utils.parseEther(TEST_LOAN_INTEREST.toString()), 72 | collateralTokenId, 73 | payableCurrency: mockERC20.address, 74 | }; 75 | 76 | const createLoanTx = await mockLoanCore.createLoan(terms); 77 | const receipt = await createLoanTx.wait(); 78 | 79 | let loanId: string; 80 | if (receipt && receipt.events && receipt.events.length === 1 && receipt.events[0].args) { 81 | loanId = receipt.events[0].args.loanId; 82 | } else { 83 | throw new Error("Unable to initialize loan"); 84 | } 85 | 86 | await mockLoanCore.startLoan(await lender.getAddress(), await borrower.getAddress(), loanId); 87 | 88 | const loanRes = await mockLoanCore.getLoan(loanId); 89 | 90 | // Extracting properties for cleaner type in test context 91 | const loanData = { 92 | borrowerNoteId: loanRes.borrowerNoteId, 93 | lenderNoteId: loanRes.lenderNoteId, 94 | }; 95 | 96 | return { 97 | loanId, 98 | loanData, 99 | repaymentController, 100 | mockLoanCore, 101 | mockERC20, 102 | borrower, 103 | lender, 104 | otherParty, 105 | signers: signers.slice(3), 106 | }; 107 | }; 108 | 109 | describe("repay", () => { 110 | beforeEach(async () => { 111 | context = await loadFixture(fixture); 112 | }); 113 | 114 | it("reverts for an invalid note ID", async () => { 115 | const { repaymentController, borrower } = context; 116 | // Use junk note ID, like 1000 117 | await expect(repaymentController.connect(borrower).repay(1000)).to.be.revertedWith( 118 | "RepaymentController: repay could not dereference loan", 119 | ); 120 | }); 121 | 122 | it("fails to repay the loan and if the payable currency is not approved", async () => { 123 | const { mockERC20, borrower, repaymentController, loanData } = context; 124 | 125 | const balanceBefore = await mockERC20.balanceOf(await borrower.getAddress()); 126 | expect(balanceBefore.eq(utils.parseEther((TEST_LOAN_PRINCIPAL + TEST_LOAN_INTEREST).toString()))); 127 | 128 | // approve withdrawal 129 | await mockERC20.connect(borrower).approve(repaymentController.address, utils.parseEther("0.001")); 130 | await expect(repaymentController.connect(borrower).repay(loanData.borrowerNoteId)).to.be.reverted; 131 | }); 132 | 133 | it("repays the loan and withdraws from the borrower's account", async () => { 134 | const { mockERC20, borrower, repaymentController, loanData } = context; 135 | 136 | const balanceBefore = await mockERC20.balanceOf(await borrower.getAddress()); 137 | expect(balanceBefore.eq(utils.parseEther((TEST_LOAN_PRINCIPAL + TEST_LOAN_INTEREST).toString()))); 138 | 139 | // approve withdrawal 140 | await mockERC20.connect(borrower).approve(repaymentController.address, utils.parseEther("100")); 141 | await repaymentController.connect(borrower).repay(loanData.borrowerNoteId); 142 | 143 | // Test that borrower no longer has funds 144 | const balanceAfter = await mockERC20.balanceOf(await borrower.getAddress()); 145 | expect(balanceAfter.eq(0)); 146 | 147 | // Correct loan state update should be tested in LoanCore 148 | }); 149 | 150 | it("allows any party to repay the loan, even if not the borrower", async () => { 151 | const { mockERC20, otherParty, repaymentController, loanData } = context; 152 | 153 | const balanceBefore = await mockERC20.balanceOf(await otherParty.getAddress()); 154 | expect(balanceBefore.eq(utils.parseEther((TEST_LOAN_PRINCIPAL + TEST_LOAN_INTEREST).toString()))); 155 | 156 | await mockERC20.connect(otherParty).approve(repaymentController.address, utils.parseEther("100")); 157 | await repaymentController.connect(otherParty).repay(loanData.borrowerNoteId); 158 | 159 | // Test that otherParty no longer has funds 160 | const balanceAfter = await mockERC20.balanceOf(await otherParty.getAddress()); 161 | expect(balanceAfter.eq(0)); 162 | 163 | // Correct loan state update should be tested in LoanCore 164 | }); 165 | }); 166 | describe("claim", () => { 167 | beforeEach(async () => { 168 | context = await loadFixture(fixture); 169 | }); 170 | 171 | it("reverts for an invalid note ID", async () => { 172 | const { repaymentController, lender } = context; 173 | 174 | // Use junk note ID, like 1000 175 | await expect(repaymentController.connect(lender).claim(1000)).to.be.revertedWith( 176 | "ERC721: owner query for nonexistent token", 177 | ); 178 | }); 179 | 180 | it("reverts for a note ID not owned by caller", async () => { 181 | const { repaymentController, lender, borrower, mockLoanCore, loanData } = context; 182 | 183 | const lenderNote = await ( 184 | await ethers.getContractFactory("PromissoryNote") 185 | ).attach(await mockLoanCore.lenderNote()); 186 | await lenderNote 187 | .connect(lender) 188 | .transferFrom(await lender.getAddress(), await borrower.getAddress(), loanData.lenderNoteId); 189 | 190 | // Use junk note ID, like 1000 191 | await expect(repaymentController.connect(lender).claim(loanData.lenderNoteId)).to.be.revertedWith( 192 | "RepaymentController: not owner of lender note", 193 | ); 194 | }); 195 | 196 | it("reverts if the claimant is not the lender", async () => { 197 | const { repaymentController, borrower, loanData } = context; 198 | 199 | // Attempt to claim note from the borrower account 200 | await expect(repaymentController.connect(borrower).claim(loanData.lenderNoteId)).to.be.revertedWith( 201 | "RepaymentController: not owner of lender note", 202 | ); 203 | }); 204 | 205 | it("claims the collateral and sends it to the lender's account", async () => { 206 | const { repaymentController, lender, loanData } = context; 207 | 208 | await repaymentController.connect(lender).claim(loanData.lenderNoteId); 209 | 210 | // Not reverted - correct loan state and disbursement should be updated in LoanCore 211 | }); 212 | }); 213 | }); 214 | -------------------------------------------------------------------------------- /docs/FlashRollover.md: -------------------------------------------------------------------------------- 1 | # `FlashRollover` 2 | 3 | Implementation of a loan rollover/refinance using [AAVE Flash Loans](https://docs.aave.com/faq/flash-loans). 4 | 5 | Borrowers with a currently open loan can collect a signature from their lender on new loan terms off-chain, 6 | then provide the desired new terms to the rollover contract. The contract will execute a flash loan to 7 | pay back their currently open loan, originate a new loan, then use the proceeds from the new loan to repay 8 | the flash loan. Borrowers are responsible for paying the difference between any new loan and the owed flash 9 | loan amount. 10 | 11 | Rollovers can also migrate a loan from one instance of the `LoanCore` contract to a new one, using the `isLegacy` flag. 12 | This can be useful if updates or fixes are made to `LoanCore` and the protocol is re-deployed. 13 | 14 | ## Data Types 15 | 16 | ``` 17 | /** 18 | * Holds parameters passed through flash loan 19 | * control flow that dictate terms of the new loan. 20 | * Contains a signature by lender for same terms. 21 | * isLegacy determines which loanCore to look for the 22 | * old loan in. 23 | */ 24 | struct OperationData { 25 | bool isLegacy; 26 | uint256 loanId; 27 | LoanLibrary.LoanTerms newLoanTerms; 28 | uint8 v; 29 | bytes32 r; 30 | bytes32 s; 31 | } 32 | 33 | /** 34 | * Defines the contracts that should be used for a 35 | * flash loan operation. May change based on if the 36 | * old loan is on the current loanCore or legacy (in 37 | * which case it requires migration). 38 | */ 39 | struct OperationContracts { 40 | ILoanCore loanCore; 41 | IERC721 borrowerNote; 42 | IERC721 lenderNote; 43 | IFeeController feeController; 44 | IERC721 assetWrapper; 45 | IRepaymentController repaymentController; 46 | IOriginationController originationController; 47 | ILoanCore newLoanLoanCore; 48 | IERC721 newLoanBorrowerNote; 49 | } 50 | ``` 51 | 52 | ## API 53 | 54 | ### `constructor` 55 | 56 | ``` 57 | constructor( 58 | ILendingPoolAddressesProvider provider, 59 | ILoanCore loanCore, 60 | ILoanCore legacyLoanCore, 61 | IOriginationController originationController, 62 | IRepaymentController repaymentController, 63 | IRepaymentController legacyRepaymentController, 64 | IERC721 borrowerNote, 65 | IERC721 legacyBorrowerNote, 66 | IERC721 lenderNote, 67 | IERC721 legacyLenderNote, 68 | IERC721 assetWrapper, 69 | IFeeController feeController 70 | ); 71 | ``` 72 | 73 | Initializes the `FlashRollover` contract with the addresses of all contracts it depends on. Some contracts need both 74 | legacy and current versions for migration purposes. Once set, these contract values cannot be changed. 75 | 76 | ### `rolloverLoan` _(external)_ 77 | 78 | ``` 79 | function rolloverLoan( 80 | bool isLegacy, 81 | uint256 loanId, 82 | LoanLibrary.LoanTerms calldata newLoanTerms, 83 | uint8 v, 84 | bytes32 r, 85 | bytes32 s 86 | ) external; 87 | ``` 88 | 89 | Executes a loan rollover using a flash loan. 90 | 91 | The argument `isLegacy` should be set to `true` if the loan needs to migrate from an old `LoanCore` deployment to a new one. 92 | Use this if the loan is being currently managed by a `LoanCore` contract whose address does not match our 93 | [current contract address](https://docs.pawn.fi/docs/contract-addresses). The signature `v`, `r`, `s` should be an 94 | [EIP-712 typed signature](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/draft-EIP712.sol) 95 | whose payload matches the `newLoanTerms`. `loanId` should be the loan that will be closed and rolled over. 96 | 97 | Requirements: 98 | 99 | - Must be called by the loan's borrower. 100 | - New loan terms must use same `collateralTokenId` as old loan. 101 | - New loan terms must use the same `payableCurrency` as old loan. 102 | - If new principal cannot repay flash loan, borrower must `approve` balance due for withdrawal by `FlashRollover` contract. 103 | 104 | ### `executeOperation` _(external)_ 105 | 106 | ``` 107 | function executeOperation( 108 | address[] calldata assets, 109 | uint256[] calldata amounts, 110 | uint256[] calldata premiums, 111 | address initiator, 112 | bytes calldata params 113 | ) external returns (bool) 114 | ``` 115 | 116 | Callback used by AAVE lending pool when a flash loan is executed. [See documentation](https://docs.aave.com/developers/guides/flash-loans#2.-calling-flashloan). 117 | 118 | At the beginning of this function, the rollover contract has the flashloan funds. The contract must contain enough funds at the end 119 | of the function to repay the loan, or the transaction will fail. `executeOperation` for a flash rollover will close out the old loan 120 | and begin a new one. 121 | 122 | Requirements: 123 | 124 | - Caller must be the AAVE lending pool. 125 | - Initiator of the flash loan must be the rollover contract. 126 | - The contract must have a balance greater than or equal to the specified funds at the start of the loan. 127 | 128 | Emits a `Rollover` event. If the rollover is a legacy migration, also emits `Migration` event. 129 | 130 | ### `_executeOperation` _(internal)_ 131 | 132 | ``` 133 | function _executeOperation( 134 | address[] calldata assets, 135 | uint256[] calldata amounts, 136 | uint256[] calldata premiums, 137 | OperationData memory opData 138 | ) internal returns (bool) 139 | ``` 140 | 141 | Encapsulated logic for handling the AAVE flash loan callback. `_executeOperation` 142 | relies on a number of helper functions to complete the following steps: 143 | 144 | 1. Determine the appropriate contracts via `_getContracts` (depending on whether the rollover includes a legacy migration). 145 | 2. Get the loan details and identify borrower and lender. 146 | 3. Ensure proper accounting via `_ensureFunds`. 147 | 4. Repay the old loan with `_repayLoan`. 148 | 5. Initialize a new loan with `_initializeNewLoan`. 149 | 6. Settle accounting with borrower, either sending leftover or collecting balance needed for flash loan repayment. 150 | 7. Approve the lending pool to withdraw the amount due from the flash loan. 151 | 152 | This is the core logic of the contract. 153 | 154 | ### `_getContracts(bool isLegacy) → OperationContracts` _(internal)_ 155 | 156 | Returns the set of contracts needed for the operation, inside 157 | the struct defined by `OperationContracts`. Returns a different result 158 | based on whether the loan requires a legacy rollover. For a legacy rollover, 159 | the execution context needs to be aware of the legacy `LoanCore`, `BorrowerNote`, 160 | `LenderNote`, and `RepaymentController` addresses. 161 | 162 | ### `_ensureFunds` _(internal)_ 163 | 164 | ``` 165 | function _ensureFunds( 166 | uint256 amount, 167 | uint256 premium, 168 | uint256 originationFee, 169 | uint256 newPrincipal 170 | ) 171 | internal 172 | pure 173 | returns ( 174 | uint256 flashAmountDue, 175 | uint256 needFromBorrower, 176 | uint256 leftoverPrincipal 177 | ) 178 | ``` 179 | 180 | Perform the computations needed to determine: 181 | 182 | 1. `flashAmountDue` - the amount that will be owed to AAVE at the end of `executeOperation`. 183 | 2. `needFromBorrower` - if new loan's principal is less than the flash amount due, the amount that the contract will attempt to withdraw from the borrower to repay AAVE. 184 | 3. `leftoverPrincipal` - if new loan's principal is more than the flash amount due, the amount that the contract will disburse to the borrower after the loan is rolled over. 185 | 186 | Note that either `needFromBorrower` or `leftoverPrincipal` should return 0, since they are computed in mutually exclusive situations. 187 | 188 | ### `_repayLoan` _(internal)_ 189 | 190 | ``` 191 | function _repayLoan( 192 | OperationContracts memory contracts, 193 | LoanLibrary.LoanData memory loanData 194 | ) internal 195 | ``` 196 | 197 | Perform the actions needed to repay the existing loan. When this function 198 | runs in the context of `_executeOperation`, it should have enough funds 199 | from flash loan proceeds to repay the loan. The function will withdraw the borrower 200 | note from the borrower, approve the withdrawal by the repayment controller of 201 | the owed funds, and call `RepaymentController` to repay the loan. It will 202 | then verify that it now owns the relevant `collateralTokenId` as the success 203 | condition of repayment. 204 | 205 | ### `_initializeNewLoan` _(internal)_ 206 | 207 | ``` 208 | function _initializeNewLoan( 209 | OperationContracts memory contracts, 210 | address borrower, 211 | address lender, 212 | uint256 collateralTokenId, 213 | OperationData memory opData 214 | ) internal returns (uint256) 215 | ``` 216 | 217 | Perform the actions needed to start a new loan. The `opData` struct should 218 | contain all needed terms and signature information to start a loan with the 219 | `OriginationController`. Once the loan is initialized, the borrower 220 | note will be transferred to `borrower`. 221 | 222 | ### `setOwner(address _owner)` _(external)_ 223 | 224 | Sets a contract owner. The owner is the only party allowed to call `flushToken`. 225 | 226 | Requirements: 227 | 228 | - Must be called by current `owner`. 229 | 230 | Emits a `SetOwner` event. 231 | 232 | ### `flushToken(IERC20 token, address to)` _(external)_ 233 | 234 | Send any ERC20 token balance held within the contract to a specified 235 | address. Needed because balance checks for flash rollover assume 236 | a starting and ending balance of 0 tokens. This prevents the contract 237 | being frozen by a non-zero token balance (either unintentionally or 238 | from a griefing attack). 239 | 240 | Requirements: 241 | 242 | - Must be called by current `owner`. 243 | 244 | ## Events 245 | 246 | ### `Rollover` 247 | 248 | ``` 249 | event Rollover( 250 | address indexed lender, 251 | address indexed borrower, 252 | uint256 collateralTokenId, 253 | uint256 newLoanId 254 | ) 255 | ``` 256 | 257 | Emitted when a loan is rolled over into a new loan. 258 | 259 | ### `Migration` 260 | 261 | ``` 262 | event Migration( 263 | address indexed oldLoanCore, 264 | address indexed newLoanCore, 265 | uint256 newLoanId 266 | ); 267 | ``` 268 | 269 | Emitted when a loan rollover migrates a loan from one instance of `LoanCore` to another. 270 | 271 | ### `SetOwner(address owner)` 272 | 273 | Emitted when the contract owner is changed. 274 | -------------------------------------------------------------------------------- /contracts/LoanCore.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | import "@openzeppelin/contracts/security/Pausable.sol"; 5 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/access/AccessControl.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 9 | import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; 10 | import "@openzeppelin/contracts/utils/Counters.sol"; 11 | import "./interfaces/ICallDelegator.sol"; 12 | import "./interfaces/IPromissoryNote.sol"; 13 | import "./interfaces/IAssetVault.sol"; 14 | import "./interfaces/IFeeController.sol"; 15 | import "./interfaces/ILoanCore.sol"; 16 | 17 | import "./PromissoryNote.sol"; 18 | 19 | /** 20 | * @dev LoanCore contract - core contract for creating, repaying, and claiming collateral for PawnFi loans 21 | */ 22 | contract LoanCore is ILoanCore, AccessControl, Pausable, ICallDelegator { 23 | using Counters for Counters.Counter; 24 | using SafeMath for uint256; 25 | using SafeERC20 for IERC20; 26 | 27 | bytes32 public constant ORIGINATOR_ROLE = keccak256("ORIGINATOR_ROLE"); 28 | bytes32 public constant REPAYER_ROLE = keccak256("REPAYER_ROLE"); 29 | bytes32 public constant FEE_CLAIMER_ROLE = keccak256("FEE_CLAIMER_ROLE"); 30 | 31 | Counters.Counter private loanIdTracker; 32 | mapping(uint256 => LoanLibrary.LoanData) private loans; 33 | mapping(uint256 => bool) private collateralInUse; 34 | IPromissoryNote public immutable override borrowerNote; 35 | IPromissoryNote public immutable override lenderNote; 36 | IERC721 public immutable override collateralToken; 37 | IFeeController public override feeController; 38 | 39 | // 10k bps per whole 40 | uint256 private constant BPS_DENOMINATOR = 10_000; 41 | 42 | constructor(IERC721 _collateralToken, IFeeController _feeController) { 43 | _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); 44 | _setupRole(FEE_CLAIMER_ROLE, _msgSender()); 45 | // only those with FEE_CLAIMER_ROLE can update or grant FEE_CLAIMER_ROLE 46 | _setRoleAdmin(FEE_CLAIMER_ROLE, FEE_CLAIMER_ROLE); 47 | 48 | feeController = _feeController; 49 | collateralToken = _collateralToken; 50 | 51 | borrowerNote = new PromissoryNote("PawnFi Borrower Note", "pBN"); 52 | lenderNote = new PromissoryNote("PawnFi Lender Note", "pLN"); 53 | 54 | // Avoid having loanId = 0 55 | loanIdTracker.increment(); 56 | } 57 | 58 | /** 59 | * @inheritdoc ILoanCore 60 | */ 61 | function getLoan(uint256 loanId) external view override returns (LoanLibrary.LoanData memory loanData) { 62 | return loans[loanId]; 63 | } 64 | 65 | /** 66 | * @inheritdoc ILoanCore 67 | */ 68 | function createLoan(LoanLibrary.LoanTerms calldata terms) 69 | external 70 | override 71 | whenNotPaused 72 | onlyRole(ORIGINATOR_ROLE) 73 | returns (uint256 loanId) 74 | { 75 | require(terms.durationSecs > 0, "LoanCore::create: Loan is already expired"); 76 | require(!collateralInUse[terms.collateralTokenId], "LoanCore::create: Collateral token already in use"); 77 | 78 | loanId = loanIdTracker.current(); 79 | loanIdTracker.increment(); 80 | 81 | loans[loanId] = LoanLibrary.LoanData( 82 | 0, 83 | 0, 84 | terms, 85 | LoanLibrary.LoanState.Created, 86 | block.timestamp + terms.durationSecs 87 | ); 88 | collateralInUse[terms.collateralTokenId] = true; 89 | emit LoanCreated(terms, loanId); 90 | } 91 | 92 | /** 93 | * @inheritdoc ILoanCore 94 | */ 95 | function startLoan( 96 | address lender, 97 | address borrower, 98 | uint256 loanId 99 | ) external override whenNotPaused onlyRole(ORIGINATOR_ROLE) { 100 | LoanLibrary.LoanData memory data = loans[loanId]; 101 | // Ensure valid initial loan state 102 | require(data.state == LoanLibrary.LoanState.Created, "LoanCore::start: Invalid loan state"); 103 | // Pull collateral token and principal 104 | collateralToken.transferFrom(_msgSender(), address(this), data.terms.collateralTokenId); 105 | 106 | IERC20(data.terms.payableCurrency).safeTransferFrom(_msgSender(), address(this), data.terms.principal); 107 | 108 | // Distribute notes and principal 109 | loans[loanId].state = LoanLibrary.LoanState.Active; 110 | uint256 borrowerNoteId = borrowerNote.mint(borrower, loanId); 111 | uint256 lenderNoteId = lenderNote.mint(lender, loanId); 112 | 113 | loans[loanId] = LoanLibrary.LoanData( 114 | borrowerNoteId, 115 | lenderNoteId, 116 | data.terms, 117 | LoanLibrary.LoanState.Active, 118 | data.dueDate 119 | ); 120 | 121 | IERC20(data.terms.payableCurrency).safeTransfer(borrower, getPrincipalLessFees(data.terms.principal)); 122 | emit LoanStarted(loanId, lender, borrower); 123 | } 124 | 125 | /** 126 | * @inheritdoc ILoanCore 127 | */ 128 | function repay(uint256 loanId) external override onlyRole(REPAYER_ROLE) { 129 | LoanLibrary.LoanData memory data = loans[loanId]; 130 | // Ensure valid initial loan state 131 | require(data.state == LoanLibrary.LoanState.Active, "LoanCore::repay: Invalid loan state"); 132 | 133 | // ensure repayment was valid 134 | uint256 returnAmount = data.terms.principal.add(data.terms.interest); 135 | IERC20(data.terms.payableCurrency).safeTransferFrom(_msgSender(), address(this), returnAmount); 136 | 137 | address lender = lenderNote.ownerOf(data.lenderNoteId); 138 | address borrower = borrowerNote.ownerOf(data.borrowerNoteId); 139 | 140 | // state changes and cleanup 141 | // NOTE: these must be performed before assets are released to prevent reentrance 142 | loans[loanId].state = LoanLibrary.LoanState.Repaid; 143 | collateralInUse[data.terms.collateralTokenId] = false; 144 | 145 | lenderNote.burn(data.lenderNoteId); 146 | borrowerNote.burn(data.borrowerNoteId); 147 | 148 | // asset and collateral redistribution 149 | IERC20(data.terms.payableCurrency).safeTransfer(lender, returnAmount); 150 | collateralToken.transferFrom(address(this), borrower, data.terms.collateralTokenId); 151 | 152 | emit LoanRepaid(loanId); 153 | } 154 | 155 | /** 156 | * @inheritdoc ILoanCore 157 | */ 158 | function claim(uint256 loanId) external override whenNotPaused onlyRole(REPAYER_ROLE) { 159 | LoanLibrary.LoanData memory data = loans[loanId]; 160 | 161 | // Ensure valid initial loan state 162 | require(data.state == LoanLibrary.LoanState.Active, "LoanCore::claim: Invalid loan state"); 163 | require(data.dueDate < block.timestamp, "LoanCore::claim: Loan not expired"); 164 | 165 | address lender = lenderNote.ownerOf(data.lenderNoteId); 166 | 167 | // NOTE: these must be performed before assets are released to prevent reentrance 168 | loans[loanId].state = LoanLibrary.LoanState.Defaulted; 169 | collateralInUse[data.terms.collateralTokenId] = false; 170 | 171 | lenderNote.burn(data.lenderNoteId); 172 | borrowerNote.burn(data.borrowerNoteId); 173 | 174 | // collateral redistribution 175 | collateralToken.transferFrom(address(this), lender, data.terms.collateralTokenId); 176 | 177 | emit LoanClaimed(loanId); 178 | } 179 | 180 | /** 181 | * Take a principal value and return the amount less protocol fees 182 | */ 183 | function getPrincipalLessFees(uint256 principal) internal view returns (uint256) { 184 | return principal.sub(principal.mul(feeController.getOriginationFee()).div(BPS_DENOMINATOR)); 185 | } 186 | 187 | // ADMIN FUNCTIONS 188 | 189 | /** 190 | * @dev Set the fee controller to a new value 191 | * 192 | * Requirements: 193 | * 194 | * - Must be called by the owner of this contract 195 | */ 196 | function setFeeController(IFeeController _newController) external onlyRole(FEE_CLAIMER_ROLE) { 197 | feeController = _newController; 198 | } 199 | 200 | /** 201 | * @dev Claim the protocol fees for the given token 202 | * 203 | * @param token The address of the ERC20 token to claim fees for 204 | * 205 | * Requirements: 206 | * 207 | * - Must be called by the owner of this contract 208 | */ 209 | function claimFees(IERC20 token) external onlyRole(FEE_CLAIMER_ROLE) { 210 | // any token balances remaining on this contract are fees owned by the protocol 211 | uint256 amount = token.balanceOf(address(this)); 212 | token.safeTransfer(_msgSender(), amount); 213 | emit FeesClaimed(address(token), _msgSender(), amount); 214 | } 215 | 216 | /** 217 | * @dev Triggers stopped state. 218 | * 219 | * Requirements: 220 | * 221 | * - The contract must not be paused. 222 | */ 223 | function pause() external onlyRole(DEFAULT_ADMIN_ROLE) { 224 | _pause(); 225 | } 226 | 227 | /** 228 | * @dev Returns to normal state. 229 | * 230 | * Requirements: 231 | * 232 | * - The contract must be paused. 233 | */ 234 | function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) { 235 | _unpause(); 236 | } 237 | 238 | /** 239 | * @inheritdoc ICallDelegator 240 | */ 241 | function canCallOn(address caller, address vault) external view override returns (bool) { 242 | // if the collateral is not currently being used in a loan, disallow 243 | if (!collateralInUse[uint256(uint160(vault))]) { 244 | return false; 245 | } 246 | 247 | for (uint256 i = 0; i < borrowerNote.balanceOf(caller); i++) { 248 | uint256 borrowerNoteId = borrowerNote.tokenOfOwnerByIndex(caller, i); 249 | uint256 loanId = borrowerNote.loanIdByNoteId(borrowerNoteId); 250 | // if the borrower is currently borrowing against this vault, 251 | // return true 252 | if (loans[loanId].terms.collateralTokenId == uint256(uint160(vault))) { 253 | return true; 254 | } 255 | } 256 | 257 | return false; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /contracts/test/CryptoPunks.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.4.8; 2 | 3 | contract CryptoPunksMarket { 4 | // You can use this hash to verify the image file containing all the punks 5 | string public imageHash = "ac39af4793119ee46bbff351d8cb6b5f23da60222126add4268e261199a2921b"; 6 | 7 | address owner; 8 | 9 | string public standard = "CryptoPunks"; 10 | string public name; 11 | string public symbol; 12 | uint8 public decimals; 13 | uint256 public totalSupply; 14 | 15 | uint256 public nextPunkIndexToAssign = 0; 16 | 17 | bool public allPunksAssigned = false; 18 | uint256 public punksRemainingToAssign = 0; 19 | 20 | //mapping (address => uint) public addressToPunkIndex; 21 | mapping(uint256 => address) public punkIndexToAddress; 22 | 23 | /* This creates an array with all balances */ 24 | mapping(address => uint256) public balanceOf; 25 | 26 | struct Offer { 27 | bool isForSale; 28 | uint256 punkIndex; 29 | address seller; 30 | uint256 minValue; // in ether 31 | address onlySellTo; // specify to sell only to a specific person 32 | } 33 | 34 | struct Bid { 35 | bool hasBid; 36 | uint256 punkIndex; 37 | address bidder; 38 | uint256 value; 39 | } 40 | 41 | // A record of punks that are offered for sale at a specific minimum value, and perhaps to a specific person 42 | mapping(uint256 => Offer) public punksOfferedForSale; 43 | 44 | // A record of the highest punk bid 45 | mapping(uint256 => Bid) public punkBids; 46 | 47 | mapping(address => uint256) public pendingWithdrawals; 48 | 49 | event Assign(address indexed to, uint256 punkIndex); 50 | event Transfer(address indexed from, address indexed to, uint256 value); 51 | event PunkTransfer(address indexed from, address indexed to, uint256 punkIndex); 52 | event PunkOffered(uint256 indexed punkIndex, uint256 minValue, address indexed toAddress); 53 | event PunkBidEntered(uint256 indexed punkIndex, uint256 value, address indexed fromAddress); 54 | event PunkBidWithdrawn(uint256 indexed punkIndex, uint256 value, address indexed fromAddress); 55 | event PunkBought(uint256 indexed punkIndex, uint256 value, address indexed fromAddress, address indexed toAddress); 56 | event PunkNoLongerForSale(uint256 indexed punkIndex); 57 | 58 | /* Initializes contract with initial supply tokens to the creator of the contract */ 59 | function CryptoPunksMarket() payable { 60 | // balanceOf[msg.sender] = initialSupply; // Give the creator all initial tokens 61 | owner = msg.sender; 62 | totalSupply = 10000; // Update total supply 63 | punksRemainingToAssign = totalSupply; 64 | name = "CRYPTOPUNKS"; // Set the name for display purposes 65 | symbol = "Ͼ"; // Set the symbol for display purposes 66 | decimals = 0; // Amount of decimals for display purposes 67 | } 68 | 69 | function setInitialOwner(address to, uint256 punkIndex) { 70 | if (msg.sender != owner) revert(); 71 | if (allPunksAssigned) revert(); 72 | if (punkIndex >= 10000) revert(); 73 | if (punkIndexToAddress[punkIndex] != to) { 74 | if (punkIndexToAddress[punkIndex] != 0x0) { 75 | balanceOf[punkIndexToAddress[punkIndex]]--; 76 | } else { 77 | punksRemainingToAssign--; 78 | } 79 | punkIndexToAddress[punkIndex] = to; 80 | balanceOf[to]++; 81 | Assign(to, punkIndex); 82 | } 83 | } 84 | 85 | function setInitialOwners(address[] addresses, uint256[] indices) { 86 | if (msg.sender != owner) revert(); 87 | uint256 n = addresses.length; 88 | for (uint256 i = 0; i < n; i++) { 89 | setInitialOwner(addresses[i], indices[i]); 90 | } 91 | } 92 | 93 | function allInitialOwnersAssigned() { 94 | if (msg.sender != owner) revert(); 95 | allPunksAssigned = true; 96 | } 97 | 98 | function getPunk(uint256 punkIndex) { 99 | if (!allPunksAssigned) revert(); 100 | if (punksRemainingToAssign == 0) revert(); 101 | if (punkIndexToAddress[punkIndex] != 0x0) revert(); 102 | if (punkIndex >= 10000) revert(); 103 | punkIndexToAddress[punkIndex] = msg.sender; 104 | balanceOf[msg.sender]++; 105 | punksRemainingToAssign--; 106 | Assign(msg.sender, punkIndex); 107 | } 108 | 109 | // Transfer ownership of a punk to another user without requiring payment 110 | function transferPunk(address to, uint256 punkIndex) { 111 | if (!allPunksAssigned) revert(); 112 | if (punkIndexToAddress[punkIndex] != msg.sender) revert(); 113 | if (punkIndex >= 10000) revert(); 114 | if (punksOfferedForSale[punkIndex].isForSale) { 115 | punkNoLongerForSale(punkIndex); 116 | } 117 | punkIndexToAddress[punkIndex] = to; 118 | balanceOf[msg.sender]--; 119 | balanceOf[to]++; 120 | Transfer(msg.sender, to, 1); 121 | PunkTransfer(msg.sender, to, punkIndex); 122 | // Check for the case where there is a bid from the new owner and refund it. 123 | // Any other bid can stay in place. 124 | Bid bid = punkBids[punkIndex]; 125 | if (bid.bidder == to) { 126 | // Kill bid and refund value 127 | pendingWithdrawals[to] += bid.value; 128 | punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); 129 | } 130 | } 131 | 132 | function punkNoLongerForSale(uint256 punkIndex) { 133 | if (!allPunksAssigned) revert(); 134 | if (punkIndexToAddress[punkIndex] != msg.sender) revert(); 135 | if (punkIndex >= 10000) revert(); 136 | punksOfferedForSale[punkIndex] = Offer(false, punkIndex, msg.sender, 0, 0x0); 137 | PunkNoLongerForSale(punkIndex); 138 | } 139 | 140 | function offerPunkForSale(uint256 punkIndex, uint256 minSalePriceInWei) { 141 | if (!allPunksAssigned) revert(); 142 | if (punkIndexToAddress[punkIndex] != msg.sender) revert(); 143 | if (punkIndex >= 10000) revert(); 144 | punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, 0x0); 145 | PunkOffered(punkIndex, minSalePriceInWei, 0x0); 146 | } 147 | 148 | function offerPunkForSaleToAddress( 149 | uint256 punkIndex, 150 | uint256 minSalePriceInWei, 151 | address toAddress 152 | ) { 153 | if (!allPunksAssigned) revert(); 154 | if (punkIndexToAddress[punkIndex] != msg.sender) revert(); 155 | if (punkIndex >= 10000) revert(); 156 | punksOfferedForSale[punkIndex] = Offer(true, punkIndex, msg.sender, minSalePriceInWei, toAddress); 157 | PunkOffered(punkIndex, minSalePriceInWei, toAddress); 158 | } 159 | 160 | function buyPunk(uint256 punkIndex) payable { 161 | if (!allPunksAssigned) revert(); 162 | Offer offer = punksOfferedForSale[punkIndex]; 163 | if (punkIndex >= 10000) revert(); 164 | if (!offer.isForSale) revert(); // punk not actually for sale 165 | if (offer.onlySellTo != 0x0 && offer.onlySellTo != msg.sender) revert(); // punk not supposed to be sold to this user 166 | if (msg.value < offer.minValue) revert(); // Didn't send enough ETH 167 | if (offer.seller != punkIndexToAddress[punkIndex]) revert(); // Seller no longer owner of punk 168 | 169 | address seller = offer.seller; 170 | 171 | punkIndexToAddress[punkIndex] = msg.sender; 172 | balanceOf[seller]--; 173 | balanceOf[msg.sender]++; 174 | Transfer(seller, msg.sender, 1); 175 | 176 | punkNoLongerForSale(punkIndex); 177 | pendingWithdrawals[seller] += msg.value; 178 | PunkBought(punkIndex, msg.value, seller, msg.sender); 179 | 180 | // Check for the case where there is a bid from the new owner and refund it. 181 | // Any other bid can stay in place. 182 | Bid bid = punkBids[punkIndex]; 183 | if (bid.bidder == msg.sender) { 184 | // Kill bid and refund value 185 | pendingWithdrawals[msg.sender] += bid.value; 186 | punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); 187 | } 188 | } 189 | 190 | function withdraw() { 191 | if (!allPunksAssigned) revert(); 192 | uint256 amount = pendingWithdrawals[msg.sender]; 193 | // Remember to zero the pending refund before 194 | // sending to prevent re-entrancy attacks 195 | pendingWithdrawals[msg.sender] = 0; 196 | msg.sender.transfer(amount); 197 | } 198 | 199 | function enterBidForPunk(uint256 punkIndex) payable { 200 | if (punkIndex >= 10000) revert(); 201 | if (!allPunksAssigned) revert(); 202 | if (punkIndexToAddress[punkIndex] == 0x0) revert(); 203 | if (punkIndexToAddress[punkIndex] == msg.sender) revert(); 204 | if (msg.value == 0) revert(); 205 | Bid existing = punkBids[punkIndex]; 206 | if (msg.value <= existing.value) revert(); 207 | if (existing.value > 0) { 208 | // Refund the failing bid 209 | pendingWithdrawals[existing.bidder] += existing.value; 210 | } 211 | punkBids[punkIndex] = Bid(true, punkIndex, msg.sender, msg.value); 212 | PunkBidEntered(punkIndex, msg.value, msg.sender); 213 | } 214 | 215 | function acceptBidForPunk(uint256 punkIndex, uint256 minPrice) { 216 | if (punkIndex >= 10000) revert(); 217 | if (!allPunksAssigned) revert(); 218 | if (punkIndexToAddress[punkIndex] != msg.sender) revert(); 219 | address seller = msg.sender; 220 | Bid bid = punkBids[punkIndex]; 221 | if (bid.value == 0) revert(); 222 | if (bid.value < minPrice) revert(); 223 | 224 | punkIndexToAddress[punkIndex] = bid.bidder; 225 | balanceOf[seller]--; 226 | balanceOf[bid.bidder]++; 227 | Transfer(seller, bid.bidder, 1); 228 | 229 | punksOfferedForSale[punkIndex] = Offer(false, punkIndex, bid.bidder, 0, 0x0); 230 | uint256 amount = bid.value; 231 | punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); 232 | pendingWithdrawals[seller] += amount; 233 | PunkBought(punkIndex, bid.value, seller, bid.bidder); 234 | } 235 | 236 | function withdrawBidForPunk(uint256 punkIndex) { 237 | if (punkIndex >= 10000) revert(); 238 | if (!allPunksAssigned) revert(); 239 | if (punkIndexToAddress[punkIndex] == 0x0) revert(); 240 | if (punkIndexToAddress[punkIndex] == msg.sender) revert(); 241 | Bid bid = punkBids[punkIndex]; 242 | if (bid.bidder != msg.sender) revert(); 243 | PunkBidWithdrawn(punkIndex, bid.value, msg.sender); 244 | uint256 amount = bid.value; 245 | punkBids[punkIndex] = Bid(false, punkIndex, 0x0, 0); 246 | // Refund the bid money 247 | msg.sender.transfer(amount); 248 | } 249 | } 250 | --------------------------------------------------------------------------------