├── .gitmodules ├── MicroLend ├── foundry.toml ├── .gitignore ├── .github │ └── workflows │ │ └── test.yml ├── README.md └── src │ └── MicroLend.sol ├── .gitignore └── MicroLend.ipynb /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "MicroLend/lib/forge-std"] 2 | path = MicroLend/lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | -------------------------------------------------------------------------------- /MicroLend/foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 7 | -------------------------------------------------------------------------------- /MicroLend/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /MicroLend/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | env: 9 | FOUNDRY_PROFILE: ci 10 | 11 | jobs: 12 | check: 13 | strategy: 14 | fail-fast: true 15 | 16 | name: Foundry project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | with: 26 | version: nightly 27 | 28 | - name: Show Forge version 29 | run: | 30 | forge --version 31 | 32 | - name: Run Forge fmt 33 | run: | 34 | forge fmt --check 35 | id: fmt 36 | 37 | - name: Run Forge build 38 | run: | 39 | forge build --sizes 40 | id: build 41 | 42 | - name: Run Forge tests 43 | run: | 44 | forge test -vvv 45 | id: test 46 | -------------------------------------------------------------------------------- /MicroLend/README.md: -------------------------------------------------------------------------------- 1 | ## Foundry 2 | 3 | **Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** 4 | 5 | Foundry consists of: 6 | 7 | - **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). 8 | - **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. 9 | - **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. 10 | - **Chisel**: Fast, utilitarian, and verbose solidity REPL. 11 | 12 | ## Documentation 13 | 14 | https://book.getfoundry.sh/ 15 | 16 | ## Usage 17 | 18 | ### Build 19 | 20 | ```shell 21 | $ forge build 22 | ``` 23 | 24 | ### Test 25 | 26 | ```shell 27 | $ forge test 28 | ``` 29 | 30 | ### Format 31 | 32 | ```shell 33 | $ forge fmt 34 | ``` 35 | 36 | ### Gas Snapshots 37 | 38 | ```shell 39 | $ forge snapshot 40 | ``` 41 | 42 | ### Anvil 43 | 44 | ```shell 45 | $ anvil 46 | ``` 47 | 48 | ### Deploy 49 | 50 | ```shell 51 | $ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key 52 | ``` 53 | 54 | ### Cast 55 | 56 | ```shell 57 | $ cast 58 | ``` 59 | 60 | ### Help 61 | 62 | ```shell 63 | $ forge --help 64 | $ anvil --help 65 | $ cast --help 66 | ``` 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tags 2 | 3 | *.bundle.* 4 | lib/ 5 | node_modules/ 6 | *.egg-info/ 7 | .ipynb_checkpoints 8 | *.tsbuildinfo 9 | 10 | # Created by https://www.gitignore.io/api/python 11 | # Edit at https://www.gitignore.io/?templates=python 12 | 13 | ### Python ### 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | build/ 25 | develop-eggs/ 26 | dist/ 27 | downloads/ 28 | eggs/ 29 | .eggs/ 30 | lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | pip-wheel-metadata/ 37 | share/python-wheels/ 38 | .installed.cfg 39 | *.egg 40 | MANIFEST 41 | 42 | # PyInstaller 43 | # Usually these files are written by a python script from a template 44 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 45 | *.manifest 46 | *.spec 47 | 48 | # Installer logs 49 | pip-log.txt 50 | pip-delete-this-directory.txt 51 | 52 | # Unit test / coverage reports 53 | htmlcov/ 54 | .tox/ 55 | .nox/ 56 | .coverage 57 | .coverage.* 58 | .cache 59 | nosetests.xml 60 | coverage.xml 61 | *.cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | .spyproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # Mr Developer 95 | .mr.developer.cfg 96 | .project 97 | .pydevproject 98 | 99 | # mkdocs documentation 100 | /site 101 | 102 | # mypy 103 | .mypy_cache/ 104 | .dmypy.json 105 | dmypy.json 106 | 107 | # Pyre type checker 108 | .pyre/ 109 | 110 | # OS X stuff 111 | *.DS_Store 112 | 113 | # End of https://www.gitignore.io/api/python 114 | 115 | _temp_extension 116 | junit.xml 117 | [uU]ntitled* 118 | notebook/static/* 119 | !notebook/static/favicons 120 | notebook/labextension 121 | notebook/schemas 122 | docs/source/changelog.md 123 | docs/source/contributing.md 124 | 125 | # playwright 126 | ui-tests/test-results 127 | ui-tests/playwright-report 128 | 129 | # VSCode 130 | .vscode 131 | 132 | # RTC 133 | .jupyter_ystore.db 134 | 135 | # yarn >=2.x local files 136 | .yarn/* 137 | .pnp.* 138 | ui-tests/.yarn/* 139 | ui-tests/.pnp.* 140 | -------------------------------------------------------------------------------- /MicroLend/src/MicroLend.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.0; 3 | 4 | interface IWETH { 5 | function deposit() external payable; 6 | function withdraw(uint) external; 7 | function transfer(address to, uint value) external returns (bool); 8 | function transferFrom(address from, address to, uintvalue) external returns (bool); 9 | function balanceOf(address owner) external view returns (uint); 10 | } 11 | 12 | contract LendingWithLiquidationWETH { 13 | IWETH public weth; 14 | 15 | struct Loan { 16 | uint principal; // The initial borrowed amount 17 | uint interestAccrued; // Total interest accrued 18 | uint lastBorrowTime; // Last time when interest was updated 19 | } 20 | 21 | mapping(address => uint) public collateral; // Collateral deposited by users 22 | mapping(address => Loan) public loans; // Track loans and interest for users 23 | 24 | uint public totalCollateral; 25 | uint public totalLoans; 26 | 27 | uint public interestRatePerSecond = 1e12; // Example: 0.0001% per second (scaled by 1e12 for precision) 28 | uint public liquidationThreshold = 150; // Collateralization ratio (in %) below which loans can be liquidated 29 | 30 | constructor(address _weth) { 31 | weth = IWETH(_weth); 32 | } 33 | 34 | // Deposit WETH as collateral 35 | function depositCollateral(uint amount) external { 36 | require(amount > 0, "Deposit must be greater than zero"); 37 | weth.transferFrom(msg.sender, address(this), amount); 38 | collateral[msg.sender] += amount; 39 | totalCollateral += amount; 40 | } 41 | 42 | // Borrow WETH against collateral 43 | function borrow(uint amount) external { 44 | require(amount > 0, "Borrow amount must be greater than zero"); 45 | 46 | // Update interest before borrowing 47 | _updateInterest(msg.sender); 48 | 49 | uint maxBorrow = collateral[msg.sender] * 100 / liquidationThreshold; // Maximum borrowable based on collateral 50 | require(amount + loans[msg.sender].principal <= maxBorrow, "Exceeds borrow limit"); 51 | 52 | loans[msg.sender].principal += amount; 53 | loans[msg.sender].lastBorrowTime = block.timestamp; 54 | 55 | totalLoans += amount; 56 | weth.transfer(msg.sender, amount); 57 | } 58 | 59 | // Repay borrowed WETH (includes interest) 60 | function repay(uint amount) external { 61 | require(loans[msg.sender].principal > 0, "No outstanding loans"); 62 | 63 | // Update interest before repayment 64 | _updateInterest(msg.sender); 65 | 66 | uint totalDue = loans[msg.sender].principal + loans[msg.sender].interestAccrued; 67 | require(amount <= totalDue, "Cannot overpay loan"); 68 | 69 | weth.transferFrom(msg.sender, address(this), amount); 70 | 71 | if (amount >= loans[msg.sender].interestAccrued) { 72 | // First, cover the accrued interest 73 | amount -= loans[msg.sender].interestAccrued; 74 | loans[msg.sender].interestAccrued = 0; 75 | 76 | // Then, reduce the principal 77 | loans[msg.sender].principal -= amount; 78 | } else { 79 | // Partial payment covers only interest 80 | loans[msg.sender].interestAccrued -= amount; 81 | } 82 | 83 | totalLoans -= amount; 84 | } 85 | 86 | // Withdraw unused collateral 87 | function withdrawCollateral(uint amount) external { 88 | require(amount > 0, "Withdraw amount must be greater than zero"); 89 | require(collateral[msg.sender] >= amount, "Insufficient collateral"); 90 | 91 | // Ensure remaining collateral is sufficient for the loan 92 | uint maxBorrow = (collateral[msg.sender] - amount) * 100 / liquidationThreshold; 93 | require(loans[msg.sender].principal <= maxBorrow, "Cannot withdraw collateral below required ratio"); 94 | 95 | collateral[msg.sender] -= amount; 96 | totalCollateral -= amount; 97 | weth.transfer(msg.sender, amount); 98 | } 99 | 100 | // Liquidate an under-collateralized loan 101 | function liquidate(address borrower) external { 102 | _updateInterest(borrower); 103 | 104 | uint totalDue = loans[borrower].principal + loans[borrower].interestAccrued; 105 | uint collateralValue = collateral[borrower] * 100 / liquidationThreshold; 106 | 107 | require(totalDue > collateralValue, "Loan is sufficiently collateralized"); 108 | 109 | // Liquidator receives the collateral 110 | uint collateralToSeize = collateral[borrower]; 111 | collateral[borrower] = 0; 112 | totalCollateral -= collateralToSeize; 113 | 114 | // Reset the borrower's loan 115 | loans[borrower].principal = 0; 116 | loans[borrower].interestAccrued = 0; 117 | loans[borrower].lastBorrowTime = 0; 118 | totalLoans -= totalDue; 119 | 120 | weth.transfer(msg.sender, collateralToSeize); 121 | } 122 | 123 | // Update interest for the borrower 124 | function _updateInterest(address borrower) internal { 125 | Loan storage loan = loans[borrower]; 126 | if (loan.principal > 0) { 127 | uint timeElapsed = block.timestamp - loan.lastBorrowTime; 128 | uint newInterest = (loan.principal * interestRatePerSecond * timeElapsed) / 1e12; 129 | 130 | loan.interestAccrued += newInterest; 131 | loan.lastBorrowTime = block.timestamp; 132 | } 133 | } 134 | 135 | // View the total amount due for a borrower (principal + interest) 136 | function totalDue(address borrower) external view returns (uint) { 137 | Loan storage loan = loans[borrower]; 138 | if (loan.principal == 0) { 139 | return 0; 140 | } 141 | 142 | uint timeElapsed = block.timestamp - loan.lastBorrowTime; 143 | uint currentInterest = (loan.principal * interestRatePerSecond * timeElapsed) / 1e12; 144 | 145 | return loan.principal + loan.interestAccrued + currentInterest; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /MicroLend.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "a4f7d529-94ff-4a51-9e62-2040d87e6b2d", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import time\n", 11 | "import random\n", 12 | "import string\n", 13 | "from collections import defaultdict" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 2, 19 | "id": "9df6fc30-b31c-48dd-a20d-6a86caac1a12", 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "class Contract:\n", 24 | " def __init__(self):\n", 25 | " self.address = '0x' + ''.join(random.choices('0123456789abcdef', k=40))\n", 26 | " def emit(self, topic, *args): print(f\"{topic} - {' '.join(map(str, args))}\")\n", 27 | " def __str__(self): return f\"{self.address[:5]}\"" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": 3, 33 | "id": "e625e524-c4e7-48c8-bebc-6da5e9f8efd7", 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "class AddressDict(defaultdict):\n", 38 | " def __getitem__(self, key):\n", 39 | " return super().__getitem__(key.address if hasattr(key, 'address') else key)\n", 40 | "\n", 41 | " def __setitem__(self, key, value):\n", 42 | " return super().__setitem__(key.address if hasattr(key, 'address') else key, value)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 5, 48 | "id": "dfb46ea3-9bf8-47e9-9f41-8e7b42a02ab4", 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "class User(Contract): pass" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 6, 58 | "id": "24625bf0-0833-4dca-88f2-dd98f6090e70", 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "shafu = User()\n", 63 | "wolfram = User()\n", 64 | "taleb = User()\n", 65 | "deutsch = User()\n", 66 | "popper = User()" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "id": "fbaf2b69-941b-44a4-a11f-91107fbd8c98", 72 | "metadata": {}, 73 | "source": [ 74 | "## ERC-20\n", 75 | "\n", 76 | "https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": 9, 82 | "id": "4727157d-4e92-4669-8fb6-cd7c6f8b9267", 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "ZERO_ADDRESS = \"0x0\"" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": 10, 92 | "id": "113c11b8-e0d1-41f7-928d-0f81f74ec3cb", 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "class Token(Contract):\n", 97 | " def __init__(self, name, symbol):\n", 98 | " super().__init__()\n", 99 | " self.name = name\n", 100 | " self.symbol = symbol\n", 101 | " self.totalSupply = 0\n", 102 | " self.allowance = AddressDict(lambda: AddressDict(int))\n", 103 | " self.balanceOf = AddressDict(int)\n", 104 | "\n", 105 | " def approve(self, sender, spender, amount):\n", 106 | " self.allowance[sender][spender] = amount;\n", 107 | " self.emit(\"Approval\", sender, spender, amount)\n", 108 | " return True\n", 109 | "\n", 110 | " def transfer(self, sender, to, amount):\n", 111 | " assert self.balanceOf[sender] >= amount\n", 112 | " self.balanceOf[sender] -= amount\n", 113 | " self.balanceOf[to] += amount\n", 114 | " self.emit(\"Transfer\", sender, to, amount)\n", 115 | " return True \n", 116 | "\n", 117 | " def transfer_from(self, fr, sender, to, amount):\n", 118 | " assert self.allowance[fr][sender] >= amount\n", 119 | " self.allowance[fr][sender] -= amount\n", 120 | " self.balanceOf[sender] -= amount\n", 121 | " self.balanceOf[to] += amount\n", 122 | " self.emit(\"Transfer\", sender, to, amount)\n", 123 | " return True\n", 124 | "\n", 125 | " def mint(self, to, amount):\n", 126 | " self.balanceOf[to] += amount\n", 127 | " self.totalSupply += amount\n", 128 | " self.emit(\"Transfer\", ZERO_ADDRESS, to, amount)\n", 129 | "\n", 130 | " def burn(self, fr, amount):\n", 131 | " self.balanceOf[fr.address] = 0\n", 132 | " self.balanceOf[fr.address] -= amount\n", 133 | " self.totalSupply -= amount\n", 134 | "\n", 135 | " def balance(self, user): return self.balanceOf[user]" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": 11, 141 | "id": "d1f3f89d-72a0-4e37-bd40-9a870149c34a", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "DYAD = Token(\"DYAD Stable\", \"DYAD\")\n", 146 | "USDC = Token(\"USD Coin\", \"USDC\")\n", 147 | "WETH = Token(\"Wrapped Ether\", \"WETH\")\n", 148 | "WBTC = Token(\"Wrapped BTC\", \"WBTC\")\n", 149 | "USDT = Token(\"Tether USD\", \"USDT\")" 150 | ] 151 | }, 152 | { 153 | "cell_type": "code", 154 | "execution_count": 12, 155 | "id": "111e3a74-d58e-4715-bf01-88a6c5694512", 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "name": "stdout", 160 | "output_type": "stream", 161 | "text": [ 162 | "Transfer - 0x0 0x51f 100\n", 163 | "Transfer - 0x51f 0x328 20\n" 164 | ] 165 | }, 166 | { 167 | "data": { 168 | "text/plain": [ 169 | "20" 170 | ] 171 | }, 172 | "execution_count": 12, 173 | "metadata": {}, 174 | "output_type": "execute_result" 175 | } 176 | ], 177 | "source": [ 178 | "DYAD.mint(wolfram, 100)\n", 179 | "DYAD.transfer(wolfram, shafu, 20)\n", 180 | "DYAD.balance(shafu)" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 13, 186 | "id": "2da796a6-2490-4e9f-8722-dffd7e79c2cb", 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "class Oracle(Contract):\n", 191 | " def __init__(self, asset0, asset1, price): \n", 192 | " self.asset0, self.asset1 = asset0, asset1\n", 193 | " self.price = price\n", 194 | " def set_price(self, price): self.price = price\n", 195 | " def __str__(self): \n", 196 | " return f\"{self.asset0.symbol}/{self.asset1.symbol} {self.price}\"" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 14, 202 | "id": "6165c495-2d1d-47c9-a5c3-bbf600c3a1a5", 203 | "metadata": {}, 204 | "outputs": [ 205 | { 206 | "name": "stdout", 207 | "output_type": "stream", 208 | "text": [ 209 | "WETH/USDC 2615.93\n", 210 | "WETH/WBTC 0.039\n" 211 | ] 212 | } 213 | ], 214 | "source": [ 215 | "ETH2USD = Oracle(WETH, USDC, 2615.93)\n", 216 | "ETH2BTC = Oracle(WETH, WBTC, 0.039)\n", 217 | "\n", 218 | "print(ETH2USD)\n", 219 | "print(ETH2BTC)" 220 | ] 221 | }, 222 | { 223 | "cell_type": "markdown", 224 | "id": "c1bcfec0-3c0f-4da0-b7df-5e8db0480e27", 225 | "metadata": {}, 226 | "source": [ 227 | "### utils" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 15, 233 | "id": "d7c173a7-dd2c-4857-ac63-1a10c422f608", 234 | "metadata": {}, 235 | "outputs": [], 236 | "source": [ 237 | "def get_time(): return int(time.time()) # seconds\n", 238 | "def apr_to_spr(x): return x / (365*24*60*60) # interest / second" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 16, 244 | "id": "afe86672-68c9-4106-90a0-b22e5e6d8207", 245 | "metadata": {}, 246 | "outputs": [], 247 | "source": [ 248 | "INTEREST_PER_YEAR = 0.02 # 2%\n", 249 | "INTEREST_SLOPE = 0.056\n", 250 | "RESERVE_FACTOR = 0.9 # 90%" 251 | ] 252 | }, 253 | { 254 | "cell_type": "code", 255 | "execution_count": 24, 256 | "id": "6ef279f1-d0cb-472d-8247-3ed263b4c4bb", 257 | "metadata": {}, 258 | "outputs": [], 259 | "source": [ 260 | "class MicroLend(Contract):\n", 261 | " def __init__(\n", 262 | " self,\n", 263 | " lend_asset,\n", 264 | " collat_asset,\n", 265 | " borrow_asset,\n", 266 | " min_collat_ratio\n", 267 | " ):\n", 268 | " super().__init__()\n", 269 | " self.lend_asset = lend_asset\n", 270 | " self.collat_asset = collat_asset\n", 271 | " self.borrow_asset = borrow_asset\n", 272 | " self.min_collat_ratio = min_collat_ratio\n", 273 | " self.balances = AddressDict(lambda: AddressDict(int))\n", 274 | " self.totals = AddressDict(int)\n", 275 | " \n", 276 | " self.reward_per_token = 0\n", 277 | " self.reward_per_token_paid = defaultdict(int)\n", 278 | " self.rewards = defaultdict(int)\n", 279 | " self.updatedAt = get_time()\n", 280 | "\n", 281 | " def get_reward_per_token(self):\n", 282 | " totalLend = self.totals[self.lend_asset.address]\n", 283 | " if totalLend == 0: return self.reward_per_token\n", 284 | "\n", 285 | " spr = apr_to_spr(INTEREST_PER_YEAR) # interest / second\n", 286 | " dt = get_time() - self.updatedAt\n", 287 | "\n", 288 | " return self.reward_per_token + (spr * dt) / totalLend\n", 289 | "\n", 290 | " def earned(self, user):\n", 291 | " balance = self.balances[user.address][self.lend_asset.address]\n", 292 | " d_rewardPerToken = self.reward_per_token - self.reward_per_token_paid[user.address]\n", 293 | " return self.rewards[user.address] + (balance * d_rewardPerToken)\n", 294 | "\n", 295 | " def updateReward(self, user):\n", 296 | " self.reward_per_token = self.get_reward_per_token()\n", 297 | " self.updatedAt = get_time()\n", 298 | "\n", 299 | " self.rewards[user.address] = self.earned(user)\n", 300 | " self.reward_per_token_paid[user.address] = self.reward_per_token\n", 301 | "\n", 302 | " def deposit(self, user, token, amount):\n", 303 | " self.updateReward(user)\n", 304 | " token.transfer(user, self, amount)\n", 305 | " self.balances[user][token] += amount\n", 306 | " self.totals[token] += amount\n", 307 | "\n", 308 | " def withdraw(self, user, token, amount):\n", 309 | " self.updateReward(user)\n", 310 | " self.balances[user.address][token.address] -= amount\n", 311 | " self.totals[token.address] -= amount\n", 312 | "\n", 313 | " def borrow(self, user, amount):\n", 314 | " self.updateReward(user)\n", 315 | " self.balances[user.address][self.borrow_asset.address] += amount\n", 316 | " self.totals[self.borrow_asset.address] += amount\n", 317 | "\n", 318 | " def repay(self, user, amount): pass\n", 319 | "\n", 320 | " def collat_ratio(self, user): \n", 321 | " borrowed = self.balances[user.address][self.borrow_asset.address]\n", 322 | " if borrowed == 0: return (1<<256) - 1 # a very large uint \n", 323 | " return self.balances[user.address][self.collat_asset.address] / borrowed\n", 324 | "\n", 325 | " def utilization(self):\n", 326 | " lend = self.totals[self.lend_asset.address]\n", 327 | " if lend == 0: return 0\n", 328 | " return self.totals[self.borrow_asset.address] / lend\n", 329 | "\n", 330 | " def borrow_apr(self): return self.utilization() * INTEREST_SLOPE\n", 331 | " def lend_apr (self): return self.borrow_apr() * RESERVE_FACTOR\n", 332 | "\n", 333 | " def stats(self, user):\n", 334 | " mL.updateReward(user)\n", 335 | " lend = self.balances[user.address][self.lend_asset.address]\n", 336 | " collat = self.balances[user.address][self.collat_asset.address]\n", 337 | " borrow = self.balances[user.address][self.borrow_asset.address]\n", 338 | " cr = round(self.collat_ratio(user)*100, 2)\n", 339 | " reward = self.rewards[user.address]\n", 340 | " print(\n", 341 | " f\"{'Lend:':<14} {lend} {self.lend_asset.symbol}\\n\"\n", 342 | " f\"{'Collateral:':<14} {collat}\\n\"\n", 343 | " f\"{'Borrowed:':<14} {borrow} {self.borrow_asset.symbol}\\n\"\n", 344 | " f\"{'Collat Ratio:':<14} {cr}%\\n\"\n", 345 | " f\"{'Reward:':<14} {reward}%\\n\"\n", 346 | " \"\\n\"\n", 347 | " f\"{'Utilization:':<14} {self.utilization()*100}%\\n\"\n", 348 | " f\"{'Lend APR:':<14} {self.lend_apr()*100}%\\n\"\n", 349 | " f\"{'Borrow APR:':<14} {self.borrow_apr()*100}%\"\n", 350 | " )\n", 351 | " \n", 352 | " #def __str__(self): return super().__str__()" 353 | ] 354 | }, 355 | { 356 | "cell_type": "code", 357 | "execution_count": 25, 358 | "id": "440ab291-a7bc-44ed-841c-6657fde66072", 359 | "metadata": {}, 360 | "outputs": [], 361 | "source": [ 362 | "mL = MicroLend(\n", 363 | " WETH, \n", 364 | " USDC,\n", 365 | " DYAD,\n", 366 | " 1.5 # 150%\n", 367 | ")" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 27, 373 | "id": "e0542e16-a8fc-4209-9c39-cfd6b5e54913", 374 | "metadata": {}, 375 | "outputs": [ 376 | { 377 | "name": "stdout", 378 | "output_type": "stream", 379 | "text": [ 380 | "Transfer - 0x0 0x51f 1000\n", 381 | "Transfer - 0x51f 0xb4c 1000\n" 382 | ] 383 | } 384 | ], 385 | "source": [ 386 | "WETH.mint(wolfram, 1_000)\n", 387 | "mL.deposit(wolfram, WETH, 1_000)" 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": 771, 393 | "id": "33aa0079-f7ab-4222-aa12-2af88bccae90", 394 | "metadata": {}, 395 | "outputs": [ 396 | { 397 | "name": "stdout", 398 | "output_type": "stream", 399 | "text": [ 400 | "Lend: 1000 WETH\n", 401 | "Collateral: 1000\n", 402 | "Borrowed: 130 DYAD\n", 403 | "Collat Ratio: 769.23%\n", 404 | "Reward: 6.341958396752917e-10%\n", 405 | "\n", 406 | "Utilization: 13.0%\n", 407 | "Lend APR: 0.6552%\n", 408 | "Borrow APR: 0.728%\n" 409 | ] 410 | } 411 | ], 412 | "source": [ 413 | "mL.stats(vitalik)" 414 | ] 415 | } 416 | ], 417 | "metadata": { 418 | "kernelspec": { 419 | "display_name": "Python 3 (ipykernel)", 420 | "language": "python", 421 | "name": "python3" 422 | }, 423 | "language_info": { 424 | "codemirror_mode": { 425 | "name": "ipython", 426 | "version": 3 427 | }, 428 | "file_extension": ".py", 429 | "mimetype": "text/x-python", 430 | "name": "python", 431 | "nbconvert_exporter": "python", 432 | "pygments_lexer": "ipython3", 433 | "version": "3.9.6" 434 | } 435 | }, 436 | "nbformat": 4, 437 | "nbformat_minor": 5 438 | } 439 | --------------------------------------------------------------------------------