├── .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 |
--------------------------------------------------------------------------------