├── README.md ├── berapolia-tutorial ├── README.md ├── contracts │ ├── Token.vy │ ├── imports │ │ ├── ecdsa.vy │ │ ├── eip712_domain_separator.vy │ │ ├── message_hash_utils.vy │ │ └── ownable.vy │ ├── interfaces │ │ ├── IERC1155.vyi │ │ ├── IERC1155MetadataURI.vyi │ │ ├── IERC1155Receiver.vyi │ │ ├── IERC20Permit.vyi │ │ ├── IERC4906.vyi │ │ ├── IERC5267.vyi │ │ ├── IERC721Enumerable.vyi │ │ ├── IERC721Metadata.vyi │ │ ├── IERC721Permit.vyi │ │ └── IERC721Receiver.vyi │ └── snapshot │ │ ├── step-01 │ │ └── Token.vy │ │ ├── step-02 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-03 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-04 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-05 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-06 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-07 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-08 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-09 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-10 │ │ ├── Minter.vy │ │ └── Token.vy │ │ ├── step-11 │ │ ├── Minter.vy │ │ └── Token.vy │ │ └── step-12 │ │ ├── Minter.vy │ │ └── Token.vy ├── pyproject.toml ├── requirements.in ├── requirements.txt ├── scripts │ ├── deploy_curve_pool.ipynb │ └── deploy_token.ipynb └── tests │ ├── conftest.py │ └── test_minter.py ├── crvusd ├── arb │ ├── .gitattributes │ ├── .gitignore │ ├── contracts │ │ └── Arbitrage.vy │ ├── scripts │ │ └── arbitrage.py │ └── settings.py └── peg-keeper │ ├── brownie-config.yaml │ └── scripts │ └── pegkeeper.py ├── lesson-01-setup ├── README.md ├── crvUSDBull.jpg └── vyper-token │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ ├── Token.py │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-02-contract ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── Minter.bak │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-03-state-variables ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-04-assert ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-05-interface ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-06-import ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── .conftest.py.swp │ ├── .test_minter.py.swp │ ├── conftest.py │ ├── test_approve.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-07-scope ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie_config.yaml │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_lend.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie_config.yaml │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-08-math ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ │ ├── Minter.vy │ │ ├── TestMath.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_lend.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ └── test_transferFrom.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ ├── Minter.vy │ ├── TestMath.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ └── test_transferFrom.py ├── lesson-09-struct ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_lend.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ ├── test_transferFrom.py │ │ └── test_withdraw.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ ├── test_transferFrom.py │ └── test_withdraw.py ├── lesson-10-hashmap ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_lend.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ ├── test_transferFrom.py │ │ └── test_withdraw.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ ├── test_transferFrom.py │ └── test_withdraw.py ├── lesson-11-event ├── README.md ├── solution.diff ├── solved │ ├── .gitattributes │ ├── .github │ │ └── workflows │ │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ │ ├── Minter.vy │ │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ │ └── token.py │ └── tests │ │ ├── conftest.py │ │ ├── test_approve.py │ │ ├── test_lend.py │ │ ├── test_minter.py │ │ ├── test_transfer.py │ │ ├── test_transferFrom.py │ │ └── test_withdraw.py ├── tests.diff └── unsolved │ ├── .gitattributes │ ├── .github │ └── workflows │ │ └── main.yaml │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── brownie-config.yaml │ ├── contracts │ ├── Minter.vy │ └── Token.vy │ ├── requirements.txt │ ├── scripts │ └── token.py │ └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ ├── test_transferFrom.py │ └── test_withdraw.py └── lesson-12-dynarray ├── README.md ├── solution.diff ├── solved ├── .DS_Store ├── .gitattributes ├── .github │ └── workflows │ │ └── main.yaml ├── .gitignore ├── LICENSE ├── README.md ├── brownie-config.yaml ├── contracts │ ├── Minter.vy │ └── Token.vy ├── requirements.txt ├── scripts │ └── token.py └── tests │ ├── conftest.py │ ├── test_approve.py │ ├── test_lend.py │ ├── test_minter.py │ ├── test_transfer.py │ ├── test_transferFrom.py │ └── test_withdraw.py ├── tests.diff └── unsolved ├── .gitattributes ├── .github └── workflows │ └── main.yaml ├── .gitignore ├── LICENSE ├── README.md ├── brownie-config.yaml ├── contracts ├── Minter.vy └── Token.vy ├── requirements.txt ├── scripts └── token.py └── tests ├── conftest.py ├── test_approve.py ├── test_lend.py ├── test_minter.py ├── test_transfer.py ├── test_transferFrom.py └── test_withdraw.py /berapolia-tutorial/README.md: -------------------------------------------------------------------------------- 1 | image 2 | 3 | # Berapolia Tutorial - April 2 4 | 5 | Presentation: 6 | * [Presentation](https://docs.google.com/presentation/d/11TzJfJhBc1zc0HbI46zP7Znes1Kyqe-7ShAAHmEEues/edit?usp=sharing) 7 | 8 | Repositories: 9 | * [Boa](https://github.com/vyperlang/titanoboa/) 10 | * [Snekmate](https://github.com/pcaversaccio/snekmate) 11 | * [Curve Lite](https://github.com/curvefi/curve-core) 12 | * [Mamushi](https://github.com/benber86/mamushi) 13 | * [Ruff](https://github.com/astral-sh/ruff) 14 | 15 | Links: 16 | * [Vyper Style](https://docs.vyperlang.org/en/stable/style-guide.html) 17 | * [Curve Style](https://github.com/CurveDocs/curve-docs/pull/324) 18 | * [Ape Academy](https://academy.apeworx.io/) 19 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/interfaces/IERC721Receiver.vyi: -------------------------------------------------------------------------------- 1 | # pragma version ~=0.4.1 2 | """ 3 | @title EIP-721 Token Receiver Interface Definition 4 | @custom:contract-name IERC721Receiver 5 | @license GNU Affero General Public License v3.0 only 6 | @author pcaversaccio 7 | @notice The interface definition for any contract 8 | that wants to support safe transfers from 9 | ERC-721 asset contracts. For more details, 10 | please refer to: 11 | https://eips.ethereum.org/EIPS/eip-721#specification. 12 | 13 | On how to use interfaces in Vyper, please visit: 14 | https://vyper.readthedocs.io/en/latest/interfaces.html#interfaces. 15 | """ 16 | 17 | 18 | @external 19 | def onERC721Received( 20 | _operator: address, _from: address, _tokenId: uint256, _data: Bytes[1_024] 21 | ) -> bytes4: 22 | """ 23 | @dev Whenever a `_tokenId` token is transferred to 24 | this contract via `safeTransferFrom` by 25 | `_operator` from `_from`, this function is called. 26 | @notice It must return its function selector to 27 | confirm the token transfer. If any other value 28 | is returned or the interface is not implemented 29 | by the recipient, the transfer will be reverted. 30 | @param _operator The 20-byte address which called 31 | the `safeTransferFrom` function. 32 | @param _from The 20-byte address which previously 33 | owned the token. 34 | @param _tokenId The 32-byte identifier of the token. 35 | @param _data The maximum 1,024-byte additional data 36 | with no specified format. 37 | @return bytes4 The 4-byte function selector of `onERC721Received`. 38 | """ 39 | ... 40 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/snapshot/step-02/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | # pragma optimize codesize 4 | 5 | 6 | """ 7 | @title CRV Stablecoin Minter 8 | @license MIT 9 | @author Curve Finance 10 | @notice Mint a stablecoin backed by $CRV 11 | @dev Sample implementation of an ERC-20 backed stablecoin 12 | """ 13 | 14 | 15 | @external 16 | def mint(): 17 | """ 18 | @notice Mint a quantity of stablecoins 19 | @dev This function is intentionally not implemented! 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/snapshot/step-03/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | # pragma optimize codesize 4 | 5 | 6 | """ 7 | @title CRV Stablecoin Minter 8 | @license MIT 9 | @author Curve Finance 10 | @notice Mint a stablecoin backed by $CRV 11 | @dev Sample implementation of an ERC-20 backed stablecoin 12 | """ 13 | 14 | 15 | @external 16 | def mint(): 17 | """ 18 | @notice Mint a quantity of stablecoins 19 | @dev This function is intentionally not implemented! 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/snapshot/step-04/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | # pragma optimize codesize 4 | 5 | 6 | """ 7 | @title CRV Stablecoin Minter 8 | @license MIT 9 | @author Curve Finance 10 | @notice Mint a stablecoin backed by $CRV 11 | @dev Sample implementation of an ERC-20 backed stablecoin 12 | """ 13 | 14 | 15 | @external 16 | def mint(): 17 | """ 18 | @notice Mint a quantity of stablecoins 19 | @dev This function is intentionally not implemented! 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/snapshot/step-05/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | # pragma optimize codesize 4 | 5 | 6 | """ 7 | @title CRV Stablecoin Minter 8 | @license MIT 9 | @author Curve Finance 10 | @notice Mint a stablecoin backed by $CRV 11 | @dev Sample implementation of an ERC-20 backed stablecoin 12 | """ 13 | 14 | 15 | interface Token: 16 | def mint(owner: address, amount: uint256): nonpayable 17 | 18 | 19 | stablecoin: public(Token) 20 | 21 | 22 | @deploy 23 | def __init__(stablecoin_addr: address): 24 | self.stablecoin = Token(stablecoin_addr) 25 | 26 | 27 | @external 28 | def mint(amount: uint256): 29 | """ 30 | @notice Mint a quantity of stablecoins 31 | @param amount Amount of stablecoins to mint 32 | """ 33 | extcall self.stablecoin.mint(msg.sender, amount) 34 | -------------------------------------------------------------------------------- /berapolia-tutorial/contracts/snapshot/step-06/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.4.1 2 | 3 | # pragma optimize codesize 4 | 5 | 6 | """ 7 | @title CRV Stablecoin Minter 8 | @license MIT 9 | @author Curve Finance 10 | @notice Mint a stablecoin backed by $CRV 11 | @dev Sample implementation of an ERC-20 backed stablecoin 12 | """ 13 | 14 | import Token as token 15 | from ethereum.ercs import IERC20 16 | 17 | # crvUSD 18 | stablecoin: public(token.__interface__) 19 | 20 | # CRV 21 | lending_token: public(IERC20) 22 | 23 | 24 | @deploy 25 | def __init__(stablecoin_addr: address, lending_token_addr: address): 26 | self.stablecoin = token.__at__(stablecoin_addr) 27 | self.lending_token = IERC20(lending_token_addr) 28 | 29 | 30 | @external 31 | def mint(amount: uint256): 32 | """ 33 | @notice Mint a quantity of stablecoins 34 | @param amount Amount of stablecoins to mint 35 | """ 36 | assert ( 37 | staticcall self.lending_token.allowance(msg.sender, self) >= amount 38 | ) # dev: "!approval" 39 | assert ( 40 | staticcall self.lending_token.balanceOf(msg.sender) >= amount 41 | ) # dev: "!balance" 42 | extcall self.lending_token.transferFrom(msg.sender, self, amount) 43 | extcall self.stablecoin.mint(msg.sender, amount) 44 | -------------------------------------------------------------------------------- /berapolia-tutorial/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 100 3 | -------------------------------------------------------------------------------- /berapolia-tutorial/requirements.in: -------------------------------------------------------------------------------- 1 | vyper>=0.4.1 2 | titanoboa>=0.2.6 3 | web3 4 | 5 | # Style 6 | mamushi>=0.0.4 7 | ruff>=0.11.2 8 | -------------------------------------------------------------------------------- /berapolia-tutorial/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import boa 2 | 3 | def test_minter_exists(minter, stablecoin): 4 | assert minter.stablecoin() == stablecoin.address 5 | -------------------------------------------------------------------------------- /crvusd/arb/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /crvusd/arb/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .env 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | -------------------------------------------------------------------------------- /crvusd/peg-keeper/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /crvusd/peg-keeper/scripts/pegkeeper.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import requests 4 | from brownie import accounts, history, Contract 5 | 6 | warnings.filterwarnings("ignore", category=DeprecationWarning, module="eth_abi.codec") 7 | 8 | 9 | def main(): 10 | pegkeepers = [ 11 | "0xaA346781dDD7009caa644A4980f044C50cD2ae22", 12 | "0xE7cd2b4EB1d98CD6a4A48B6071D46401Ac7DC5C8", 13 | "0x6B765d07cf966c745B340AdCa67749fE75B5c345", 14 | "0x1ef89Ed0eDd93D1EC09E4c07373f69C49f4dcCae", 15 | ] 16 | max_val = 0 17 | best_addr = None 18 | 19 | for pegkeeper in pegkeepers: 20 | _p = Contract(pegkeeper) 21 | profit = _p.estimate_caller_profit() / 10**18 22 | print(Contract(_p.pool()).name(), profit) 23 | if profit > max_val: 24 | max_val, best_addr = profit, _p 25 | 26 | # Call update on the best pool 27 | if max_val > 0: 28 | best_addr.update({"from": accounts[0]}) 29 | lp = Contract(best_addr.pool()) 30 | print( 31 | f"Using {lp.name()} actually receives", lp.balanceOf(accounts[0]) / 10**18 32 | ) 33 | gas_price = get_gas_price() 34 | eth_price = get_eth_price() 35 | print( 36 | history[-1].gas_used * gas_price * eth_price / 10**18, 37 | f"@ {gas_price/(10 ** 9)} gwei", 38 | ) 39 | 40 | 41 | def get_eth_price(): 42 | tricrypto = Contract("0xd51a44d3fae010294c616388b506acda1bfaae46") 43 | return tricrypto.price_oracle(1) / 10**18 44 | 45 | 46 | def get_gas_price(): 47 | resp = requests.get("https://api.curve.fi/api/getGas").json() 48 | gas_price = resp["data"]["gas"]["rapid"] 49 | return gas_price 50 | -------------------------------------------------------------------------------- /lesson-01-setup/crvUSDBull.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvefi/vyper-tutorial/c7f45d979d6a87e90a3d1ce9912d4f47c5bf8aa2/lesson-01-setup/crvUSDBull.jpg -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/README.md: -------------------------------------------------------------------------------- 1 | # Introducing Brownie 2 | 3 | ## [🎥 Video 1: Introduction 🎬](https://www.youtube.com/watch?v=nkvIFE2QVp0&list=PLVOHzVzbg7bFUaOGwN0NOgkTItUAVyBBQ) 4 | 5 | Brownie is a Python-based development and testing framework for smart contracts targeting the Ethereum Virtual Machine 6 | 7 | ## Features 8 | * Full support for Solidity and Vyper 9 | * Contract testing via pytest, including trace-based coverage evaluation 10 | * Property-based and stateful testing via hypothesis 11 | * Powerful debugging tools, including python-style tracebacks and custom error strings 12 | * Built-in console for quick project interaction 13 | * Support for ethPM packages 14 | 15 | # Resources 16 | 17 | ## Tutorial Resources: 18 | * [Github](https://github.com/curvefi/brownie-tutorial) 19 | * [YouTube](https://www.youtube.com/playlist?list=PLVOHzVzbg7bFUaOGwN0NOgkTItUAVyBBQ) 20 | 21 | ## Informative 22 | * [Documentation (HTML)](https://eth-brownie.readthedocs.io/) 23 | * [Documentation (PDF)](https://eth-brownie.readthedocs.io/_/downloads/en/stable/pdf/) 24 | * [Tutorial](https://medium.com/@iamdefinitelyahuman/getting-started-with-brownie-part-1-9b2181f4cb99) 25 | 26 | ## Social Media 27 | * [Github](https://github.com/eth-brownie/brownie) 28 | * [Gitter](https://gitter.im/eth-brownie/community) 29 | * [Twitter](https://twitter.com/BrownieEth) 30 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | -------------------------------------------------------------------------------- /lesson-01-setup/vyper-token/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-02-contract/solution.diff: -------------------------------------------------------------------------------- 1 | Only in solved/contracts/: Minter.vy 2 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/contracts/Minter.bak: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | @external 4 | def mint(quantity: uint256) -> bool: 5 | return True 6 | 7 | 8 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-02-contract/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | -------------------------------------------------------------------------------- /lesson-02-contract/tests.diff: -------------------------------------------------------------------------------- 1 | diff -r ../lesson-1-setup/vyper-token/tests/conftest.py unsolved/tests/conftest.py 2 | 15a16,20 3 | > 4 | > 5 | > @pytest.fixture(scope="module") 6 | > def minter(Minter, accounts): 7 | > return Minter.deploy({"from": accounts[0]}) 8 | Only in unsolved/tests: test_minter.py 9 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-02-contract/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solution.diff: -------------------------------------------------------------------------------- 1 | Only in unsolved/contracts: .Minter.vy.swp 2 | diff unsolved/contracts/Token.vy solved/contracts/Token.vy 3 | 123a124,134 4 | > 5 | > 6 | > @external 7 | > def mint(to_addr: address, amount: uint256): 8 | > """ 9 | > @notice Mint tokens 10 | > @param to_addr Address to receive tokens 11 | > @param amount Number of tokens to mint 12 | > """ 13 | > self.totalSupply += amount 14 | > self.balances[to_addr] += amount 15 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-03-state-variables/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, accounts): 10 | quantity = 10**18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | token.mint(accounts[0], quantity) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, accounts): 18 | quantity = 10**18 19 | init_supply = token.totalSupply() 20 | 21 | token.mint(accounts[0], quantity) 22 | assert token.totalSupply() == init_supply + quantity 23 | -------------------------------------------------------------------------------- /lesson-03-state-variables/tests.diff: -------------------------------------------------------------------------------- 1 | diff -r ../lesson-02-contract/solved/tests/test_minter.py unsolved/tests/test_minter.py 2 | 6a7,22 3 | > 4 | > 5 | > def test_token_balance_updates_on_mint(token, accounts): 6 | > quantity = 10**18 7 | > init_bal = token.balanceOf(accounts[0]) 8 | > 9 | > token.mint(accounts[0], quantity) 10 | > assert token.balanceOf(accounts[0]) == init_bal + quantity 11 | > 12 | > 13 | > def test_token_total_supply_updates_on_mint(token, accounts): 14 | > quantity = 10**18 15 | > init_supply = token.totalSupply() 16 | > 17 | > token.mint(accounts[0], quantity) 18 | > assert token.totalSupply() == init_supply + quantity 19 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-03-state-variables/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import brownie 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, accounts): 10 | quantity = 10**18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | token.mint(accounts[0], quantity) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, accounts): 18 | quantity = 10**18 19 | init_supply = token.totalSupply() 20 | 21 | token.mint(accounts[0], quantity) 22 | assert token.totalSupply() == init_supply + quantity 23 | -------------------------------------------------------------------------------- /lesson-04-assert/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 4: Assert Statements 2 | 3 | ## [🎥 Video 4 Assert 🎬](https://youtu.be/GzVXWXI5JdM) 4 | 5 | Vyper relies frequently on `assert` statements add additional security. 6 | 7 | In this lesson we secure our ERC20 to restrict minting functionality only to approved addresses. We'll also cover public state variables, constructor functions, and the msg object. 8 | 9 | 10 | ## ASSERT STATEMENT 11 | 12 | An `assert` statement evaluates a condition at the current line of code. 13 | If the condition evaluates to FALSE, the transaction reverts. 14 | Optionally, an error message may be surfaced to the user. 15 | 16 | assert value == 5, "Value is not five" 17 | 18 | 19 | ## PUBLIC STATE VARIABLES 20 | 21 | If a state variable is marked as public, the compiler will automatically 22 | create an external getter function so anybody may read its value. 23 | 24 | data: public(uint256) 25 | 26 | 27 | ## CONSTRUCTOR FUNCTION 28 | 29 | `__init__` is a special initialization function called once during deployment. 30 | This function is commonly used to set initial values for storage variables. 31 | It cannot reference other contract functions. 32 | 33 | def __init__(): 34 | self.owner = msg.sender 35 | 36 | 37 | ## MESSAGE OBJECT 38 | 39 | In Vyper, `msg` is a reserved environment variable containing 40 | information about the current transaction. 41 | 42 | msg.sender # Address calling the transaction 43 | msg.value # Amount of wei sent with the message 44 | msg.gas # Remaining gas 45 | msg.data # Bytes of message calldata 46 | 47 | -------------------------------------------------------------------------------- /lesson-04-assert/solution.diff: -------------------------------------------------------------------------------- 1 | diff solved/contracts/Token.vy unsolved/contracts/Token.vy 2 | 33,34d32 3 | < owner: public(address) 4 | < minter: public(address) 5 | 43,44d40 6 | < self.owner = msg.sender 7 | < self.minter = msg.sender 8 | 137d132 9 | < assert self.minter == msg.sender 10 | 140,150d134 11 | < 12 | < 13 | < @external 14 | < def set_minter(minter_addr: address): 15 | < """ 16 | < @notice Update the address allowed to mint a token 17 | < @dev Must be called by Contract owner 18 | < @param minter_addr Address to become new minter 19 | < """ 20 | < assert self.owner == msg.sender 21 | < self.minter = minter_addr 22 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-04-assert/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | token.mint(accounts[0], quantity, {"from": token.owner()}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | token.mint(accounts[0], quantity, {"from": token.owner()}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | -------------------------------------------------------------------------------- /lesson-04-assert/tests.diff: -------------------------------------------------------------------------------- 1 | diff ../lesson-03-state-variables/solved/tests/test_minter.py unsolved/tests/test_minter.py 2 | 1d0 3 | < #!/usr/bin/python3 4 | 2a2 5 | > from brownie import * 6 | 13c13 7 | < token.mint(accounts[0], quantity) 8 | --- 9 | > token.mint(accounts[0], quantity, {"from": token.owner()}) 10 | 18c18 11 | < quantity = 10**18 12 | --- 13 | > quantity = 10 ** 18 14 | 21c21 15 | < token.mint(accounts[0], quantity) 16 | --- 17 | > token.mint(accounts[0], quantity, {"from": token.owner()}) 18 | 22a23,45 19 | > 20 | > 21 | > def test_owner_can_set_minter(token, accounts): 22 | > assert token.minter() != accounts[1] 23 | > 24 | > token.set_minter(accounts[1], {"from": token.owner()}) 25 | > assert token.minter() == accounts[1] 26 | > 27 | > 28 | > def test_nonowner_cannot_set_minter(token, accounts): 29 | > hacker = accounts[1] 30 | > assert hacker != token.owner() 31 | > 32 | > with brownie.reverts(): 33 | > token.set_minter(hacker, {"from": hacker}) 34 | > 35 | > 36 | > def test_nonminter_cannot_mint(token, accounts): 37 | > hacker = accounts[1] 38 | > assert hacker != token.minter() 39 | > 40 | > with brownie.reverts(): 41 | > token.mint(hacker, 10 ** 18, {"from": hacker}) 42 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts): 20 | return Minter.deploy({"from": accounts[0]}) 21 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-04-assert/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | token.mint(accounts[0], quantity, {"from": token.owner()}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | token.mint(accounts[0], quantity, {"from": token.owner()}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | -------------------------------------------------------------------------------- /lesson-05-interface/solution.diff: -------------------------------------------------------------------------------- 1 | 10a11,15 2 | > interface Token: 3 | > def mint(to_addr: address, amount: uint256): nonpayable 4 | > 5 | > token: public(Token) 6 | > 7 | 12c17,21 8 | < def mint(): 9 | --- 10 | > def __init__(token_addr: address): 11 | > self.token = Token(token_addr) 12 | > 13 | > @external 14 | > def mint(quantity: uint256): 15 | 15c24 16 | < @dev This function is intentionally not implemented! 17 | --- 18 | > @param quantity Quantity to mint 19 | 17c26 20 | < pass 21 | --- 22 | > self.token.mint(msg.sender, quantity) 23 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | interface Token: 12 | def mint(to_addr: address, amount: uint256): nonpayable 13 | 14 | token: public(Token) 15 | 16 | @external 17 | def __init__(token_addr: address): 18 | self.token = Token(token_addr) 19 | 20 | @external 21 | def mint(quantity: uint256): 22 | """ 23 | @notice Mint a quantity of stablecoins 24 | @param quantity Quantity to mint 25 | """ 26 | self.token.mint(msg.sender, quantity) 27 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts, token): 20 | minter = Minter.deploy(token, {"from": accounts[0]}) 21 | token.set_minter(minter, {"from": token.owner()}) 22 | return minter 23 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-05-interface/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | -------------------------------------------------------------------------------- /lesson-05-interface/tests.diff: -------------------------------------------------------------------------------- 1 | diff -r ../lesson-04-assert/solved/tests/conftest.py unsolved/tests/conftest.py 2 | 19,20c19,22 3 | < def minter(Minter, accounts): 4 | < return Minter.deploy({"from": accounts[0]}) 5 | --- 6 | > def minter(Minter, accounts, token): 7 | > minter = Minter.deploy(token, {"from": accounts[0]}) 8 | > token.set_minter(minter, {"from": token.owner()}) 9 | > return minter 10 | diff -r ../lesson-04-assert/solved/tests/test_minter.py unsolved/tests/test_minter.py 11 | 9c9 12 | < def test_token_balance_updates_on_mint(token, accounts): 13 | --- 14 | > def test_token_balance_updates_on_mint(token, minter, accounts): 15 | 13c13 16 | < token.mint(accounts[0], quantity, {"from": token.owner()}) 17 | --- 18 | > minter.mint(quantity, {"from": accounts[0]}) 19 | 17c17 20 | < def test_token_total_supply_updates_on_mint(token, accounts): 21 | --- 22 | > def test_token_total_supply_updates_on_mint(token, minter, accounts): 23 | 21c21 24 | < token.mint(accounts[0], quantity, {"from": token.owner()}) 25 | --- 26 | > minter.mint(quantity, {"from": accounts[0]}) 27 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | @external 12 | def mint(): 13 | """ 14 | @notice Mint a quantity of stablecoins 15 | @dev This function is intentionally not implemented! 16 | """ 17 | pass 18 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.fixture(scope="function", autouse=True) 7 | def isolate(fn_isolation): 8 | # perform a chain rewind after completing each test, to ensure proper isolation 9 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 10 | pass 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def token(Token, accounts): 15 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 16 | 17 | 18 | @pytest.fixture(scope="module") 19 | def minter(Minter, accounts, token): 20 | minter = Minter.deploy(token, {"from": accounts[0]}) 21 | token.set_minter(minter, {"from": token.owner()}) 22 | return minter 23 | -------------------------------------------------------------------------------- /lesson-05-interface/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | -------------------------------------------------------------------------------- /lesson-06-import/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 6: Imports 2 | 3 | ## [🎥 Video 6 : Import 🎬](https://youtu.be/Scp9SEJj-FI) 4 | 5 | In this lesson we streamline our ability to work with other contracts by importing external interfaces directly. 6 | 7 | https://vyper.readthedocs.io/en/stable/interfaces.html 8 | 9 | 10 | ## IMPORTING INTERFACES 11 | 12 | In Vyper, you can directly import an interface stored within a file. 13 | Import a Vyper contract and automatically extract the interface for you. 14 | 15 | 16 | ### ABSOLUTE IMPORTS 17 | 18 | Absolute imports must include an alias for the package or the compiler will raise an error. 19 | 20 | import as 21 | 22 | 23 | ### RELATIVE IMPORTS 24 | 25 | You can also use the "from" syntax for relative or absolute imports 26 | 27 | from import 28 | from import as 29 | 30 | 31 | ### BUILT-IN INTERFACES 32 | 33 | Vyper includes common [built-in interfaces](https://github.com/vyperlang/vyper) for common token standards. 34 | For example, you can import an ERC20 token with a simple command: 35 | 36 | from vyper.interfaces import ERC20 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /lesson-06-import/solution.diff: -------------------------------------------------------------------------------- 1 | diff -r unsolved/contracts/Minter.vy solved/contracts/Minter.vy 2 | 1c1 3 | < # @version 0.3.3 4 | --- 5 | > # @version 0.3.7 6 | 11,12c11,12 7 | < interface Token: 8 | < def mint(to_addr: address, amount: uint256): nonpayable 9 | --- 10 | > import Token as Token 11 | > from vyper.interfaces import ERC20 12 | 14c14,15 13 | < token: public(Token) 14 | --- 15 | > stablecoin: public(Token) # $crvUSD 16 | > lending_token: public(ERC20) # $CRV 17 | 17,18c18,20 18 | < def __init__(token_addr: address): 19 | < self.token = Token(token_addr) 20 | --- 21 | > def __init__(token_addr: address, lending_token_addr: address): 22 | > self.stablecoin = Token(token_addr) 23 | > self.lending_token = ERC20(lending_token_addr) 24 | 26c28,31 25 | < self.token.mint(msg.sender, quantity) 26 | --- 27 | > assert self.lending_token.allowance(msg.sender, self) >= quantity, "Lacks Approval" 28 | > assert self.lending_token.balanceOf(msg.sender) >= quantity, "Lacks Balance" 29 | > self.lending_token.transferFrom(msg.sender, self, quantity) 30 | > self.stablecoin.mint(msg.sender, quantity) 31 | diff -r unsolved/contracts/Token.vy solved/contracts/Token.vy 32 | 1c1 33 | < # @version ^0.2.0 34 | --- 35 | > # @version ^0.3.7 36 | -------------------------------------------------------------------------------- /lesson-06-import/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-06-import/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-06-import/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-06-import/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-06-import/solved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | import Token as Token 12 | from vyper.interfaces import ERC20 13 | 14 | stablecoin: public(Token) # $crvUSD 15 | lending_token: public(ERC20) # $CRV 16 | 17 | @external 18 | def __init__(token_addr: address, lending_token_addr: address): 19 | self.stablecoin = Token(token_addr) 20 | self.lending_token = ERC20(lending_token_addr) 21 | 22 | @external 23 | def mint(quantity: uint256): 24 | """ 25 | @notice Mint a quantity of stablecoins 26 | @param quantity Quantity to mint 27 | """ 28 | assert self.lending_token.allowance(msg.sender, self) >= quantity, "Lacks Approval" 29 | assert self.lending_token.balanceOf(msg.sender) >= quantity, "Lacks Balance" 30 | self.lending_token.transferFrom(msg.sender, self, quantity) 31 | self.stablecoin.mint(msg.sender, quantity) 32 | -------------------------------------------------------------------------------- /lesson-06-import/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-06-import/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-06-import/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie_tokens import MintableForkToken 5 | from brownie import network 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def isolate(fn_isolation): 9 | # perform a chain rewind after completing each test, to ensure proper isolation 10 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 11 | pass 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def token(Token, accounts): 16 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def crv(accounts, Token): 21 | if 'fork' in network.show_active(): 22 | crv = MintableForkToken('0xD533a949740bb3306d119CC777fa900bA034cd52') 23 | crv._mint_for_testing(accounts[0], 10 ** 18) 24 | else: 25 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10 ** 18, {'from': accounts[0]}) 26 | return crv 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def minter(Minter, accounts, token, crv): 31 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 32 | token.set_minter(minter, {"from": token.owner()}) 33 | crv.approve(minter, crv.balanceOf(accounts[0]), {'from': accounts[0]}) 34 | return minter 35 | -------------------------------------------------------------------------------- /lesson-06-import/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-06-import/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | 47 | def test_crv_transfers_on_mint(token, crv, accounts, minter): 48 | init_bal = crv.balanceOf(accounts[0]) 49 | minter.mint(10 ** 18, {"from": accounts[0]}) 50 | assert crv.balanceOf(accounts[0]) < init_bal 51 | -------------------------------------------------------------------------------- /lesson-06-import/tests.diff: -------------------------------------------------------------------------------- 1 | diff -r ../lesson-05-interface/solved/tests/conftest.py unsolved/tests/conftest.py 2 | 4c4,5 3 | < 4 | --- 5 | > from brownie_tokens import MintableForkToken 6 | > from brownie import network 7 | 19,20c20,31 8 | < def minter(Minter, accounts, token): 9 | < minter = Minter.deploy(token, {"from": accounts[0]}) 10 | --- 11 | > def crv(accounts, Token): 12 | > if 'fork' in network.show_active(): 13 | > crv = MintableForkToken('0xD533a949740bb3306d119CC777fa900bA034cd52') 14 | > crv._mint_for_testing(accounts[0], 10 ** 18) 15 | > else: 16 | > crv = Token.deploy("Dummy CRV", "CRV", 18, 10 ** 18, {'from': accounts[0]}) 17 | > return crv 18 | > 19 | > 20 | > @pytest.fixture(scope="module") 21 | > def minter(Minter, accounts, token, crv): 22 | > minter = Minter.deploy(token, crv, {"from": accounts[0]}) 23 | 21a33 24 | > crv.approve(minter, crv.balanceOf(accounts[0]), {'from': accounts[0]}) 25 | diff -r ../lesson-05-interface/solved/tests/test_minter.py unsolved/tests/test_minter.py 26 | 45a46,50 27 | > 28 | > def test_crv_transfers_on_mint(token, crv, accounts, minter): 29 | > init_bal = crv.balanceOf(accounts[0]) 30 | > minter.mint(10 ** 18, {"from": accounts[0]}) 31 | > assert crv.balanceOf(accounts[0]) < init_bal 32 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.3 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | interface Token: 12 | def mint(to_addr: address, amount: uint256): nonpayable 13 | 14 | token: public(Token) 15 | 16 | @external 17 | def __init__(token_addr: address): 18 | self.token = Token(token_addr) 19 | 20 | @external 21 | def mint(quantity: uint256): 22 | """ 23 | @notice Mint a quantity of stablecoins 24 | @param quantity Quantity to mint 25 | """ 26 | self.token.mint(msg.sender, quantity) 27 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/tests/.conftest.py.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvefi/vyper-tutorial/c7f45d979d6a87e90a3d1ce9912d4f47c5bf8aa2/lesson-06-import/unsolved/tests/.conftest.py.swp -------------------------------------------------------------------------------- /lesson-06-import/unsolved/tests/.test_minter.py.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvefi/vyper-tutorial/c7f45d979d6a87e90a3d1ce9912d4f47c5bf8aa2/lesson-06-import/unsolved/tests/.test_minter.py.swp -------------------------------------------------------------------------------- /lesson-06-import/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie_tokens import MintableForkToken 5 | from brownie import network 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def isolate(fn_isolation): 9 | # perform a chain rewind after completing each test, to ensure proper isolation 10 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 11 | pass 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def token(Token, accounts): 16 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 17 | 18 | 19 | @pytest.fixture(scope="module") 20 | def crv(accounts, Token): 21 | if 'fork' in network.show_active(): 22 | crv = MintableForkToken('0xD533a949740bb3306d119CC777fa900bA034cd52') 23 | crv._mint_for_testing(accounts[0], 10 ** 18) 24 | else: 25 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10 ** 18, {'from': accounts[0]}) 26 | return crv 27 | 28 | 29 | @pytest.fixture(scope="module") 30 | def minter(Minter, accounts, token, crv): 31 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 32 | token.set_minter(minter, {"from": token.owner()}) 33 | crv.approve(minter, crv.balanceOf(accounts[0]), {'from': accounts[0]}) 34 | return minter 35 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-06-import/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | 47 | def test_crv_transfers_on_mint(token, crv, accounts, minter): 48 | init_bal = crv.balanceOf(accounts[0]) 49 | minter.mint(10 ** 18, {"from": accounts[0]}) 50 | assert crv.balanceOf(accounts[0]) < init_bal 51 | -------------------------------------------------------------------------------- /lesson-07-scope/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 7: Variable Scope 2 | 3 | ## [🎥 Video 7 : Scope 🎬](https://youtu.be/w__9qQh_taE) 4 | 5 | In this lesson we'll fetch some basic price data for $CRV to serve as the oracle price. In the process we'll cover topics such as returns, function scope decorators, and local variables. 6 | 7 | Note that now we are working against contracts deployed to mainnet, so tests will work better if you run them using a mainnet fork. We also recommend installing Brownie Token Tester. Full instructions are available in unit 6 of the accompanying [Brownie Tutorial](https://github.com/curvefi/brownie-tutorial/tree/main/lesson-06-tokens). 8 | 9 | 10 | ## INTERNAL FUNCTIONS 11 | 12 | A function decorated with an [@internal decorator](https://vyper.readthedocs.io/en/stable/scoping-and-declarations.html) can only be called from within the contract. 13 | Internal functions can be called via the `self` object, and must be called after they are defined. 14 | 15 | 16 | ## VARIABLE SCOPE 17 | 18 | Vyper has three types of variables, accessible within [different scopes](https://vyper.readthedocs.io/en/stable/scoping-and-declarations.html). 19 | 20 | * Storage: data written to the blockchain, globally accessible via the `self` object 21 | * Memory: helper variables used within the scope of a single function and discarded 22 | * Calldata: arguments passed to a function by the contract caller, then discarded 23 | -------------------------------------------------------------------------------- /lesson-07-scope/solution.diff: -------------------------------------------------------------------------------- 1 | 13a14,21 2 | > interface CRVOracle: 3 | > def price_oracle() -> uint256: view 4 | > 5 | > interface ETHOracle: 6 | > def price_oracle(arg: uint256) -> uint256: view 7 | > 8 | > # Token Addresses 9 | > 10 | 16a25,29 11 | > # Oracle Addresses 12 | > 13 | > crv_eth_oracle: public(CRVOracle) 14 | > eth_usd_oracle: public(ETHOracle) 15 | > 16 | 21a35,51 17 | > # Price Oracles 18 | > self.crv_eth_oracle = CRVOracle(0x8301AE4fc9c624d1D396cbDAa1ed877821D7C511) 19 | > self.eth_usd_oracle = ETHOracle(0xD51a44d3FaE010294C616388b506AcdA1bfAAE46) 20 | > 21 | > 22 | > @internal 23 | > @view 24 | > def _price_usd() -> uint256: 25 | > token_price_eth: uint256 = self.crv_eth_oracle.price_oracle() 26 | > eth_price_usd: uint256 = self.eth_usd_oracle.price_oracle(1) 27 | > return token_price_eth * eth_price_usd / 10 ** 18 28 | > 29 | > @external 30 | > @view 31 | > def price_usd() -> uint256: 32 | > return self._price_usd() 33 | > 34 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/brownie_config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie_tokens import MintableForkToken 5 | from brownie import network 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def isolate(fn_isolation): 9 | # perform a chain rewind after completing each test, to ensure proper isolation 10 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 11 | pass 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def token(Token, accounts): 16 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 17 | 18 | @pytest.fixture(scope="module") 19 | def is_forked(): 20 | if 'fork' in network.show_active(): 21 | return True 22 | else: 23 | return False 24 | 25 | @pytest.fixture(scope="module") 26 | def crv(accounts, Token, is_forked): 27 | if is_forked: 28 | crv = MintableForkToken('0xD533a949740bb3306d119CC777fa900bA034cd52') 29 | crv._mint_for_testing(accounts[0], 10 ** 18) 30 | else: 31 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10 ** 18, {'from': accounts[0]}) 32 | return crv 33 | 34 | 35 | @pytest.fixture(scope="module") 36 | def minter(Minter, accounts, token, crv): 37 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 38 | token.set_minter(minter, {"from": token.owner()}) 39 | crv.approve(minter, crv.balanceOf(accounts[0]), {'from': accounts[0]}) 40 | return minter 41 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10 ** 19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10 ** 19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10 ** 19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10 ** 19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10 ** 19] 54 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | import requests, json 3 | import pytest 4 | 5 | def test_price_oracle_function_exists(minter): 6 | assert hasattr(minter, 'price_usd') 7 | 8 | def test_price_oracle_returns_value(minter, is_forked): 9 | if not is_forked: 10 | pytest.skip() 11 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 12 | price = json.loads(requests.get(url).content).get('curve-dao-token').get('usd') 13 | assert (minter.price_usd() / 10 ** 18) > price * .95 14 | assert (minter.price_usd() / 10 ** 18) < price * 1.05 15 | -------------------------------------------------------------------------------- /lesson-07-scope/solved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | 47 | def test_crv_transfers_on_mint(token, crv, accounts, minter): 48 | init_bal = crv.balanceOf(accounts[0]) 49 | minter.mint(10 ** 18, {"from": accounts[0]}) 50 | assert crv.balanceOf(accounts[0]) < init_bal 51 | -------------------------------------------------------------------------------- /lesson-07-scope/tests.diff: -------------------------------------------------------------------------------- 1 | diff ../lesson-06-import/solved/tests/conftest.py unsolved/tests/conftest.py 2 | 18d17 3 | < 4 | 20c19 5 | < def crv(accounts, Token): 6 | --- 7 | > def is_forked(): 8 | 21a21,27 9 | > return True 10 | > else: 11 | > return False 12 | > 13 | > @pytest.fixture(scope="module") 14 | > def crv(accounts, Token, is_forked): 15 | > if is_forked: 16 | Only in unsolved/tests/: test_lend.py 17 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/brownie_config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/contracts/Minter.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | 3 | """ 4 | @title CRV Stablecoin Minter 5 | @license MIT 6 | @author Curve Finance 7 | @notice Mint a stablecoin backed by $CRV 8 | @dev Sample implementation of an ERC-20 backed stablecoin 9 | """ 10 | 11 | import Token as Token 12 | from vyper.interfaces import ERC20 13 | 14 | stablecoin: public(Token) # $crvUSD 15 | lending_token: public(ERC20) # $CRV 16 | 17 | @external 18 | def __init__(token_addr: address, lending_token_addr: address): 19 | self.stablecoin = Token(token_addr) 20 | self.lending_token = ERC20(lending_token_addr) 21 | 22 | @external 23 | def mint(quantity: uint256): 24 | """ 25 | @notice Mint a quantity of stablecoins 26 | @param quantity Quantity to mint 27 | """ 28 | assert self.lending_token.allowance(msg.sender, self) >= quantity, "Lacks Approval" 29 | assert self.lending_token.balanceOf(msg.sender) >= quantity, "Lacks Balance" 30 | self.lending_token.transferFrom(msg.sender, self, quantity) 31 | self.stablecoin.mint(msg.sender, quantity) 32 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie_tokens import MintableForkToken 5 | from brownie import network 6 | 7 | @pytest.fixture(scope="function", autouse=True) 8 | def isolate(fn_isolation): 9 | # perform a chain rewind after completing each test, to ensure proper isolation 10 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 11 | pass 12 | 13 | 14 | @pytest.fixture(scope="module") 15 | def token(Token, accounts): 16 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 17 | 18 | @pytest.fixture(scope="module") 19 | def is_forked(): 20 | if 'fork' in network.show_active(): 21 | return True 22 | else: 23 | return False 24 | 25 | @pytest.fixture(scope="module") 26 | def crv(accounts, Token, is_forked): 27 | if is_forked: 28 | crv = MintableForkToken('0xD533a949740bb3306d119CC777fa900bA034cd52') 29 | crv._mint_for_testing(accounts[0], 10 ** 18) 30 | else: 31 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10 ** 18, {'from': accounts[0]}) 32 | return crv 33 | 34 | 35 | @pytest.fixture(scope="module") 36 | def minter(Minter, accounts, token, crv): 37 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 38 | token.set_minter(minter, {"from": token.owner()}) 39 | crv.approve(minter, crv.balanceOf(accounts[0]), {'from': accounts[0]}) 40 | return minter 41 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | from brownie import * 2 | import requests, json 3 | import pytest 4 | 5 | def test_price_oracle_function_exists(minter): 6 | assert hasattr(minter, 'price_usd') 7 | 8 | def test_price_oracle_returns_value(minter, is_forked): 9 | if not is_forked: 10 | pytest.skip() 11 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 12 | price = json.loads(requests.get(url).content).get('curve-dao-token').get('usd') 13 | assert (minter.price_usd() / 10 ** 18) > price * .95 14 | assert (minter.price_usd() / 10 ** 18) < price * 1.05 15 | -------------------------------------------------------------------------------- /lesson-07-scope/unsolved/tests/test_minter.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | 5 | def test_minter_deployed(minter): 6 | assert hasattr(minter, "mint") 7 | 8 | 9 | def test_token_balance_updates_on_mint(token, minter, accounts): 10 | quantity = 10 ** 18 11 | init_bal = token.balanceOf(accounts[0]) 12 | 13 | minter.mint(quantity, {"from": accounts[0]}) 14 | assert token.balanceOf(accounts[0]) == init_bal + quantity 15 | 16 | 17 | def test_token_total_supply_updates_on_mint(token, minter, accounts): 18 | quantity = 10 ** 18 19 | init_supply = token.totalSupply() 20 | 21 | minter.mint(quantity, {"from": accounts[0]}) 22 | assert token.totalSupply() == init_supply + quantity 23 | 24 | 25 | def test_owner_can_set_minter(token, accounts): 26 | assert token.minter() != accounts[1] 27 | 28 | token.set_minter(accounts[1], {"from": token.owner()}) 29 | assert token.minter() == accounts[1] 30 | 31 | 32 | def test_nonowner_cannot_set_minter(token, accounts): 33 | hacker = accounts[1] 34 | assert hacker != token.owner() 35 | 36 | with brownie.reverts(): 37 | token.set_minter(hacker, {"from": hacker}) 38 | 39 | 40 | def test_nonminter_cannot_mint(token, accounts): 41 | hacker = accounts[1] 42 | assert hacker != token.minter() 43 | 44 | with brownie.reverts(): 45 | token.mint(hacker, 10 ** 18, {"from": hacker}) 46 | 47 | def test_crv_transfers_on_mint(token, crv, accounts, minter): 48 | init_bal = crv.balanceOf(accounts[0]) 49 | minter.mint(10 ** 18, {"from": accounts[0]}) 50 | assert crv.balanceOf(accounts[0]) < init_bal 51 | -------------------------------------------------------------------------------- /lesson-08-math/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 8: Integer Math 2 | 3 | ## [🎥 Video 8 : Math 🎬](https://youtu.be/YL1Dg4y9CVs) 4 | 5 | In this lesson we're adding a `get_dy` function, which computes the number of $crvUSD stablecoins received for `dx`, an input of the collateral token ($CRV). 6 | 7 | In the process the lesson highlights some of the intricacies of working with integer arithmetic within smart contracts. A supplemental file TestMath.vy displayed in the video is included in the `contracts` directory. 8 | 9 | Vyper contains additional advanced math functions. For further reading: 10 | 11 | * [Vyper Built In Functions](https://vyper.readthedocs.io/en/stable/built-in-functions.html) 12 | * [Curve Optimized Library](https://github.com/bout3fiddy/boa-tricrypto/blob/main/contracts/CurveCryptoMathOptimized3.vy) 13 | 14 | 15 | ## INTEGER MATH 16 | 17 | [Arithmetic in Vyper](https://vyper.readthedocs.io/en/stable/types.html#arithmetic-operators) uses the basic operators from Python. 18 | All arithmetic operators utilize safemath by default and revert on overflow. 19 | 20 | * `x + y` Addition 21 | * `x - y` Subtraction 22 | * `-x` Unary minus/Negation 23 | * `x * y` Multiplication 24 | * `x / y` Integer Division 25 | * `x ** y` Exponentiation 26 | * `x % y` Modulo 27 | 28 | 29 | ## DECIMAL TYPES 30 | 31 | Vyper supports a [decimal variable type](https://vyper.readthedocs.io/en/stable/types.html#decimals) that renders as `fixed168x10` in the ABI. 32 | This tutorial instead follows the convention of shifting 18 decimal places and rounding it off as a uint. 33 | -------------------------------------------------------------------------------- /lesson-08-math/solution.diff: -------------------------------------------------------------------------------- 1 | 28a29,32 2 | > # Collateralization 3 | > collateral_pct: public(uint256) 4 | > 5 | > 6 | 37a42,44 7 | > # Collateralization 8 | > self.collateral_pct = 10 ** 18 * 4 / 5 9 | > 10 | 45a53,60 11 | > 12 | > @internal 13 | > @view 14 | > def _get_dy(quantity: uint256) -> uint256: 15 | > liq_price: uint256 = self._price_usd() * self.collateral_pct / 10 ** 18 16 | > return quantity * liq_price / 10 ** 18 17 | > 18 | > 19 | 54a70,81 20 | > 21 | > @external 22 | > @view 23 | > def get_dy(quantity:uint256) -> uint256: 24 | > """ 25 | > @notice Get amount of USD for a deposit of $CRV 26 | > @param quantity $CRV tokens to deposit 27 | > @return Number of $crvUSD returned 28 | > """ 29 | > return self._get_dy(quantity) 30 | > 31 | > 32 | 65c92 33 | < self.stablecoin.mint(msg.sender, quantity) 34 | --- 35 | > self.stablecoin.mint(msg.sender, self._get_dy(quantity)) 36 | -------------------------------------------------------------------------------- /lesson-08-math/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-08-math/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-08-math/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-08-math/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-08-math/solved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-08-math/solved/contracts/TestMath.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | 3 | coin_price1: public(uint256) 4 | coin_price2: public(decimal) 5 | coin_price3: public(uint256) 6 | coin_price4: public(uint256) 7 | 8 | @external 9 | def __init__(): 10 | self.coin_price1 = 69 / 100 11 | self.coin_price2 = 69.0 / 100.0 12 | self.coin_price3 = 69 / 100 * 10 ** 18 13 | self.coin_price4 = 10 ** 18 * 69 / 100 14 | 15 | @external 16 | @view 17 | def test_overflow() -> uint256: 18 | return self.coin_price4 ** 5 / 10 ** 18 19 | 20 | -------------------------------------------------------------------------------- /lesson-08-math/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-08-math/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-08-math/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-08-math/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-08-math/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/contracts/TestMath.vy: -------------------------------------------------------------------------------- 1 | # @version 0.3.7 2 | 3 | coin_price1: public(uint256) 4 | coin_price2: public(decimal) 5 | coin_price3: public(uint256) 6 | coin_price4: public(uint256) 7 | 8 | @external 9 | def __init__(): 10 | self.coin_price1 = 69 / 100 11 | self.coin_price2 = 69.0 / 100.0 12 | self.coin_price3 = 69 / 100 * 10 ** 18 13 | self.coin_price4 = 10 ** 18 * 69 / 100 14 | 15 | @external 16 | @view 17 | def test_overflow() -> uint256: 18 | return self.coin_price4 ** 5 / 10 ** 18 19 | 20 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-08-math/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-09-struct/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 9: Struct Data Types 2 | 3 | ## [🎥 Video 9 : Struct 🎬](https://youtu.be/fdo-UiKvHAo) 4 | 5 | In this lesson we're adding a repayment function along with a state variable to store important loan information. By the end of this unit, the contract will only be capable of managing a single loan -- we'll add the capacity to track several user loans in the next unit on hashmaps. 6 | 7 | This lesson introduces the `struct` datatype to manage loan parameters. Structs somewhat resemble Python objects, in that they can contain disparate data types which are readily accessible. 8 | 9 | Our solution will use a `struct` to track loan parameters including the following: 10 | 11 | ``` 12 | struct Loan: 13 | liquidation_price: uint256 14 | deposit_amount: uint256 15 | ``` 16 | 17 | In the script, we create a state variable `open_loan` instantiated as a Loan struct 18 | 19 | ``` 20 | open_loan: public(Loan) 21 | ``` 22 | 23 | We can now instantiate the variable using the following syntax. The solution file does so in the following fashion: 24 | 25 | ``` 26 | self.open_loan = Loan({liquidation_price: liq_price, deposit_amount: quantity}) 27 | ``` 28 | 29 | Now the properties of the struct are easy to access. One could easily return the liquidation price using the following:: 30 | 31 | ``` 32 | return self.open_loan.liquidation_price 33 | ``` 34 | 35 | 36 | ## FURTHER READING 37 | 38 | * [Vyper Struct Documentation](https://github.com/bout3fiddy/boa-tricrypto/blob/main/contracts/CurveCryptoMathOptimized3.vy) 39 | * [Bowtied Island: Vyper Structs and Mappings](https://bowtiedisland.com/vyper-for-beginners-structs-and-mappings/) 40 | -------------------------------------------------------------------------------- /lesson-09-struct/solution.diff: -------------------------------------------------------------------------------- 1 | 23a24,27 2 | > struct Loan: 3 | > liquidation_price: uint256 4 | > deposit_amount: uint256 5 | > 6 | 38a43 7 | > open_loan: public(Loan) 8 | 76a82,88 9 | > @internal 10 | > @view 11 | > def _repay_amount() -> uint256: 12 | > loan: Loan = self.open_loan 13 | > return loan.liquidation_price * loan.deposit_amount / 10 ** 18 14 | > 15 | > 16 | 101a114,123 17 | > @external 18 | > @view 19 | > def repay_amount() -> uint256: 20 | > """ 21 | > @notice Get repayment amount 22 | > @return Amount of tokens required to repay 23 | > """ 24 | > return self._repay_amount() 25 | > 26 | > 27 | 119a142,143 28 | > liq_price: uint256 = self._price_usd() * self.collateral_pct / 10 ** 18 29 | > self.open_loan = Loan({liquidation_price: liq_price, deposit_amount: quantity}) 30 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-09-struct/solved/tests/test_withdraw.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import * 4 | 5 | 6 | def test_repay_exists(minter): 7 | assert hasattr(minter, "repay_amount") 8 | 9 | 10 | def test_open_loan_exists(minter): 11 | assert hasattr(minter, "open_loan") 12 | 13 | 14 | def test_repay_amount_works(minter, accounts, is_forked): 15 | if not is_forked: 16 | pytest.skip() 17 | quantity = 10**18 18 | minter.mint(quantity, {"from": accounts[0]}) 19 | expected_price = minter.price_usd() * 0.8 / 10**18 20 | assert round(minter.repay_amount() / 10**18, 10) == round(expected_price, 10) 21 | 22 | 23 | def test_open_loan_works(minter, accounts, is_forked): 24 | if not is_forked: 25 | pytest.skip() 26 | quantity = 10**18 27 | minter.mint(quantity, {"from": accounts[0]}) 28 | 29 | # Check Liquidation Price 30 | assert round(minter.open_loan()[0] / 10**18, 5) == round( 31 | minter.price_usd() * 0.8 / 10**18, 5 32 | ) 33 | 34 | # Check Deposit Amount 35 | assert minter.open_loan()[1] == quantity 36 | -------------------------------------------------------------------------------- /lesson-09-struct/tests.diff: -------------------------------------------------------------------------------- 1 | Only in unsolved/tests: test_withdraw.py 2 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-09-struct/unsolved/tests/test_withdraw.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | import pytest 3 | from brownie import * 4 | 5 | 6 | def test_repay_exists(minter): 7 | assert hasattr(minter, "repay_amount") 8 | 9 | 10 | def test_open_loan_exists(minter): 11 | assert hasattr(minter, "open_loan") 12 | 13 | 14 | def test_repay_amount_works(minter, accounts, is_forked): 15 | if not is_forked: 16 | pytest.skip() 17 | quantity = 10**18 18 | minter.mint(quantity, {"from": accounts[0]}) 19 | expected_price = minter.price_usd() * 0.8 / 10**18 20 | assert round(minter.repay_amount() / 10**18, 10) == round(expected_price, 10) 21 | 22 | 23 | def test_open_loan_works(minter, accounts, is_forked): 24 | if not is_forked: 25 | pytest.skip() 26 | quantity = 10**18 27 | minter.mint(quantity, {"from": accounts[0]}) 28 | 29 | # Check Liquidation Price 30 | assert round(minter.open_loan()[0] / 10**18, 5) == round( 31 | minter.price_usd() * 0.8 / 10**18, 5 32 | ) 33 | 34 | # Check Deposit Amount 35 | assert minter.open_loan()[1] == quantity 36 | -------------------------------------------------------------------------------- /lesson-10-hashmap/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 10: HashMap Mappings 2 | 3 | ## [🎥 Video 10 : HashMap 🎬](https://youtu.be/VX1zjKTOUx0) 4 | 5 | To complete our repayment function, we upgrade our contract to manage one loan per user via a Vyper `HashMap` mapping. 6 | 7 | A `HashMap` in Vyper maps a keccak256 hash of every possible key to a value, initially set to the variable's default value (usually zero). 8 | This optimizes for quick lookup and efficient storage, but has no concept of "length" and therefore cannot be iterated over. Otherwise, a HashMap resembles a Python dictionary in its syntax. 9 | 10 | We use a mapping to upgrade our `open_loan` variable, which could only track a single user, to map any user address to its associated `Loan` struct. 11 | 12 | ``` 13 | open_loans: public(HashMap[address, Loan]) 14 | ``` 15 | 16 | Then we update every subsequent use of `open_loan` to pass an address as index as if it were a Python dictionary. 17 | 18 | ``` 19 | self.open_loans[msg.sender] = Loan({liquidation_price: liq_price, deposit_amount: quantity}) 20 | ``` 21 | 22 | We have all the tools necessary to finalize our repay function, which transfers the necessary quantity of stablecoins from the user and returns collateral. 23 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solution.diff: -------------------------------------------------------------------------------- 1 | diff unsolved/contracts/Minter.vy solved/contracts/Minter.vy 2 | 43c43 3 | < open_loan: public(Loan) 4 | --- 5 | > open_loans: public(HashMap[address, Loan]) 6 | 84,85c84,85 7 | < def _repay_amount() -> uint256: 8 | < loan: Loan = self.open_loan 9 | --- 10 | > def _repay_amount(addr: address) -> uint256: 11 | > loan: Loan = self.open_loans[addr] 12 | 116c116 13 | < def repay_amount() -> uint256: 14 | --- 15 | > def repay_amount(addr: address) -> uint256: 16 | 118a119 17 | > @param addr Address to lookup 18 | 121c122 19 | < return self._repay_amount() 20 | --- 21 | > return self._repay_amount(addr) 22 | 143c144 23 | < self.open_loan = Loan({liquidation_price: liq_price, deposit_amount: quantity}) 24 | --- 25 | > self.open_loans[msg.sender] = Loan({liquidation_price: liq_price, deposit_amount: quantity}) 26 | 146a148,168 27 | > 28 | > 29 | > @external 30 | > def repay(): 31 | > """ 32 | > @notice Repay loan in full 33 | > @dev Will revert if user lacks approvals or the balance to repay in full 34 | > """ 35 | > # Run checks 36 | > user: address = msg.sender 37 | > assert self.stablecoin.balanceOf(user) >= self._repay_amount(user) 38 | > 39 | > # Try to transfer stablecoin 40 | > self.stablecoin.transferFrom(user, self, self._repay_amount(user)) 41 | > 42 | > # Clear out the loan 43 | > quantity: uint256 = self.open_loans[user].deposit_amount 44 | > self.open_loans[user] = Loan({liquidation_price: 0, deposit_amount: 0}) 45 | > 46 | > # Return the user's collateral 47 | > self.lending_token.transfer(user, quantity) 48 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-10-hashmap/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-10-hashmap/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-11-event/solution.diff: -------------------------------------------------------------------------------- 1 | 27a28,30 2 | > event Liquidation: 3 | > user: address 4 | > loan: Loan 5 | 28a32 6 | > 7 | 88a93,98 8 | > @internal 9 | > @view 10 | > def _can_liquidate(user: address) -> bool: 11 | > return self._price_usd() < self.open_loans[user].liquidation_price 12 | > 13 | > 14 | 124a135,145 15 | > @external 16 | > @view 17 | > def can_liquidate(user: address) -> bool: 18 | > """ 19 | > @notice Is user subject to liquidation? 20 | > @param user Address of user to check 21 | > @return bool True if up for liquidation 22 | > """ 23 | > return self._can_liquidate(user) 24 | > 25 | > 26 | 168a190,207 27 | > 28 | > 29 | > @external 30 | > def liquidate(user: address): 31 | > """ 32 | > @notice Liquidate a user if token price drops below liquidation price 33 | > @param user Address to be liquidated 34 | > """ 35 | > # Verify price is below liquidation 36 | > assert self._can_liquidate(user) 37 | > 38 | > # Clear out loan data 39 | > log Liquidation(user, self.open_loans[user]) 40 | > self.open_loans[user] = Loan({liquidation_price: 0, deposit_amount: 0}) 41 | > 42 | > # Liquidate 43 | > transfer_val: uint256 = self.open_loans[user].deposit_amount 44 | > self.lending_token.transfer(msg.sender, transfer_val) 45 | -------------------------------------------------------------------------------- /lesson-11-event/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-11-event/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-11-event/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-11-event/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-11-event/solved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-11-event/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-11-event/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-11-event/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-11-event/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-11-event/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-11-event/tests.diff: -------------------------------------------------------------------------------- 1 | diff --color ../lesson-10-hashmap/solved/tests/test_withdraw.py unsolved/tests/test_withdraw.py 2 | 65a66,107 3 | > 4 | > def test_liquidate_event(minter, accounts, is_forked, token, crv): 5 | > if not is_forked: 6 | > pytest.skip() 7 | > quantity = 10**18 8 | > 9 | > user = accounts[1] 10 | > crv._mint_for_testing(user, quantity) 11 | > 12 | > init_crv = crv.balanceOf(user) 13 | > init_token = token.balanceOf(user) 14 | > expected = minter.get_dy(quantity) 15 | > 16 | > crv.approve(minter, quantity, {"from": user}) 17 | > minter.mint(quantity, {"from": user}) 18 | > 19 | > oracle = Contract(minter.crv_eth_oracle()) 20 | > init_price = minter.price_usd() 21 | > target_bal = oracle.balances(1) 22 | > crv._mint_for_testing(accounts[0], target_bal) 23 | > crv.approve(oracle, 2 ** 256 - 1, {'from': accounts[0]}) 24 | > oracle.add_liquidity([0, target_bal], 0, {'from': accounts[0]}) 25 | > 26 | > i = 0 27 | > lp = Contract('0xEd4064f376cB8d68F770FB1Ff088a3d0F3FF5c4d') 28 | > 29 | > while minter.can_liquidate(accounts[1]) == False: 30 | > i += 1 31 | > crv._mint_for_testing(accounts[0], target_bal) 32 | > oracle.exchange(1, 0, target_bal, 0, {'from': accounts[0]}) 33 | > chain.mine(timedelta=60 * 60 * 24 ) 34 | > 35 | > if i == 50: 36 | > assert False, "Could not manipulate price" 37 | > 38 | > loan_info = minter.open_loans(accounts[1]) 39 | > assert loan_info[0] > 0 40 | > assert loan_info[1] > 0 41 | > 42 | > tx = minter.liquidate(accounts[1], {'from': accounts[0]}) 43 | > assert tx.events['Liquidation']['user'] == accounts[1] 44 | > assert tx.events['Liquidation']['loan'] == loan_info 45 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-11-event/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-12-dynarray/README.md: -------------------------------------------------------------------------------- 1 | # Lesson 12: DynArray 2 | 3 | ## [🎥 Video 12 : Dynamic Arrays 🎬](https://youtu.be/txx68frMlho) 4 | 5 | With our stablecoin at feature parity, this lesson covers more advanced concepts. 6 | 7 | Specifically, we focus on upgrading our stablecoin to utilize the concept of liquidity bands from the actual $crvUSD. Instead of adding liquidity at a specific liquidation price, the real $crvUSD adds liquidity at a range of liquidity prices, and smoothly liquidates and de-liquidates as the price sweeps throughout this range. 8 | 9 | To accomplish this, we introduce two new concepts in this video. We store the concept of liquidity bands as a Vyper `dynamic array`, and store the maximum number of possible bands as a `constant`. 10 | 11 | Due to the complexity of the solution, we do not walk through the entire upgrade in the video. Our solution is stored in the `solved/` directory and may be different from your solution. We encourage you to share your solution with us for feedback. 12 | 13 | 14 | ## VYPER DYNAMIC ARRAY 15 | A variable type that functions similar to a Python array, up to a maximum length. 16 | The EVM can't process uncapped arrays, so forcing a ceiling allows for compiler optimizations. 17 | 18 | my_array: DynArray[type, max_length] 19 | 20 | 21 | ## VYPER CONSTANT 22 | A constant can be used whenever Vyper requires a fixed number, like in loops. 23 | Constants are the only state variable type defined directly on instantiation. 24 | 25 | MY_CONSTANT: constant(uint256) = 10000 26 | 27 | 28 | ## FURTHER READING 29 | 30 | * https://github.com/curvefi/curve-stablecoin 31 | * https://github.com/curvefi/curve-stablecoin-js 32 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/curvefi/vyper-tutorial/c7f45d979d6a87e90a3d1ce9912d4f47c5bf8aa2/lesson-12-dynarray/solved/.DS_Store -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-12-dynarray/solved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | pull_request: 4 | schedule: 5 | # run this workflow every Monday at 1PM UTC 6 | - cron: "* 13 * * 1" 7 | 8 | name: main workflow 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Cache Compiler Installations 22 | uses: actions/cache@v2 23 | with: 24 | path: | 25 | ~/.solcx 26 | ~/.vvm 27 | key: compiler-cache 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v1 31 | 32 | - name: Install Ganache 33 | run: npm install -g ganache-cli@6.10.1 34 | 35 | - name: Setup Python 3.8 36 | uses: actions/setup-python@v2 37 | with: 38 | python-version: 3.8 39 | 40 | - name: Install Requirements 41 | run: pip install -r requirements.txt 42 | 43 | - name: Run Tests 44 | run: brownie test -C 45 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brownie Mixes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/brownie-config.yaml: -------------------------------------------------------------------------------- 1 | autofetch_sources: true 2 | networks: 3 | default: mainnet-fork 4 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/requirements.txt: -------------------------------------------------------------------------------- 1 | eth-brownie>=1.11.0,<2.0.0 2 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/scripts/token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from brownie import Token, accounts 4 | 5 | 6 | def main(): 7 | return Token.deploy("Test Token", "TST", 18, 1e21, {'from': accounts[0]}) 8 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | from brownie import network 5 | from brownie_tokens import MintableForkToken 6 | 7 | 8 | @pytest.fixture(scope="function", autouse=True) 9 | def isolate(fn_isolation): 10 | # perform a chain rewind after completing each test, to ensure proper isolation 11 | # https://eth-brownie.readthedocs.io/en/v1.10.3/tests-pytest-intro.html#isolation-fixtures 12 | pass 13 | 14 | 15 | @pytest.fixture(scope="module") 16 | def token(Token, accounts): 17 | return Token.deploy("Test Token", "TST", 18, 1e21, {"from": accounts[0]}) 18 | 19 | 20 | @pytest.fixture(scope="module") 21 | def is_forked(): 22 | if "fork" in network.show_active(): 23 | return True 24 | else: 25 | return False 26 | 27 | 28 | @pytest.fixture(scope="module") 29 | def crv(accounts, Token, is_forked): 30 | if is_forked: 31 | crv = MintableForkToken("0xD533a949740bb3306d119CC777fa900bA034cd52") 32 | crv._mint_for_testing(accounts[0], 10**18) 33 | else: 34 | crv = Token.deploy("Dummy CRV", "CRV", 18, 10**18, {"from": accounts[0]}) 35 | return crv 36 | 37 | 38 | @pytest.fixture(scope="module") 39 | def minter(Minter, accounts, token, crv): 40 | minter = Minter.deploy(token, crv, {"from": accounts[0]}) 41 | token.set_minter(minter, {"from": token.owner()}) 42 | crv.approve(minter, crv.balanceOf(accounts[0]), {"from": accounts[0]}) 43 | return minter 44 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/tests/test_approve.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("idx", range(5)) 7 | def test_initial_approval_is_zero(token, accounts, idx): 8 | assert token.allowance(accounts[0], accounts[idx]) == 0 9 | 10 | 11 | def test_approve(token, accounts): 12 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 13 | 14 | assert token.allowance(accounts[0], accounts[1]) == 10**19 15 | 16 | 17 | def test_modify_approve(token, accounts): 18 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 19 | token.approve(accounts[1], 12345678, {"from": accounts[0]}) 20 | 21 | assert token.allowance(accounts[0], accounts[1]) == 12345678 22 | 23 | 24 | def test_revoke_approve(token, accounts): 25 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 26 | token.approve(accounts[1], 0, {"from": accounts[0]}) 27 | 28 | assert token.allowance(accounts[0], accounts[1]) == 0 29 | 30 | 31 | def test_approve_self(token, accounts): 32 | token.approve(accounts[0], 10**19, {"from": accounts[0]}) 33 | 34 | assert token.allowance(accounts[0], accounts[0]) == 10**19 35 | 36 | 37 | def test_only_affects_target(token, accounts): 38 | token.approve(accounts[1], 10**19, {"from": accounts[0]}) 39 | 40 | assert token.allowance(accounts[1], accounts[0]) == 0 41 | 42 | 43 | def test_returns_true(token, accounts): 44 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 45 | 46 | assert tx.return_value is True 47 | 48 | 49 | def test_approval_event_fires(accounts, token): 50 | tx = token.approve(accounts[1], 10**19, {"from": accounts[0]}) 51 | 52 | assert len(tx.events) == 1 53 | assert tx.events["Approval"].values() == [accounts[0], accounts[1], 10**19] 54 | -------------------------------------------------------------------------------- /lesson-12-dynarray/unsolved/tests/test_lend.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | import requests 5 | from brownie import * 6 | 7 | 8 | def test_price_oracle_function_exists(minter): 9 | assert hasattr(minter, "price_usd") 10 | 11 | 12 | def test_price_oracle_returns_value(minter, is_forked): 13 | if not is_forked: 14 | pytest.skip() 15 | url = f"https://api.coingecko.com/api/v3/simple/price?ids=curve-dao-token&vs_currencies=usd" 16 | price = json.loads(requests.get(url).content).get("curve-dao-token").get("usd") 17 | assert (minter.price_usd() / 10**18) > price * 0.95 18 | assert (minter.price_usd() / 10**18) < price * 1.05 19 | --------------------------------------------------------------------------------