├── .github └── workflows │ ├── store-kontrol-cache.yml │ ├── test-foundry.yml │ └── test-kontrol.yml ├── .gitignore ├── .gitmodules ├── README.md ├── foundry.toml ├── lemmas ├── eq-lemmas.k └── keccak-lemmas.k ├── remappings.txt ├── src ├── mocks │ └── ERC20Mock.sol ├── tokens │ ├── ERC20.sol │ ├── ERC4626.sol │ ├── MultiVault.sol │ └── WETH9.sol └── utils │ ├── FixedPointMathLib.sol │ ├── Pausable.sol │ └── SafeTransferLib.sol └── test ├── BNB.t.sol ├── ERC4626.t.sol ├── Equivalence.t.sol ├── MultiVault.t.sol └── WETH9.t.sol /.github/workflows/store-kontrol-cache.yml: -------------------------------------------------------------------------------- 1 | name: Cache Install-Kontrol 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | install-kontrol-and-cache: 8 | concurrency: kontrol-caching 9 | name: Cache Install-Kontrol 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Install Kontrol 14 | uses: runtimeverification/install-kontrol@v1.0.1 15 | with: 16 | version: latest 17 | 18 | - name: Cache Install Kontrol 19 | id: cache-install-kontrol 20 | uses: actions/cache/save@v4 21 | with: 22 | path: /nix/store 23 | key: kontrol-${{ runner.os }}-install-kontrol 24 | -------------------------------------------------------------------------------- /.github/workflows/test-foundry.yml: -------------------------------------------------------------------------------- 1 | name: Test Foundry 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - master 8 | 9 | env: 10 | FOUNDRY_PROFILE: ci 11 | 12 | jobs: 13 | check: 14 | concurrency: kontrol-ci-demo-${{ github.ref_name }} 15 | strategy: 16 | fail-fast: true 17 | 18 | name: Foundry project 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Install Foundry 26 | uses: foundry-rs/foundry-toolchain@v1 27 | with: 28 | version: nightly 29 | 30 | - name: Run Forge build 31 | run: | 32 | forge --version 33 | forge build --sizes 34 | id: build 35 | 36 | - name: Run Forge tests 37 | run: | 38 | forge test -vvv 39 | id: test 40 | -------------------------------------------------------------------------------- /.github/workflows/test-kontrol.yml: -------------------------------------------------------------------------------- 1 | name: Test Kontrol 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | FOUNDRY_PROFILE: 7 | description: 'The Foundry profile to use' 8 | required: false 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | env: 14 | FOUNDRY_PROFILE: ci 15 | 16 | jobs: 17 | kontrol-test: 18 | strategy: 19 | fail-fast: true 20 | concurrency: kontrol-ci-demo-${{ github.ref_name }} 21 | 22 | name: Kontrol Tests 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/cache/restore@v4 26 | id: restore-kontrol-cache 27 | with: 28 | path: /nix/store 29 | key: kontrol-${{ runner.os }}-install-kontrol 30 | 31 | - uses: actions/checkout@v4 32 | with: 33 | submodules: recursive 34 | 35 | - name: Install Kontrol 36 | uses: runtimeverification/install-kontrol@v1.0.1 37 | with: 38 | version: latest 39 | 40 | - name: Cache Install Kontrol 41 | id: cache-install-kontrol 42 | uses: actions/cache/save@v4 43 | with: 44 | path: /nix/store 45 | key: ${{ steps.restore-kontrol-cache.outputs.cache-primary-key}} 46 | 47 | - name: Run Kontrol 48 | run: kontrol build 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/kontrol-cheatcodes"] 5 | path = lib/kontrol-cheatcodes 6 | url = https://github.com/runtimeverification/kontrol-cheatcodes 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Secureum Workshop on Kontrol 2 | 3 | This repository contains a project that will be used during a Secureum workshop on Kontrol. Contracts present in this repository are modified for educational purposes, please DO NOT use them in production. 4 | 5 | ## Documentation 6 | 7 | Documentation and installation instructions for Kontrol can be found in [Kontrol Book](https://docs.runtimeverification.com/kontrol). Source code for Kontol is available in Kontrol [repository](https://github.com/runtimeverification/kontrol). 8 | 9 | ## Workshop Instructions 10 | 11 | ### Day Three — Introduction to Kontrol 12 | 13 | The first workshop for Kontrol is focused on basic usage of its commands in application to [WETH9](https://github.com/runtimeverification/secureum-kontrol/blob/master/src/tokens/WETH9.sol) contract and the [tests]([url](https://github.com/runtimeverification/secureum-kontrol/blob/master/test/WETH9.t.sol)) we have defined for it. 14 | 15 | ### Build 16 | 17 | To clone and build this project, run 18 | 19 | ```shell 20 | git clone https://github.com/runtimeverification/secureum-kontrol.git && cd secureum-kontrol 21 | ``` 22 | followed by 23 | ```shell 24 | kontrol build 25 | ``` 26 | This command will run `forge build` under the hood, and will, then, use the produced compilation artifacts to generate K definitions that can be verified. You can inspect the `out/` folder to view the artifacts produced by `kontrol build` and make sure that it executed successfully. 27 | 28 | ### Prove 29 | 30 | To prove that WETH9 `approve()` function changes `allowance` correctly, you can run the following command: 31 | ```shell 32 | kontrol prove --match-test 'WETH9Test.test_approve(address,address,uint256)' 33 | ``` 34 | The process should end successfully, indicating that this property is always true and, whatever the values of `from`, `to`, and `amount` are, `approve()` behaves correctly. 35 | The [test]([url](https://github.com/runtimeverification/secureum-kontrol/blob/master/test/WETH9.t.sol)) being executed is as follows: 36 | ```solidity 37 | function test_approve(address from, address to, uint256 amount) public { 38 | vm.prank(from); 39 | asset.approve(to, amount); 40 | 41 | uint256 toAllowance = asset.allowance(from, to); 42 | assert(toAllowance == amount); 43 | } 44 | ``` 45 | ### Day Four — Advanced Symbolic Testing with Kontrol 46 | 47 | The second workshop for Kontrol will cover several aspects, including [ERC4626](https://github.com/runtimeverification/secureum-kontrol/blob/master/test/ERC4626.t.sol) tests, addressing verification challenges such as loops and dynamically-sized Solidity types, and proving equivalence between Solidity and low-level gas optimized implementations for [Solady](https://github.com/runtimeverification/secureum-kontrol/blob/master/test/Equivalence.t.sol). 48 | 49 | ### Proving properties 50 | 51 | In the first part of the workshop, the following commands would help build the project and run two tests for the ERC4626 implementation we are using: 52 | ```shell 53 | kontrol build 54 | kontrol prove --match-test test_totalAssets_doesNotRevert --match-test test_totalAssets_revertsWhenPaused -j2 55 | ``` 56 | 57 | The first test, checking if `test_totalAssets_doesNotRevert` never reverts, as requested by the EIP 58 | ```solidity 59 | function test_totalAssets_doesNotRevert(address caller) public { 60 | _notBuiltinAddress(caller); 61 | vm.prank(caller); vault.totalAssets(); 62 | } 63 | ``` 64 | will revert, since the implementation of the ERC4626 vault we are using here (for illustration purposes!) reverts if the contract has been paused. 65 | 66 | ### Proving equivalence 67 | 68 | Kontrol, and FV in general, can be used to assert equivalence between different implementations of smart contract functions. To prove that a highly optimized implementation of `mulWad` funciton in Solady is equivalent to a more readable Solidity version, run 69 | ```shell 70 | kontrol build --require lemmas/eq-lemmas.k --module-import EquivalenceTest:EQ-LEMMAS 71 | ``` 72 | to build a project with lemmas that, as discussed in the workshop, are needed for the following test to succeed: 73 | ```solidity 74 | function test_mulWad(uint256 x, uint256 y) public { 75 | if (y == 0 || x <= type(uint256).max / y) { 76 | uint256 zSpec = (x * y) / WAD; 77 | uint256 zImpl = mulWad(x, y); 78 | assert(zImpl == zSpec); 79 | } else { 80 | vm.expectRevert(); 81 | this.mulWad(x, y); 82 | } 83 | } 84 | ``` 85 | Then, run 86 | ```shell 87 | kontrol prove --match-test testMulWad --smt-solver 10000 88 | ``` 89 | 90 | ### Help 91 | 92 | If you need any assistance, please reach out to us in a dedicated [Secureum Discord channel](https://discord.com/channels/814328279468474419/1221389981516304425) or [RV Discord](https://discord.gg/CurfmXNtbN). 93 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | ast = true 6 | 7 | [profile.ci] 8 | 9 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 10 | -------------------------------------------------------------------------------- /lemmas/eq-lemmas.k: -------------------------------------------------------------------------------- 1 | requires "evm.md" 2 | requires "foundry.md" 3 | 4 | module EQ-LEMMAS 5 | imports BOOL 6 | imports FOUNDRY 7 | imports INFINITE-GAS 8 | imports INT-SYMBOLIC 9 | 10 | rule bool2Word ( X ) => 1 requires X [simplification] 11 | rule bool2Word ( X ) => 0 requires notBool X [simplification] 12 | 13 | endmodule 14 | -------------------------------------------------------------------------------- /lemmas/keccak-lemmas.k: -------------------------------------------------------------------------------- 1 | requires "evm.md" 2 | requires "foundry.md" 3 | 4 | module KECCAK-LEMMAS 5 | imports BOOL 6 | imports FOUNDRY 7 | imports INFINITE-GAS 8 | imports INT-SYMBOLIC 9 | imports MAP-SYMBOLIC 10 | syntax StepSort ::= Int 11 | | Bool 12 | | Bytes 13 | | Set 14 | // ------------------------- 15 | syntax KItem ::= runLemma ( StepSort ) 16 | | doneLemma( StepSort ) 17 | rule runLemma(T) => doneLemma(T) ... 18 | // --------------------------------------------- 19 | syntax Bool ::= #notEq ( KItem, KItem ) [function, no-evaluators] 20 | // ---------------------------------------------------------------------------------------------------- 21 | 22 | // 23 | // keccak assumptions: these assumptions are not sound in principle, but are 24 | // required for verification - they should be collected at the end of execution 25 | // 26 | // ######################## 27 | // Keccak 28 | // ######################## 29 | 30 | //Required for #Ceil(#buf) 31 | rule 0 <=Int keccak( _ ) => true [simplification] 32 | rule keccak( _ ) true [simplification] 33 | 34 | // keccak does not equal a concrete value 35 | rule [keccak-eq-conc-false]: keccak(_A) ==Int _B => false [symbolic(_A), concrete(_B), simplification] 36 | rule [keccak-neq-conc-true]: keccak(_A) =/=Int _B => true [symbolic(_A), concrete(_B), simplification] 37 | 38 | // corollary of `keccak-eq-conc-false` 39 | rule [keccak-eq-conc-false-extended]: 40 | ( ( keccak ( _X ) +Int _A ) modInt pow256 ) ==Int _Y => false 41 | [simplification, symbolic(_X), concrete(_A, _Y)] 42 | 43 | // keccak is injective 44 | rule [keccak-inj]: keccak(A) ==Int keccak(B) => A ==K B [simplification] 45 | 46 | // keccak has no "fixpoint" 47 | rule [keccak-no-fix-eq-false]: #buf(32, keccak(X)) ==K X => false [simplification] 48 | rule [keccak-no-fix-neq-true]: #buf(32, keccak(X)) =/=K X => true [simplification] 49 | 50 | // disequality of keccak under shifting 51 | rule ( ( keccak ( _X ) +Int A ) modInt pow256 ) ==Int keccak ( _Y ) => false 52 | requires 0 M [simplification] 57 | 58 | endmodule 59 | -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | forge-std/=lib/forge-std/src/ 2 | -------------------------------------------------------------------------------- /src/mocks/ERC20Mock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.20; 3 | 4 | import {ERC20} from "../tokens/ERC20.sol"; 5 | 6 | /// @author copied from openzeppelin-contracts (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/mocks/token/ERC20Mock.sol) 7 | contract ERC20Mock is ERC20 { 8 | constructor() ERC20("ERC20Mock", "E20M", 18) {} 9 | 10 | function mint(address account, uint256 amount) external { 11 | _mint(account, amount); 12 | } 13 | 14 | function burn(address account, uint256 amount) external { 15 | _burn(account, amount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tokens/ERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | /// @notice Modern and gas efficient ERC20 5 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC20.sol) 6 | /// @author Modified from Uniswap (https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) 7 | /// @dev Do not manually set balances without updating totalSupply, as the sum of all user balances must not exceed it. 8 | abstract contract ERC20 { 9 | /*////////////////////////////////////////////////////////////// 10 | EVENTS 11 | //////////////////////////////////////////////////////////////*/ 12 | 13 | event Transfer(address indexed from, address indexed to, uint256 amount); 14 | 15 | event Approval(address indexed owner, address indexed spender, uint256 amount); 16 | 17 | /*////////////////////////////////////////////////////////////// 18 | METADATA STORAGE 19 | //////////////////////////////////////////////////////////////*/ 20 | 21 | string public name; 22 | 23 | string public symbol; 24 | 25 | uint8 public immutable decimals; 26 | 27 | /*////////////////////////////////////////////////////////////// 28 | ERC20 STORAGE 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | uint256 public totalSupply; 32 | 33 | mapping(address => uint256) public balanceOf; 34 | 35 | mapping(address => mapping(address => uint256)) public allowance; 36 | 37 | /*////////////////////////////////////////////////////////////// 38 | CONSTRUCTOR 39 | //////////////////////////////////////////////////////////////*/ 40 | 41 | constructor( 42 | string memory _name, 43 | string memory _symbol, 44 | uint8 _decimals 45 | ) { 46 | name = _name; 47 | symbol = _symbol; 48 | decimals = _decimals; 49 | } 50 | 51 | /*////////////////////////////////////////////////////////////// 52 | ERC20 LOGIC 53 | //////////////////////////////////////////////////////////////*/ 54 | 55 | function approve(address spender, uint256 amount) public virtual returns (bool) { 56 | allowance[msg.sender][spender] = amount; 57 | 58 | emit Approval(msg.sender, spender, amount); 59 | 60 | return true; 61 | } 62 | 63 | function transfer(address to, uint256 amount) public virtual returns (bool) { 64 | balanceOf[msg.sender] -= amount; 65 | 66 | // Cannot overflow because the sum of all user 67 | // balances can't exceed the max uint256 value. 68 | unchecked { 69 | balanceOf[to] += amount; 70 | } 71 | 72 | emit Transfer(msg.sender, to, amount); 73 | 74 | return true; 75 | } 76 | 77 | function transferFrom( 78 | address from, 79 | address to, 80 | uint256 amount 81 | ) public virtual returns (bool) { 82 | uint256 allowed = allowance[from][msg.sender]; // Saves gas for limited approvals. 83 | 84 | if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount; 85 | 86 | balanceOf[from] -= amount; 87 | 88 | // Cannot overflow because the sum of all user 89 | // balances can't exceed the max uint256 value. 90 | unchecked { 91 | balanceOf[to] += amount; 92 | } 93 | 94 | emit Transfer(from, to, amount); 95 | 96 | return true; 97 | } 98 | 99 | /*////////////////////////////////////////////////////////////// 100 | INTERNAL MINT/BURN LOGIC 101 | //////////////////////////////////////////////////////////////*/ 102 | 103 | function _mint(address to, uint256 amount) internal virtual { 104 | totalSupply += amount; 105 | 106 | // Cannot overflow because the sum of all user 107 | // balances can't exceed the max uint256 value. 108 | unchecked { 109 | balanceOf[to] += amount; 110 | } 111 | 112 | emit Transfer(address(0), to, amount); 113 | } 114 | 115 | function _burn(address from, uint256 amount) internal virtual { 116 | balanceOf[from] -= amount; 117 | 118 | // Cannot underflow because a user's balance 119 | // will never be larger than the total supply. 120 | unchecked { 121 | totalSupply -= amount; 122 | } 123 | 124 | emit Transfer(from, address(0), amount); 125 | } 126 | } -------------------------------------------------------------------------------- /src/tokens/ERC4626.sol: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | /// !!! WARNING !!! /// 3 | /// THIS CONTRACT CONTAINS VULNERABILITIES. DO NOT USE OR DEPLOY. /// 4 | /// IT WAS CREATED FOR EDUCATIONAL PURPOSES. /// 5 | /////////////////////////////////////////////////////////////////////////////// 6 | 7 | // SPDX-License-Identifier: AGPL-3.0-only 8 | pragma solidity >=0.8.0; 9 | 10 | import {ERC20} from "./ERC20.sol"; 11 | import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; 12 | import {FixedPointMathLib} from "../utils/FixedPointMathLib.sol"; 13 | import {Pausable} from "../utils/Pausable.sol"; 14 | 15 | /// @notice Minimal ERC4626 tokenized Vault implementation. 16 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC4626.sol) 17 | contract ERC4626 is ERC20, Pausable { 18 | using SafeTransferLib for ERC20; 19 | using FixedPointMathLib for uint256; 20 | 21 | event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares); 22 | 23 | event Withdraw( 24 | address indexed caller, 25 | address indexed receiver, 26 | address indexed owner, 27 | uint256 assets, 28 | uint256 shares 29 | ); 30 | 31 | ERC20 public immutable asset; 32 | 33 | constructor( 34 | ERC20 _asset, 35 | string memory _name, 36 | string memory _symbol 37 | // Fixed decimals: 38 | ) ERC20(_name, _symbol, 18) { 39 | asset = _asset; 40 | } 41 | 42 | function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { 43 | require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); 44 | asset.safeTransferFrom(msg.sender, address(this), assets); 45 | _mint(receiver, shares); 46 | emit Deposit(msg.sender, receiver, assets, shares); 47 | } 48 | 49 | function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { 50 | assets = previewMint(shares); 51 | asset.safeTransferFrom(msg.sender, address(this), assets); 52 | _mint(receiver, shares); 53 | emit Deposit(msg.sender, receiver, assets, shares); 54 | } 55 | 56 | function withdraw( 57 | uint256 assets, 58 | address receiver, 59 | address owner 60 | ) public virtual returns (uint256 shares) { 61 | shares = previewWithdraw(assets); 62 | if (msg.sender != owner) { 63 | uint256 allowed = allowance[owner][msg.sender]; 64 | if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; 65 | } 66 | _burn(owner, shares); 67 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 68 | asset.safeTransfer(receiver, assets); 69 | } 70 | 71 | function redeem( 72 | uint256 shares, 73 | address receiver, 74 | address owner 75 | ) public virtual returns (uint256 assets) { 76 | if (msg.sender != owner) { 77 | uint256 allowed = allowance[owner][msg.sender]; 78 | if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; 79 | } 80 | require((assets = previewRedeem(shares)) != 0, "ZERO_ASSETS"); 81 | _burn(owner, shares); 82 | emit Withdraw(msg.sender, receiver, owner, assets, shares); 83 | asset.safeTransfer(receiver, assets); 84 | } 85 | 86 | // WARN: `whenNotPaused` is added for illustration purposes 87 | function totalAssets() public view virtual whenNotPaused returns (uint256) { 88 | return asset.balanceOf(address(this)); 89 | } 90 | 91 | function convertToShares(uint256 assets) public view virtual returns (uint256) { 92 | return totalSupply == 0 ? assets : assets.mulDivDown(totalSupply, totalAssets()); 93 | } 94 | 95 | function convertToAssets(uint256 shares) public view virtual returns (uint256) { 96 | return totalSupply == 0 ? shares : shares.mulDivDown(totalAssets(), totalSupply); 97 | } 98 | 99 | function previewDeposit(uint256 assets) public view virtual returns (uint256) { 100 | return convertToShares(assets); 101 | } 102 | 103 | function previewMint(uint256 shares) public view virtual returns (uint256) { 104 | return totalSupply == 0 ? shares : shares.mulDivUp(totalAssets(), totalSupply); 105 | } 106 | 107 | function previewWithdraw(uint256 assets) public view virtual returns (uint256) { 108 | // Flipped rounding direction: 109 | return totalSupply == 0 ? assets : assets.mulDivUp(totalSupply, totalAssets()); 110 | } 111 | 112 | function previewRedeem(uint256 shares) public view virtual returns (uint256) { 113 | return convertToAssets(shares); 114 | } 115 | 116 | function maxDeposit(address) public view virtual returns (uint256) { 117 | return type(uint256).max; 118 | } 119 | 120 | function maxMint(address) public view virtual returns (uint256) { 121 | return type(uint256).max; 122 | } 123 | 124 | function maxWithdraw(address owner) public view virtual returns (uint256) { 125 | return convertToAssets(balanceOf[owner]); 126 | } 127 | 128 | function maxRedeem(address owner) public view virtual returns (uint256) { 129 | return balanceOf[owner]; 130 | } 131 | } -------------------------------------------------------------------------------- /src/tokens/MultiVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.1; 3 | 4 | import {ERC20} from "./ERC20.sol"; 5 | import {SafeTransferLib} from "../utils/SafeTransferLib.sol"; 6 | import {FixedPointMathLib} from "../utils/FixedPointMathLib.sol"; 7 | 8 | /// @notice Prototype of ERC4626 MultiVault extension 9 | /// @author Modified from MultiVault (https://github.com/z0r0z/MultiVault/blob/main/src/MultiVault.sol) 10 | contract MultiVault { 11 | using SafeTransferLib for ERC20; 12 | using FixedPointMathLib for uint256; 13 | 14 | /*/////////////////////////////////////////////////////////////// 15 | EVENTS 16 | //////////////////////////////////////////////////////////////*/ 17 | 18 | event Create(address indexed asset, uint256 id); 19 | 20 | /*/////////////////////////////////////////////////////////////// 21 | STORAGE 22 | //////////////////////////////////////////////////////////////*/ 23 | 24 | /// @notice Vault id tracking 25 | uint256 public id; 26 | 27 | /// @notice Multiple vault underlying assets 28 | mapping(uint256 => Vault) public vaults; 29 | 30 | struct Vault { 31 | address asset; 32 | uint256 totalSupply; 33 | bytes vaultData; 34 | } 35 | 36 | /*/////////////////////////////////////////////////////////////// 37 | MULTIVAULT LOGIC 38 | //////////////////////////////////////////////////////////////*/ 39 | 40 | /// @notice Create new Vault 41 | /// @param asset new underlying token for vaultId 42 | function create(address asset, uint256 totalSupply, bytes calldata data) public returns (bool) { 43 | vaults[id].asset = asset; 44 | vaults[id].totalSupply = totalSupply; 45 | vaults[id].vaultData = data; 46 | id += 1; 47 | 48 | emit Create(asset, id); 49 | 50 | return true; 51 | } 52 | 53 | // the rest of the logic is omitted 54 | } -------------------------------------------------------------------------------- /src/tokens/WETH9.sol: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2015, 2016, 2017 Dapphub 2 | 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU General Public License for more details. 12 | 13 | // You should have received a copy of the GNU General Public License 14 | // along with this program. If not, see . 15 | 16 | pragma solidity ^0.8.13; 17 | 18 | contract WETH9 { 19 | string public name = "Wrapped Ether"; 20 | string public symbol = "WETH"; 21 | uint8 public decimals = 18; 22 | 23 | event Approval(address indexed src, address indexed guy, uint256 wad); 24 | event Transfer(address indexed src, address indexed dst, uint256 wad); 25 | event Deposit(address indexed dst, uint256 wad); 26 | event Withdrawal(address indexed src, uint256 wad); 27 | 28 | mapping(address => uint256) public balanceOf; 29 | mapping(address => mapping(address => uint256)) public allowance; 30 | 31 | fallback() external payable { 32 | deposit(); 33 | } 34 | 35 | function deposit() public payable { 36 | balanceOf[msg.sender] += msg.value; 37 | emit Deposit(msg.sender, msg.value); 38 | } 39 | 40 | function withdraw(uint256 wad) public { 41 | require(balanceOf[msg.sender] >= wad); 42 | balanceOf[msg.sender] -= wad; 43 | payable(msg.sender).transfer(wad); 44 | emit Withdrawal(msg.sender, wad); 45 | } 46 | 47 | function totalSupply() public view returns (uint256) { 48 | return address(this).balance; 49 | } 50 | 51 | function approve(address guy, uint256 wad) public returns (bool) { 52 | allowance[msg.sender][guy] = wad; 53 | emit Approval(msg.sender, guy, wad); 54 | return true; 55 | } 56 | 57 | function transfer(address dst, uint256 wad) public returns (bool) { 58 | return transferFrom(msg.sender, dst, wad); 59 | } 60 | 61 | function transferFrom(address src, address dst, uint256 wad) public returns (bool) { 62 | require(balanceOf[src] >= wad); 63 | 64 | if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) { 65 | require(allowance[src][msg.sender] >= wad); 66 | allowance[src][msg.sender] -= wad; 67 | } 68 | 69 | balanceOf[src] -= wad; 70 | balanceOf[dst] += wad; 71 | 72 | emit Transfer(src, dst, wad); 73 | 74 | return true; 75 | } 76 | } -------------------------------------------------------------------------------- /src/utils/FixedPointMathLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | /// @notice Arithmetic library with operations for fixed-point numbers. 5 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) 6 | /// @author Inspired by USM (https://github.com/usmfum/USM/blob/master/contracts/WadMath.sol) 7 | library FixedPointMathLib { 8 | /*////////////////////////////////////////////////////////////// 9 | SIMPLIFIED FIXED POINT OPERATIONS 10 | //////////////////////////////////////////////////////////////*/ 11 | 12 | uint256 internal constant MAX_UINT256 = 2**256 - 1; 13 | 14 | uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. 15 | 16 | function mulWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 17 | return mulDivDown(x, y, WAD); // Equivalent to (x * y) / WAD rounded down. 18 | } 19 | 20 | function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 21 | return mulDivUp(x, y, WAD); // Equivalent to (x * y) / WAD rounded up. 22 | } 23 | 24 | function divWadDown(uint256 x, uint256 y) internal pure returns (uint256) { 25 | return mulDivDown(x, WAD, y); // Equivalent to (x * WAD) / y rounded down. 26 | } 27 | 28 | function divWadUp(uint256 x, uint256 y) internal pure returns (uint256) { 29 | return mulDivUp(x, WAD, y); // Equivalent to (x * WAD) / y rounded up. 30 | } 31 | 32 | /*////////////////////////////////////////////////////////////// 33 | LOW LEVEL FIXED POINT OPERATIONS 34 | //////////////////////////////////////////////////////////////*/ 35 | 36 | function mulDivDown( 37 | uint256 x, 38 | uint256 y, 39 | uint256 denominator 40 | ) internal pure returns (uint256 z) { 41 | /// @solidity memory-safe-assembly 42 | assembly { 43 | // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) 44 | if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { 45 | revert(0, 0) 46 | } 47 | 48 | // Divide x * y by the denominator. 49 | z := div(mul(x, y), denominator) 50 | } 51 | } 52 | 53 | function mulDivUp( 54 | uint256 x, 55 | uint256 y, 56 | uint256 denominator 57 | ) internal pure returns (uint256 z) { 58 | /// @solidity memory-safe-assembly 59 | assembly { 60 | // Equivalent to require(denominator != 0 && (y == 0 || x <= type(uint256).max / y)) 61 | if iszero(mul(denominator, iszero(mul(y, gt(x, div(MAX_UINT256, y)))))) { 62 | revert(0, 0) 63 | } 64 | 65 | // If x * y modulo the denominator is strictly greater than 0, 66 | // 1 is added to round up the division of x * y by the denominator. 67 | z := add(gt(mod(mul(x, y), denominator), 0), div(mul(x, y), denominator)) 68 | } 69 | } 70 | 71 | function rpow( 72 | uint256 x, 73 | uint256 n, 74 | uint256 scalar 75 | ) internal pure returns (uint256 z) { 76 | /// @solidity memory-safe-assembly 77 | assembly { 78 | switch x 79 | case 0 { 80 | switch n 81 | case 0 { 82 | // 0 ** 0 = 1 83 | z := scalar 84 | } 85 | default { 86 | // 0 ** n = 0 87 | z := 0 88 | } 89 | } 90 | default { 91 | switch mod(n, 2) 92 | case 0 { 93 | // If n is even, store scalar in z for now. 94 | z := scalar 95 | } 96 | default { 97 | // If n is odd, store x in z for now. 98 | z := x 99 | } 100 | 101 | // Shifting right by 1 is like dividing by 2. 102 | let half := shr(1, scalar) 103 | 104 | for { 105 | // Shift n right by 1 before looping to halve it. 106 | n := shr(1, n) 107 | } n { 108 | // Shift n right by 1 each iteration to halve it. 109 | n := shr(1, n) 110 | } { 111 | // Revert immediately if x ** 2 would overflow. 112 | // Equivalent to iszero(eq(div(xx, x), x)) here. 113 | if shr(128, x) { 114 | revert(0, 0) 115 | } 116 | 117 | // Store x squared. 118 | let xx := mul(x, x) 119 | 120 | // Round to the nearest number. 121 | let xxRound := add(xx, half) 122 | 123 | // Revert if xx + half overflowed. 124 | if lt(xxRound, xx) { 125 | revert(0, 0) 126 | } 127 | 128 | // Set x to scaled xxRound. 129 | x := div(xxRound, scalar) 130 | 131 | // If n is even: 132 | if mod(n, 2) { 133 | // Compute z * x. 134 | let zx := mul(z, x) 135 | 136 | // If z * x overflowed: 137 | if iszero(eq(div(zx, x), z)) { 138 | // Revert if x is non-zero. 139 | if iszero(iszero(x)) { 140 | revert(0, 0) 141 | } 142 | } 143 | 144 | // Round to the nearest number. 145 | let zxRound := add(zx, half) 146 | 147 | // Revert if zx + half overflowed. 148 | if lt(zxRound, zx) { 149 | revert(0, 0) 150 | } 151 | 152 | // Return properly scaled zxRound. 153 | z := div(zxRound, scalar) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | 160 | /*////////////////////////////////////////////////////////////// 161 | GENERAL NUMBER UTILITIES 162 | //////////////////////////////////////////////////////////////*/ 163 | 164 | function sqrt(uint256 x) internal pure returns (uint256 z) { 165 | /// @solidity memory-safe-assembly 166 | assembly { 167 | let y := x // We start y at x, which will help us make our initial estimate. 168 | 169 | z := 181 // The "correct" value is 1, but this saves a multiplication later. 170 | 171 | // This segment is to get a reasonable initial estimate for the Babylonian method. With a bad 172 | // start, the correct # of bits increases ~linearly each iteration instead of ~quadratically. 173 | 174 | // We check y >= 2^(k + 8) but shift right by k bits 175 | // each branch to ensure that if x >= 256, then y >= 256. 176 | if iszero(lt(y, 0x10000000000000000000000000000000000)) { 177 | y := shr(128, y) 178 | z := shl(64, z) 179 | } 180 | if iszero(lt(y, 0x1000000000000000000)) { 181 | y := shr(64, y) 182 | z := shl(32, z) 183 | } 184 | if iszero(lt(y, 0x10000000000)) { 185 | y := shr(32, y) 186 | z := shl(16, z) 187 | } 188 | if iszero(lt(y, 0x1000000)) { 189 | y := shr(16, y) 190 | z := shl(8, z) 191 | } 192 | 193 | // Goal was to get z*z*y within a small factor of x. More iterations could 194 | // get y in a tighter range. Currently, we will have y in [256, 256*2^16). 195 | // We ensured y >= 256 so that the relative difference between y and y+1 is small. 196 | // That's not possible if x < 256 but we can just verify those cases exhaustively. 197 | 198 | // Now, z*z*y <= x < z*z*(y+1), and y <= 2^(16+8), and either y >= 256, or x < 256. 199 | // Correctness can be checked exhaustively for x < 256, so we assume y >= 256. 200 | // Then z*sqrt(y) is within sqrt(257)/sqrt(256) of sqrt(x), or about 20bps. 201 | 202 | // For s in the range [1/256, 256], the estimate f(s) = (181/1024) * (s+1) is in the range 203 | // (1/2.84 * sqrt(s), 2.84 * sqrt(s)), with largest error when s = 1 and when s = 256 or 1/256. 204 | 205 | // Since y is in [256, 256*2^16), let a = y/65536, so that a is in [1/256, 256). Then we can estimate 206 | // sqrt(y) using sqrt(65536) * 181/1024 * (a + 1) = 181/4 * (y + 65536)/65536 = 181 * (y + 65536)/2^18. 207 | 208 | // There is no overflow risk here since y < 2^136 after the first branch above. 209 | z := shr(18, mul(z, add(y, 65536))) // A mul() is saved from starting z at 181. 210 | 211 | // Given the worst case multiplicative error of 2.84 above, 7 iterations should be enough. 212 | z := shr(1, add(z, div(x, z))) 213 | z := shr(1, add(z, div(x, z))) 214 | z := shr(1, add(z, div(x, z))) 215 | z := shr(1, add(z, div(x, z))) 216 | z := shr(1, add(z, div(x, z))) 217 | z := shr(1, add(z, div(x, z))) 218 | z := shr(1, add(z, div(x, z))) 219 | 220 | // If x+1 is a perfect square, the Babylonian method cycles between 221 | // floor(sqrt(x)) and ceil(sqrt(x)). This statement ensures we return floor. 222 | // See: https://en.wikipedia.org/wiki/Integer_square_root#Using_only_integer_division 223 | // Since the ceil is rare, we save gas on the assignment and repeat division in the rare case. 224 | // If you don't care whether the floor or ceil square root is returned, you can remove this statement. 225 | z := sub(z, lt(div(x, z), z)) 226 | } 227 | } 228 | 229 | function unsafeMod(uint256 x, uint256 y) internal pure returns (uint256 z) { 230 | /// @solidity memory-safe-assembly 231 | assembly { 232 | // Mod x by y. Note this will return 233 | // 0 instead of reverting if y is zero. 234 | z := mod(x, y) 235 | } 236 | } 237 | 238 | function unsafeDiv(uint256 x, uint256 y) internal pure returns (uint256 r) { 239 | /// @solidity memory-safe-assembly 240 | assembly { 241 | // Divide x by y. Note this will return 242 | // 0 instead of reverting if y is zero. 243 | r := div(x, y) 244 | } 245 | } 246 | 247 | function unsafeDivUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 248 | /// @solidity memory-safe-assembly 249 | assembly { 250 | // Add 1 to x * y if x % y > 0. Note this will 251 | // return 0 instead of reverting if y is zero. 252 | z := add(gt(mod(x, y), 0), div(x, y)) 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/utils/Pausable.sol: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | /// !!! WARNING !!! /// 3 | /// THIS CONTRACT CONTAINS VULNERABILITIES. DO NOT USE OR DEPLOY. /// 4 | /// IT WAS CREATED FOR EDUCATIONAL PURPOSES. /// 5 | /////////////////////////////////////////////////////////////////////////////// 6 | 7 | // SPDX-License-Identifier: MIT 8 | // Modified OpenZeppelin Contracts (last updated v5.0.0) (utils/Pausable.sol) 9 | 10 | pragma solidity ^0.8.20; 11 | 12 | /** 13 | * @author Modified from openzeppelin-contracts (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Pausable.sol) 14 | * @dev Contract module which allows children to implement an emergency stop 15 | * mechanism that can be triggered by an authorized account. 16 | */ 17 | abstract contract Pausable { 18 | // WARN: `paused` is made public for illustration purposes 19 | bool public paused; 20 | 21 | /** 22 | * @dev Emitted when the pause is triggered by `account`. 23 | */ 24 | event Paused(address account); 25 | 26 | /** 27 | * @dev Emitted when the pause is lifted by `account`. 28 | */ 29 | event Unpaused(address account); 30 | 31 | /** 32 | * @dev The operation failed because the contract is paused. 33 | */ 34 | error EnforcedPause(); 35 | 36 | /** 37 | * @dev The operation failed because the contract is not paused. 38 | */ 39 | error ExpectedPause(); 40 | 41 | /** 42 | * @dev Initializes the contract in unpaused state. 43 | */ 44 | constructor() { 45 | paused = false; 46 | } 47 | 48 | /** 49 | * @dev Modifier to make a function callable only when the contract is not paused. 50 | * 51 | * Requirements: 52 | * 53 | * - The contract must not be paused. 54 | */ 55 | modifier whenNotPaused() { 56 | _requireNotPaused(); 57 | _; 58 | } 59 | 60 | /** 61 | * @dev Modifier to make a function callable only when the contract is paused. 62 | * 63 | * Requirements: 64 | * 65 | * - The contract must be paused. 66 | */ 67 | modifier whenPaused() { 68 | _requirePaused(); 69 | _; 70 | } 71 | 72 | // WARN: modified for illustration puposes 73 | /** 74 | * @dev Triggers stopped state 75 | */ 76 | function pause() public virtual { 77 | paused = true; 78 | emit Paused(msg.sender); 79 | } 80 | 81 | /** 82 | * @dev Returns to normal state. 83 | */ 84 | function _unpause() public virtual { 85 | paused = false; 86 | emit Unpaused(msg.sender); 87 | } 88 | 89 | /** 90 | * @dev Throws if the contract is paused. 91 | */ 92 | function _requireNotPaused() internal view virtual { 93 | if (paused) { 94 | revert EnforcedPause(); 95 | } 96 | } 97 | 98 | /** 99 | * @dev Throws if the contract is not paused. 100 | */ 101 | function _requirePaused() internal view virtual { 102 | if (!paused) { 103 | revert ExpectedPause(); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/utils/SafeTransferLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: AGPL-3.0-only 2 | pragma solidity >=0.8.0; 3 | 4 | import {ERC20} from "../tokens/ERC20.sol"; 5 | 6 | /// @notice Safe ETH and ERC20 transfer library that gracefully handles missing return values. 7 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol) 8 | /// @dev Use with caution! Some functions in this library knowingly create dirty bits at the destination of the free memory pointer. 9 | /// @dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller. 10 | library SafeTransferLib { 11 | /*////////////////////////////////////////////////////////////// 12 | ETH OPERATIONS 13 | //////////////////////////////////////////////////////////////*/ 14 | 15 | function safeTransferETH(address to, uint256 amount) internal { 16 | bool success; 17 | 18 | /// @solidity memory-safe-assembly 19 | assembly { 20 | // Transfer the ETH and store if it succeeded or not. 21 | success := call(gas(), to, amount, 0, 0, 0, 0) 22 | } 23 | 24 | require(success, "ETH_TRANSFER_FAILED"); 25 | } 26 | 27 | /*////////////////////////////////////////////////////////////// 28 | ERC20 OPERATIONS 29 | //////////////////////////////////////////////////////////////*/ 30 | 31 | function safeTransferFrom( 32 | ERC20 token, 33 | address from, 34 | address to, 35 | uint256 amount 36 | ) internal { 37 | bool success; 38 | 39 | /// @solidity memory-safe-assembly 40 | assembly { 41 | // Get a pointer to some free memory. 42 | let freeMemoryPointer := mload(0x40) 43 | 44 | // Write the abi-encoded calldata into memory, beginning with the function selector. 45 | mstore(freeMemoryPointer, 0x23b872dd00000000000000000000000000000000000000000000000000000000) 46 | mstore(add(freeMemoryPointer, 4), and(from, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "from" argument. 47 | mstore(add(freeMemoryPointer, 36), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. 48 | mstore(add(freeMemoryPointer, 68), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. 49 | 50 | success := and( 51 | // Set success to whether the call reverted, if not we check it either 52 | // returned exactly 1 (can't just be non-zero data), or had no return data. 53 | or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), 54 | // We use 100 because the length of our calldata totals up like so: 4 + 32 * 3. 55 | // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. 56 | // Counterintuitively, this call must be positioned second to the or() call in the 57 | // surrounding and() call or else returndatasize() will be zero during the computation. 58 | call(gas(), token, 0, freeMemoryPointer, 100, 0, 32) 59 | ) 60 | } 61 | 62 | require(success, "TRANSFER_FROM_FAILED"); 63 | } 64 | 65 | function safeTransfer( 66 | ERC20 token, 67 | address to, 68 | uint256 amount 69 | ) internal { 70 | bool success; 71 | 72 | /// @solidity memory-safe-assembly 73 | assembly { 74 | // Get a pointer to some free memory. 75 | let freeMemoryPointer := mload(0x40) 76 | 77 | // Write the abi-encoded calldata into memory, beginning with the function selector. 78 | mstore(freeMemoryPointer, 0xa9059cbb00000000000000000000000000000000000000000000000000000000) 79 | mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. 80 | mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. 81 | 82 | success := and( 83 | // Set success to whether the call reverted, if not we check it either 84 | // returned exactly 1 (can't just be non-zero data), or had no return data. 85 | or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), 86 | // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. 87 | // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. 88 | // Counterintuitively, this call must be positioned second to the or() call in the 89 | // surrounding and() call or else returndatasize() will be zero during the computation. 90 | call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) 91 | ) 92 | } 93 | 94 | require(success, "TRANSFER_FAILED"); 95 | } 96 | 97 | function safeApprove( 98 | ERC20 token, 99 | address to, 100 | uint256 amount 101 | ) internal { 102 | bool success; 103 | 104 | /// @solidity memory-safe-assembly 105 | assembly { 106 | // Get a pointer to some free memory. 107 | let freeMemoryPointer := mload(0x40) 108 | 109 | // Write the abi-encoded calldata into memory, beginning with the function selector. 110 | mstore(freeMemoryPointer, 0x095ea7b300000000000000000000000000000000000000000000000000000000) 111 | mstore(add(freeMemoryPointer, 4), and(to, 0xffffffffffffffffffffffffffffffffffffffff)) // Append and mask the "to" argument. 112 | mstore(add(freeMemoryPointer, 36), amount) // Append the "amount" argument. Masking not required as it's a full 32 byte type. 113 | 114 | success := and( 115 | // Set success to whether the call reverted, if not we check it either 116 | // returned exactly 1 (can't just be non-zero data), or had no return data. 117 | or(and(eq(mload(0), 1), gt(returndatasize(), 31)), iszero(returndatasize())), 118 | // We use 68 because the length of our calldata totals up like so: 4 + 32 * 2. 119 | // We use 0 and 32 to copy up to 32 bytes of return data into the scratch space. 120 | // Counterintuitively, this call must be positioned second to the or() call in the 121 | // surrounding and() call or else returndatasize() will be zero during the computation. 122 | call(gas(), token, 0, freeMemoryPointer, 68, 0, 32) 123 | ) 124 | } 125 | 126 | require(success, "APPROVE_FAILED"); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /test/BNB.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import "kontrol-cheatcodes/KontrolCheats.sol"; 6 | import {ERC20} from "../src/tokens/ERC20.sol"; 7 | 8 | contract BNBTest is Test, KontrolCheats { 9 | address constant DEPLOYED_ERC20 = address(491460923342184218035706888008750043977755113263); 10 | 11 | function setUp() public { 12 | bytes memory code = bytes(hex"6060604052600436106100db576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806306fdde03146100dd578063095ea7b31461016b57806318160ddd146101c557806323b872dd146101ee578063313ce567146102675780633bed33ce1461029657806342966c68146102b95780636623fc46146102f457806370a082311461032f5780638da5cb5b1461037c57806395d89b41146103d1578063a9059cbb1461045f578063cd4217c1146104a1578063d7a78db8146104ee578063dd62ed3e14610529575b005b34156100e857600080fd5b6100f0610595565b6040518080602001828103825283818151815260200191508051906020019080838360005b83811015610130578082015181840152602081019050610115565b50505050905090810190601f16801561015d5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561017657600080fd5b6101ab600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610633565b604051808215151515815260200191505060405180910390f35b34156101d057600080fd5b6101d86106ce565b6040518082815260200191505060405180910390f35b34156101f957600080fd5b61024d600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506106d4565b604051808215151515815260200191505060405180910390f35b341561027257600080fd5b61027a610af8565b604051808260ff1660ff16815260200191505060405180910390f35b34156102a157600080fd5b6102b76004808035906020019091905050610b0b565b005b34156102c457600080fd5b6102da6004808035906020019091905050610bcc565b604051808215151515815260200191505060405180910390f35b34156102ff57600080fd5b6103156004808035906020019091905050610d1e565b604051808215151515815260200191505060405180910390f35b341561033a57600080fd5b610366600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610eea565b6040518082815260200191505060405180910390f35b341561038757600080fd5b61038f610f02565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156103dc57600080fd5b6103e4610f28565b6040518080602001828103825283818151815260200191508051906020019080838360005b83811015610424578082015181840152602081019050610409565b50505050905090810190601f1680156104515780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561046a57600080fd5b61049f600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091908035906020019091905050610fc6565b005b34156104ac57600080fd5b6104d8600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611253565b6040518082815260200191505060405180910390f35b34156104f957600080fd5b61050f600480803590602001909190505061126b565b604051808215151515815260200191505060405180910390f35b341561053457600080fd5b61057f600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050611437565b6040518082815260200191505060405180910390f35b60008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561062b5780601f106106005761010080835404028352916020019161062b565b820191906000526020600020905b81548152906001019060200180831161060e57829003601f168201915b505050505081565b6000808211151561064357600080fd5b81600760003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506001905092915050565b60035481565b6000808373ffffffffffffffffffffffffffffffffffffffff1614156106f957600080fd5b60008211151561070857600080fd5b81600560008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561075457600080fd5b600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482600560008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020540110156107e157600080fd5b600760008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482111561086a57600080fd5b6108b3600560008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548361145c565b600560008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061093f600560008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611475565b600560008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610a08600760008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548361145c565b600760008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190509392505050565b600260009054906101000a900460ff1681565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141515610b6757600080fd5b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f193505050501515610bc957600080fd5b50565b600081600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610c1a57600080fd5b600082111515610c2957600080fd5b610c72600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548361145c565b600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610cc16003548361145c565b6003819055503373ffffffffffffffffffffffffffffffffffffffff167fcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5836040518082815260200191505060405180910390a260019050919050565b600081600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610d6c57600080fd5b600082111515610d7b57600080fd5b610dc4600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548361145c565b600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610e50600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611475565b600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff167f2cfce4af01bcb9d6cf6c84ee1b7c491100b8695368264146a94d71e10a63083f836040518082815260200191505060405180910390a260019050919050565b60056020528060005260406000206000915090505481565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60018054600181600116156101000203166002900480601f016020809104026020016040519081016040528092919081815260200182805460018160011615610100020316600290048015610fbe5780601f10610f9357610100808354040283529160200191610fbe565b820191906000526020600020905b815481529060010190602001808311610fa157829003601f168201915b505050505081565b60008273ffffffffffffffffffffffffffffffffffffffff161415610fea57600080fd5b600081111515610ff957600080fd5b80600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002054101561104557600080fd5b600560008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205481600560008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020540110156110d257600080fd5b61111b600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548261145c565b600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506111a7600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205482611475565b600560008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508173ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a35050565b60066020528060005260406000206000915090505481565b600081600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156112b957600080fd5b6000821115156112c857600080fd5b611311600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020548361145c565b600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000208190555061139d600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205483611475565b600660003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055503373ffffffffffffffffffffffffffffffffffffffff167ff97a274face0b5517365ad396b1fdba6f68bd3135ef603e44272adba3af5a1e0836040518082815260200191505060405180910390a260019050919050565b6007602052816000526040600020602052806000526040600020600091509150505481565b600061146a8383111561149f565b818303905092915050565b60008082840190506114958482101580156114905750838210155b61149f565b8091505092915050565b8015156114ab57600080fd5b505600a165627a7a72305820401723de360a17f2bd6924a0a77b3d6db97f6b43c3e423dc45a92e672ff79fc00029"); 13 | 14 | vm.etch(DEPLOYED_ERC20, code); 15 | } 16 | 17 | function test_totalSupply() public { 18 | ERC20(DEPLOYED_ERC20).totalSupply(); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /test/ERC4626.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {ERC4626} from "../src/tokens/ERC4626.sol"; 6 | import {ERC20} from "../src/tokens/ERC20.sol"; 7 | import {ERC20Mock} from "../src/mocks/ERC20Mock.sol"; 8 | import "kontrol-cheatcodes/KontrolCheats.sol"; 9 | 10 | contract ERC4626Test is Test, KontrolCheats { 11 | ERC20 public asset; 12 | ERC4626 public vault; 13 | address public alice; 14 | address public bob; 15 | 16 | function _notBuiltinAddress(address addr) internal view { 17 | vm.assume(addr != address(this)); 18 | vm.assume(addr != address(vm)); 19 | vm.assume(addr != address(asset)); 20 | vm.assume(addr != address(vault)); 21 | } 22 | 23 | function setUp() public { 24 | asset = new ERC20Mock(); 25 | vault = new ERC4626(ERC20(address(asset)), "Vault", "VAULT"); 26 | 27 | kevm.symbolicStorage(address(vault)); 28 | } 29 | 30 | // totalAssets MUST NOT revert 31 | function test_totalAssets_doesNotRevert(address caller) public { 32 | _notBuiltinAddress(caller); 33 | vm.prank(caller); vault.totalAssets(); 34 | } 35 | 36 | // totalAssets MUST revert when paused 37 | function test_totalAssets_revertsWhenPaused(address caller) public { 38 | _notBuiltinAddress(caller); 39 | 40 | vault.pause(); 41 | 42 | vm.startPrank(caller); 43 | 44 | vm.expectRevert(); 45 | vault.totalAssets(); 46 | } 47 | 48 | function test_approve_emitsEvent(address from, address to, uint256 amount) public { 49 | _notBuiltinAddress(from); 50 | _notBuiltinAddress(to); 51 | 52 | vm.expectEmit(true, true, false, true); 53 | emit ERC20.Approval(from, to, amount); 54 | 55 | vm.prank(from); 56 | vault.approve(to, amount); 57 | } 58 | 59 | function test_assume_noOverflow(uint x, uint y) public { 60 | vm.assume(x <= x + y); 61 | assert(true); 62 | } 63 | 64 | function test_assume_noOverflow_freshVars() public { 65 | uint256 x = kevm.freshUInt(32); 66 | uint256 y = kevm.freshUInt(32); 67 | vm.assume(x <= x + y); 68 | assert(true); 69 | } 70 | } -------------------------------------------------------------------------------- /test/Equivalence.t.sol: -------------------------------------------------------------------------------- 1 | import {Test, console} from "forge-std/Test.sol"; 2 | 3 | /// @author Modified from Solady (https://github.com/vectorized/solady/blob/main/src/utils/FixedPointMathLib.sol) 4 | /// @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/FixedPointMathLib.sol) 5 | contract EquivalenceTest is Test { 6 | uint256 internal constant WAD = 1e18; // The scalar of ETH and most ERC20s. 7 | 8 | /// @dev Equivalent to `(x * y) / WAD` rounded down. 9 | function mulWad(uint x, uint y) public pure returns (uint256 z) { 10 | assembly { 11 | // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. 12 | if mul(y, gt(x, div(not(0), y))) { 13 | mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. 14 | revert(0x1c, 0x04) 15 | } 16 | z := div(mul(x, y), WAD) 17 | } 18 | } 19 | 20 | /// @dev Equivalent to `(x * y) / WAD` rounded up. 21 | function mulWadUp(uint256 x, uint256 y) public pure returns (uint256 z) { 22 | /// @solidity memory-safe-assembly 23 | assembly { 24 | // Equivalent to `require(y == 0 || x <= type(uint256).max / y)`. 25 | if mul(y, gt(x, div(not(0), y))) { 26 | mstore(0x00, 0xbac65e5b) // `MulWadFailed()`. 27 | revert(0x1c, 0x04) 28 | } 29 | z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)) 30 | } 31 | } 32 | 33 | function test_mulWad(uint256 x, uint256 y) public { 34 | if (y == 0 || x <= type(uint256).max / y) { 35 | uint256 zSpec = (x * y) / WAD; 36 | uint256 zImpl = mulWad(x, y); 37 | assert(zImpl == zSpec); 38 | } else { 39 | vm.expectRevert(); 40 | this.mulWad(x, y); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /test/MultiVault.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import {MultiVault} from "../src/tokens/MultiVault.sol"; 6 | import "kontrol-cheatcodes/KontrolCheats.sol"; 7 | 8 | contract MultiVaultTest is Test, KontrolCheats { 9 | MultiVault public multiVault; 10 | 11 | struct Vault { 12 | address asset; 13 | uint256 totalSupply; 14 | bytes vaultData; 15 | } 16 | 17 | function _notBuiltinAddress(address addr) internal view { 18 | vm.assume(addr != address(this)); 19 | vm.assume(addr != address(vm)); 20 | vm.assume(addr != address(multiVault)); 21 | } 22 | 23 | function setUp() public { 24 | multiVault = new MultiVault(); 25 | } 26 | 27 | /// @custom:kontrol-bytes-length-equals vaultData: 256, 28 | function test_create_vault(address asset, uint256 totalSupply, bytes calldata vaultData) public { 29 | multiVault.create(asset, totalSupply, vaultData); 30 | } 31 | 32 | /// @custom:kontrol-array-length-equals vaults: 4, 33 | /// @custom:kontrol-bytes-length-equals vaultData: 256, 34 | function test_create_loop(Vault[] calldata vaults, uint length) public { 35 | // WARN: missing check on `vaults.length` and `length` 36 | // WARN: Unbounded `for` loop is an anti-pattern 37 | for (uint i = 0; i < length; i++) { 38 | multiVault.create(vaults[i].asset, vaults[i].totalSupply, vaults[i].vaultData); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /test/WETH9.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import {Test, console} from "forge-std/Test.sol"; 5 | import "kontrol-cheatcodes/KontrolCheats.sol"; 6 | import "../src/tokens/WETH9.sol"; 7 | 8 | contract WETH9Test is Test, KontrolCheats { 9 | WETH9 public asset; 10 | address public alice; 11 | address public bob; 12 | 13 | function setUp() public { 14 | asset = new WETH9(); 15 | 16 | alice = makeAddr("Alice"); 17 | bob = makeAddr("Bob"); 18 | } 19 | 20 | function test_approve_AliceToBob(uint256 amount) public { 21 | // Foundry cheatcode changing `msg.sender` to specified address 22 | vm.prank(alice); 23 | // `alice` approving `amount` WETH to `bob` 24 | asset.approve(bob, amount); 25 | 26 | // reading updated `bob` allowance to spend `alice` tokens 27 | uint256 bobAllowance = asset.allowance(alice, bob); 28 | // asserting that it was updated to `amount` 29 | assert(bobAllowance == amount); 30 | } 31 | 32 | function test_approve(address from, address to, uint256 amount) public { 33 | vm.prank(from); 34 | asset.approve(to, amount); 35 | 36 | uint256 toAllowance = asset.allowance(from, to); 37 | assert(toAllowance == amount); 38 | } 39 | 40 | function test_deposit(address from) public payable { 41 | _notBuiltinAddress(from); 42 | 43 | vm.prank(from); 44 | asset.deposit{value: msg.value}(); 45 | } 46 | 47 | function _notBuiltinAddress(address addr) internal view { 48 | vm.assume(addr != address(this)); 49 | vm.assume(addr != address(vm)); 50 | vm.assume(addr != address(asset)); 51 | } 52 | 53 | /* 54 | function test_withdraw(address from, uint256 amount) public payable { 55 | _notBuiltinAddress(from); 56 | 57 | vm.assume(from > address(9)); 58 | vm.deal(address(asset), amount); 59 | 60 | kevm.symbolicStorage(address(asset)); 61 | 62 | vm.prank(from); 63 | asset.withdraw(amount); 64 | } 65 | */ 66 | } 67 | --------------------------------------------------------------------------------