├── .env.example ├── audits ├── 2024-11-5-morpho-token-upgradeable-open-zeppelin.pdf └── 2024-11-5-morpho-token-upgradeable-cantina-managed.pdf ├── foundry.lock ├── test ├── helpers │ ├── interfaces │ │ ├── ITransferBundler.sol │ │ ├── IERC20WrapperBundler.sol │ │ └── IMulticall.sol │ ├── libraries │ │ └── EncodeLib.sol │ ├── BaseTest.sol │ └── SigUtils.sol ├── deployment │ ├── BaseDeploymentTest.sol │ └── EthereumDeploymentTest.sol ├── DelegationTokenInternalTest.sol ├── MorphoTokenOptimismTest.sol ├── MigrationTest.sol └── MorphoTokenEthereumTest.sol ├── .gitignore ├── .gitmodules ├── certora ├── helpers │ ├── MorphoTokenEthereumHarness.sol │ ├── MorphoTokenOptimismHarness.sol │ └── DelegationTokenHarness.sol ├── Makefile ├── confs │ ├── ERC20Ethereum.conf │ ├── ERC20Optimism.conf │ ├── ERC20InvariantsEthereum.conf │ ├── ERC20InvariantsOptimism.conf │ ├── RevertsERC20Ethereum.conf │ ├── RevertsERC20Optimism.conf │ ├── ExternalCallsEthereum.conf │ ├── ExternalCallsOptimism.conf │ ├── RevertsMintBurnEthereum.conf │ ├── RevertsMintBurnOptimism.conf │ ├── MintBurnEthereum.conf │ ├── MintBurnOptimism.conf │ ├── DelegationEthereum.conf │ └── DelegationOptimism.conf ├── specs │ ├── ExternalCalls.spec │ ├── RevertsMintBurnOptimism.spec │ ├── RevertsMintBurnEthereum.spec │ ├── RevertsERC20.spec │ ├── MintBurnEthereum.spec │ ├── MintBurnOptimism.spec │ ├── ERC20Invariants.spec │ ├── Delegation.spec │ └── ERC20.spec ├── applyMunging.patch └── README.md ├── foundry.toml ├── src ├── interfaces │ ├── IDelegation.sol │ └── IOptimismMintableERC20.sol ├── MorphoTokenEthereum.sol ├── Wrapper.sol ├── MorphoTokenOptimism.sol └── DelegationToken.sol ├── .github └── workflows │ ├── foundry.yml │ └── certora.yml ├── script ├── DeployMorphoTokenBase.sol └── DeployMorphoTokenEthereum.sol ├── README.md └── LICENSE /.env.example: -------------------------------------------------------------------------------- 1 | ALCHEMY_KEY= 2 | -------------------------------------------------------------------------------- /audits/2024-11-5-morpho-token-upgradeable-open-zeppelin.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-org/morpho-token/HEAD/audits/2024-11-5-morpho-token-upgradeable-open-zeppelin.pdf -------------------------------------------------------------------------------- /audits/2024-11-5-morpho-token-upgradeable-cantina-managed.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/morpho-org/morpho-token/HEAD/audits/2024-11-5-morpho-token-upgradeable-cantina-managed.pdf -------------------------------------------------------------------------------- /foundry.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lib/forge-std": { 3 | "rev": "8f24d6b04c92975e0795b5868aa0d783251cdeaa" 4 | }, 5 | "lib/openzeppelin-contracts-upgradeable": { 6 | "rev": "fa525310e45f91eb20a6d3baa2644be8e0adba31" 7 | } 8 | } -------------------------------------------------------------------------------- /test/helpers/interfaces/ITransferBundler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | interface ITransferBundler { 5 | function erc20TransferFrom(address asset, uint256 amount) external; 6 | } 7 | -------------------------------------------------------------------------------- /test/helpers/interfaces/IERC20WrapperBundler.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | interface IERC20WrapperBundler { 5 | function erc20WrapperDepositFor(address wrapper, uint256 amount) external; 6 | } 7 | -------------------------------------------------------------------------------- /.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 | 16 | # Certora 17 | .certora_internal 18 | munged/ 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/openzeppelin-contracts-upgradeable"] 5 | path = lib/openzeppelin-contracts-upgradeable 6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable 7 | -------------------------------------------------------------------------------- /certora/helpers/MorphoTokenEthereumHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import {DelegationTokenHarness, Signature, Delegation} from "./DelegationTokenHarness.sol"; 5 | import "../../munged/MorphoTokenEthereum.sol"; 6 | 7 | contract MorphoTokenEthereumHarness is MorphoTokenEthereum, DelegationTokenHarness {} 8 | -------------------------------------------------------------------------------- /test/helpers/interfaces/IMulticall.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | interface IMulticall { 5 | /// @notice Executes an ordered batch of delegatecalls to this contract. 6 | /// @param data The ordered array of calldata to execute. 7 | function multicall(bytes[] calldata data) external payable; 8 | } 9 | -------------------------------------------------------------------------------- /certora/Makefile: -------------------------------------------------------------------------------- 1 | munged: ../munged 2 | 3 | ../munged: $(wildcard ../src/*.sol) applyMunging.patch 4 | @rm -rf ../munged 5 | @cp -r ../src ../munged 6 | @patch -p0 -d ../munged < applyMunging.patch 7 | 8 | record: 9 | diff -ruN ../src ../munged | sed 's+ \.\./src/+ +g' | sed 's+ \.\./munged/+ +g' > applyMunging.patch 10 | 11 | clean: 12 | rm -rf ../munged 13 | 14 | .PHONY: clean record munged # Do not add ../munged here, as it is useful to protect munged edits 15 | -------------------------------------------------------------------------------- /certora/confs/ERC20Ethereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenEthereum.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereum" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereum:certora/specs/ERC20.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Ethereum ERC20" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/ERC20Optimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenOptimism.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimism" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimism:certora/specs/ERC20.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Optimism ERC20" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/ERC20InvariantsEthereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenEthereum.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereum" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereum:certora/specs/ERC20Invariants.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Ethereum ERC20" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/ERC20InvariantsOptimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenOptimism.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimism" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimism:certora/specs/ERC20Invariants.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Optimism ERC20" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/RevertsERC20Ethereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenEthereum.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereum" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereum:certora/specs/RevertsERC20.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho token ERC20 Reverts Ethereum" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/RevertsERC20Optimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenOptimism.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimism" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimism:certora/specs/RevertsERC20.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho token ERC20 Reverts Optimism" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/ExternalCallsEthereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenEthereum.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereum" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereum:certora/specs/ExternalCalls.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho Token Ethereum External Calls" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/ExternalCallsOptimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenOptimism.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimism" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimism:certora/specs/ExternalCalls.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho Token Optimism External Calls" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/RevertsMintBurnEthereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenEthereum.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereum" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereum:certora/specs/RevertsMintBurnEthereum.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho token ERC20 Mint Burn Reverts" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/RevertsMintBurnOptimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "src/MorphoTokenOptimism.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimism" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimism:certora/specs/RevertsMintBurnOptimism.spec", 10 | "rule_sanity": "basic", 11 | "server": "production", 12 | "packages": [ 13 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 14 | ], 15 | "loop_iter": "3", 16 | "optimistic_loop": true, 17 | "msg": "Morpho token ERC20 Mint Burn Reverts" 18 | } 19 | -------------------------------------------------------------------------------- /certora/helpers/MorphoTokenOptimismHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import {DelegationTokenHarness, Signature, Delegation} from "./DelegationTokenHarness.sol"; 5 | import "../../munged/MorphoTokenOptimism.sol"; 6 | import {ECDSA} from 7 | "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; 8 | 9 | contract MorphoTokenOptimismHarness is MorphoTokenOptimism, DelegationTokenHarness { 10 | constructor(address newRemoteToken, address newBridge) MorphoTokenOptimism(newRemoteToken, newBridge) {} 11 | } 12 | -------------------------------------------------------------------------------- /certora/confs/MintBurnEthereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/helpers/MorphoTokenEthereumHarness.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenEthereumHarness" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenEthereumHarness:certora/specs/MintBurnEthereum.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Ethereum ERC20 mint and burn" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/MintBurnOptimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/helpers/MorphoTokenOptimismHarness.sol" 4 | ], 5 | "parametric_contracts": [ 6 | "MorphoTokenOptimismHarness" 7 | ], 8 | "solc": "solc-0.8.27", 9 | "verify": "MorphoTokenOptimismHarness:certora/specs/MintBurnOptimism.spec", 10 | "packages": [ 11 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 12 | ], 13 | "loop_iter": "3", 14 | "optimistic_loop": true, 15 | "rule_sanity": "basic", 16 | "server": "production", 17 | "msg": "Morpho Token Optimism ERC20 mint and burn" 18 | } 19 | -------------------------------------------------------------------------------- /certora/confs/DelegationEthereum.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/helpers/MorphoTokenEthereumHarness.sol", 4 | "certora/helpers/MorphoTokenOptimismHarness.sol" 5 | ], 6 | "parametric_contracts": [ 7 | "MorphoTokenEthereumHarness" 8 | ], 9 | "solc": "solc-0.8.27", 10 | "verify": "MorphoTokenEthereumHarness:certora/specs/Delegation.spec", 11 | "packages": [ 12 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 13 | ], 14 | "loop_iter": "3", 15 | "optimistic_loop": true, 16 | "rule_sanity": "basic", 17 | "server": "production", 18 | "msg": "Morpho Token Ethereum Delegation" 19 | } 20 | -------------------------------------------------------------------------------- /certora/confs/DelegationOptimism.conf: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "certora/helpers/MorphoTokenOptimismHarness.sol", 4 | "certora/helpers/MorphoTokenEthereumHarness.sol" 5 | ], 6 | "parametric_contracts": [ 7 | "MorphoTokenOptimismHarness" 8 | ], 9 | "solc": "solc-0.8.27", 10 | "verify": "MorphoTokenOptimismHarness:certora/specs/Delegation.spec", 11 | "packages": [ 12 | "@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts" 13 | ], 14 | "loop_iter": "3", 15 | "optimistic_loop": true, 16 | "rule_sanity": "basic", 17 | "server": "production", 18 | "msg": "Morpho Token Optimism Delegation" 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/libraries/EncodeLib.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {IERC20WrapperBundler} from "../interfaces/IERC20WrapperBundler.sol"; 5 | import {ITransferBundler} from "../interfaces/ITransferBundler.sol"; 6 | 7 | library EncodeLib { 8 | function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { 9 | return abi.encodeCall(IERC20WrapperBundler.erc20WrapperDepositFor, (asset, amount)); 10 | } 11 | 12 | function _erc20TransferFrom(address asset, uint256 amount) internal pure returns (bytes memory) { 13 | return abi.encodeCall(ITransferBundler.erc20TransferFrom, (asset, amount)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | via-ir = true 6 | optimizer_runs = 999999 7 | evm_version = "cancun" 8 | 9 | [profile.default.fuzz] 10 | runs = 256 11 | 12 | [profile.default.fmt] 13 | wrap_comments = true 14 | 15 | [lint] 16 | exclude_lints = [ 17 | "erc20-unchecked-transfer", 18 | "unused-import", 19 | "unaliased-plain-import", 20 | "unwrapped-modifier-logic", 21 | "mixed-case-variable", 22 | "screaming-snake-case-immutable", 23 | ] 24 | 25 | [rpc_endpoints] 26 | ethereum = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" 27 | base = "https://base-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}" 28 | 29 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 30 | -------------------------------------------------------------------------------- /certora/helpers/DelegationTokenHarness.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import {DelegationToken, Signature, Delegation} from "../../munged/DelegationToken.sol"; 5 | import {ECDSA} from 6 | "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; 7 | 8 | contract DelegationTokenHarness is DelegationToken { 9 | function delegatorFromSig(Delegation calldata delegation, Signature calldata signature) 10 | external 11 | view 12 | returns (address) 13 | { 14 | address delegator = ECDSA.recover( 15 | _hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegation))), 16 | signature.v, 17 | signature.r, 18 | signature.s 19 | ); 20 | return delegator; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/IDelegation.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | struct Delegation { 5 | address delegatee; 6 | uint256 nonce; 7 | uint256 expiry; 8 | } 9 | 10 | struct Signature { 11 | uint8 v; 12 | bytes32 r; 13 | bytes32 s; 14 | } 15 | 16 | /// @title IDelegation 17 | /// @author Morpho Association 18 | /// @custom:security-contact security@morpho.org 19 | interface IDelegation { 20 | function delegatedVotingPower(address account) external view returns (uint256); 21 | 22 | function delegatee(address account) external view returns (address); 23 | 24 | function delegationNonce(address account) external view returns (uint256); 25 | 26 | function delegate(address delegatee) external; 27 | 28 | function delegateWithSig(Delegation calldata delegation, Signature calldata signature) external; 29 | } 30 | -------------------------------------------------------------------------------- /src/interfaces/IOptimismMintableERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.5.0; 3 | 4 | import { 5 | IERC165 6 | } from "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; 7 | 8 | /// @title IOptimismMintableERC20 9 | /// @author Morpho Association 10 | /// @custom:security-contact security@morpho.org 11 | /// @notice This interface is available on the OptimismMintableERC20 contract. 12 | /// We declare it as a separate interface so that it can be used in 13 | /// custom implementations of OptimismMintableERC20. 14 | interface IOptimismMintableERC20 is IERC165 { 15 | function remoteToken() external view returns (address); 16 | 17 | function bridge() external returns (address); 18 | 19 | function mint(address to, uint256 amount) external; 20 | 21 | function burn(address from, uint256 amount) external; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/foundry.yml: -------------------------------------------------------------------------------- 1 | name: Foundry 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | 19 | - name: Install Foundry 20 | uses: foundry-rs/foundry-toolchain@v1 21 | with: 22 | version: nightly 23 | 24 | - name: Show Forge version 25 | run: | 26 | forge --version 27 | 28 | - name: Run Forge fmt 29 | run: | 30 | forge fmt --check 31 | id: fmt 32 | 33 | - name: Run Forge build 34 | run: | 35 | forge build --sizes 36 | id: build 37 | 38 | - name: Run Forge tests 39 | run: | 40 | forge test -vvv 41 | id: test 42 | env: 43 | ALCHEMY_KEY: ${{ secrets.PROTOCOL_ALCHEMY_KEY }} 44 | -------------------------------------------------------------------------------- /certora/specs/ExternalCalls.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | // True when a CALL has been placed. 4 | persistent ghost bool hasExternalCall { 5 | init_state axiom hasExternalCall == false; 6 | } 7 | 8 | // True when a DELEGATECALL has been placed. 9 | persistent ghost bool hasDelegateCall { 10 | init_state axiom hasDelegateCall == false; 11 | } 12 | 13 | hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { 14 | hasExternalCall = true; 15 | } 16 | 17 | hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { 18 | hasDelegateCall = true; 19 | } 20 | 21 | // Check that the contract is reentrant safe as it makes no external call. 22 | invariant reentrancySafe() 23 | !hasExternalCall; 24 | 25 | // Check that the contract makes no delegate call, except during an upgrade when doing a setup call to the new implementation. 26 | invariant noDelegateCalls() 27 | !hasDelegateCall 28 | filtered { 29 | f -> f.selector != sig:upgradeToAndCall(address, bytes).selector 30 | } 31 | -------------------------------------------------------------------------------- /src/MorphoTokenEthereum.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import {DelegationToken} from "./DelegationToken.sol"; 5 | 6 | /// @title MorphoTokenEthereum 7 | /// @author Morpho Association 8 | /// @custom:security-contact security@morpho.org 9 | /// @notice The Morpho token contract for Ethereum. 10 | contract MorphoTokenEthereum is DelegationToken { 11 | /* CONSTANTS */ 12 | 13 | /// @dev The name of the token. 14 | string internal constant NAME = "Morpho Token"; 15 | 16 | /// @dev The symbol of the token. 17 | string internal constant SYMBOL = "MORPHO"; 18 | 19 | /* EXTERNAL */ 20 | 21 | /// @notice Initializes the contract. 22 | /// @param owner The new owner. 23 | /// @param wrapper The wrapper contract address to migrate legacy MORPHO tokens to the new one. 24 | function initialize(address owner, address wrapper) external initializer { 25 | __ERC20_init(NAME, SYMBOL); 26 | __ERC20Permit_init(NAME); 27 | __Ownable_init(owner); 28 | 29 | _mint(wrapper, 1_000_000_000e18); // Mint 1B to the wrapper contract. 30 | } 31 | 32 | /// @notice Mints tokens. 33 | function mint(address to, uint256 amount) external onlyOwner { 34 | _mint(to, amount); 35 | } 36 | 37 | /// @notice Burns sender's tokens. 38 | function burn(uint256 amount) external { 39 | _burn(_msgSender(), amount); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/deployment/BaseDeploymentTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "../../lib/forge-std/src/Test.sol"; 5 | 6 | import {DeployMorphoTokenBase} from "../../script/DeployMorphoTokenBase.sol"; 7 | import {MorphoTokenOptimism} from "../../src/MorphoTokenOptimism.sol"; 8 | import {UUPSUpgradeable} from "../../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 9 | 10 | bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 11 | 12 | contract DumbImplementation is UUPSUpgradeable { 13 | function _authorizeUpgrade(address) internal override {} 14 | } 15 | 16 | contract BaseDeploymentTest is DeployMorphoTokenBase, Test { 17 | address token; 18 | 19 | function setUp() public virtual { 20 | // DEPLOYMENTS 21 | token = run(); 22 | } 23 | 24 | function testDeployment() public view { 25 | assertEq(MorphoTokenOptimism(token).totalSupply(), 0); 26 | assertEq(MorphoTokenOptimism(token).owner(), MORPHO_DAO); 27 | } 28 | 29 | function testUpgrade() public { 30 | address newImplementation = address(new DumbImplementation()); 31 | vm.prank(MORPHO_DAO); 32 | MorphoTokenOptimism(token).upgradeToAndCall(newImplementation, hex""); 33 | assertEq(address(uint160(uint256(vm.load(token, IMPLEMENTATION_SLOT)))), newImplementation); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /script/DeployMorphoTokenBase.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../lib/forge-std/src/Script.sol"; 5 | import "../lib/forge-std/src/console.sol"; 6 | 7 | import {MorphoTokenOptimism} from "../src/MorphoTokenOptimism.sol"; 8 | import { 9 | ERC1967Proxy 10 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 11 | 12 | contract DeployMorphoTokenBase is Script { 13 | address public constant MORPHO_DAO = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; 14 | address public constant REMOTE_TOKEN = 0x58D97B57BB95320F9a05dC918Aef65434969c2B2; 15 | address public constant BRIDGE = 0x4200000000000000000000000000000000000010; 16 | 17 | function run() public returns (address) { 18 | vm.createSelectFork(vm.rpcUrl("base")); 19 | 20 | vm.startBroadcast(); 21 | 22 | // Deploy Token implementation 23 | address tokenImplementation = address(new MorphoTokenOptimism(REMOTE_TOKEN, BRIDGE)); 24 | console.log("Deployed token implementation at", tokenImplementation); 25 | 26 | // Deploy Token proxy and initialize it. 27 | address token = address( 28 | new ERC1967Proxy( 29 | tokenImplementation, abi.encodeWithSelector(MorphoTokenOptimism.initialize.selector, MORPHO_DAO) 30 | ) 31 | ); 32 | console.log("Deployed token proxy at", address(token)); 33 | 34 | vm.stopBroadcast(); 35 | 36 | return token; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/certora.yml: -------------------------------------------------------------------------------- 1 | name: Certora 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | verify: 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | fail-fast: false 16 | 17 | matrix: 18 | conf: 19 | - DelegationEthereum 20 | - DelegationOptimism 21 | - ERC20InvariantsEthereum 22 | - ERC20Ethereum 23 | - ERC20InvariantsOptimism 24 | - ERC20Optimism 25 | - ExternalCallsEthereum 26 | - ExternalCallsOptimism 27 | - MintBurnEthereum 28 | - MintBurnOptimism 29 | - RevertsERC20Ethereum 30 | - RevertsERC20Optimism 31 | - RevertsMintBurnEthereum 32 | - RevertsMintBurnOptimism 33 | 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | submodules: recursive 38 | 39 | - name: Install python 40 | uses: actions/setup-python@v5 41 | with: 42 | python-version: ">=3.11" 43 | 44 | - name: Install certora 45 | run: pip install certora-cli 46 | 47 | - name: Install solc (0.8.27) 48 | run: | 49 | wget https://github.com/ethereum/solidity/releases/download/v0.8.27/solc-static-linux 50 | chmod +x solc-static-linux 51 | sudo mv solc-static-linux /usr/local/bin/solc-0.8.27 52 | 53 | - name: Apply munging 54 | run: make -C certora munged 55 | 56 | - name: Verify ${{ matrix.conf }} specification 57 | run: certoraRun certora/confs/${{ matrix.conf }}.conf 58 | env: 59 | CERTORAKEY: ${{ secrets.CERTORAKEY }} 60 | -------------------------------------------------------------------------------- /test/deployment/EthereumDeploymentTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "../../lib/forge-std/src/Test.sol"; 5 | 6 | import {MorphoTokenEthereum} from "../../src/MorphoTokenEthereum.sol"; 7 | import {Wrapper} from "../../src/Wrapper.sol"; 8 | import {DeployMorphoTokenEthereum} from "../../script/DeployMorphoTokenEthereum.sol"; 9 | import {UUPSUpgradeable} from "../../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 10 | 11 | bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; 12 | 13 | contract DumbImplementation is UUPSUpgradeable { 14 | function _authorizeUpgrade(address) internal override {} 15 | } 16 | 17 | contract EthereumDeploymentTest is DeployMorphoTokenEthereum, Test { 18 | address token; 19 | address wrapper; 20 | 21 | function setUp() public virtual { 22 | // DEPLOYMENTS 23 | (token, wrapper) = run(); 24 | } 25 | 26 | function testDeployment() public view { 27 | assertEq(Wrapper(wrapper).NEW_MORPHO(), token); 28 | assertEq(MorphoTokenEthereum(token).totalSupply(), 1_000_000_000e18); 29 | assertEq(MorphoTokenEthereum(token).balanceOf(wrapper), 1_000_000_000e18); 30 | assertEq(MorphoTokenEthereum(token).owner(), MORPHO_DAO); 31 | } 32 | 33 | function testUpgrade() public { 34 | address newImplementation = address(new DumbImplementation()); 35 | vm.prank(MORPHO_DAO); 36 | MorphoTokenEthereum(token).upgradeToAndCall(newImplementation, hex""); 37 | assertEq(address(uint160(uint256(vm.load(token, IMPLEMENTATION_SLOT)))), newImplementation); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /script/DeployMorphoTokenEthereum.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import "../lib/forge-std/src/Script.sol"; 5 | import "../lib/forge-std/src/console.sol"; 6 | 7 | import {MorphoTokenEthereum} from "../src/MorphoTokenEthereum.sol"; 8 | import {Wrapper} from "../src/Wrapper.sol"; 9 | import { 10 | ERC1967Proxy 11 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 12 | 13 | contract DeployMorphoTokenEthereum is Script { 14 | address public constant MORPHO_DAO = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; 15 | 16 | function run() public returns (address, address) { 17 | vm.createSelectFork(vm.rpcUrl("ethereum")); 18 | 19 | vm.startBroadcast(); 20 | 21 | // Deploy Token implementation 22 | address tokenImplementation = address(new MorphoTokenEthereum()); 23 | console.log("Deployed token implementation at", tokenImplementation); 24 | 25 | address expectedWrapper = vm.computeCreateAddress(msg.sender, vm.getNonce(msg.sender) + 1); 26 | 27 | // Deploy Token proxy 28 | address token = address( 29 | new ERC1967Proxy( 30 | tokenImplementation, 31 | abi.encodeWithSelector(MorphoTokenEthereum.initialize.selector, MORPHO_DAO, expectedWrapper) 32 | ) 33 | ); 34 | 35 | console.log("Deployed token proxy at", address(token)); 36 | 37 | // Deploy Wrapper 38 | address wrapper = address(new Wrapper(address(token))); 39 | console.log("Deployed wrapper at", wrapper); 40 | 41 | require(wrapper == expectedWrapper, "wrapper != expectedWrapper"); 42 | 43 | vm.stopBroadcast(); 44 | 45 | return (token, wrapper); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /certora/specs/RevertsMintBurnOptimism.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | methods { 4 | function totalSupply() external returns uint256 envfree; 5 | function balanceOf(address) external returns uint256 envfree; 6 | function delegatee(address) external returns address envfree; 7 | function delegatedVotingPower(address) external returns uint256 envfree; 8 | } 9 | 10 | // Check the revert conditions for the mint function. 11 | rule mintRevertConditions(env e, address to, uint256 amount) { 12 | mathint totalSupplyBefore = totalSupply(); 13 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(to)); 14 | 15 | // Safe require as zero address can't possibly delegate voting power which is verified in zeroAddressNoVotingPower. 16 | require delegatee(0) == 0; 17 | 18 | // Safe require as it is verified in totalSupplyGTEqSumOfVotingPower. 19 | require toVotingPowerBefore <= totalSupply(); 20 | 21 | mint@withrevert(e, to, amount); 22 | assert lastReverted <=> e.msg.sender != currentContract.bridge || to == 0 || e.msg.value != 0 || totalSupplyBefore + amount > max_uint256; 23 | } 24 | 25 | // Check the revert conditions for the burn function. 26 | rule burnRevertConditions(env e, address from, uint256 amount) { 27 | uint256 balanceOfFromBefore = balanceOf(from); 28 | uint256 fromVotingPowerBefore = delegatedVotingPower(delegatee(from)); 29 | 30 | // Safe require as zero address can't possibly delegate voting power which is verified in zeroAddressNoVotingPower. 31 | require delegatee(0) == 0; 32 | 33 | // Safe require as it is verified in delegatedLTEqDelegateeVP. 34 | require fromVotingPowerBefore >= balanceOfFromBefore; 35 | 36 | burn@withrevert(e, from, amount); 37 | assert lastReverted <=> e.msg.sender != currentContract.bridge || from == 0 || balanceOfFromBefore < amount || e.msg.value != 0; 38 | } 39 | -------------------------------------------------------------------------------- /certora/specs/RevertsMintBurnEthereum.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | methods { 4 | function owner() external returns address envfree; 5 | function totalSupply() external returns uint256 envfree; 6 | function balanceOf(address) external returns uint256 envfree; 7 | function delegatee(address) external returns address envfree; 8 | function delegatedVotingPower(address) external returns uint256 envfree; 9 | } 10 | 11 | // Check the revert conditions for the burn function. 12 | rule mintRevertConditions(env e, address to, uint256 amount) { 13 | mathint totalSupplyBefore = totalSupply(); 14 | uint256 balanceOfSenderBefore = balanceOf(e.msg.sender); 15 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(to)); 16 | 17 | // Safe require as zero address can't possibly delegate voting power which is verified in zeroAddressNoVotingPower . 18 | require delegatee(0) == 0; 19 | 20 | // Safe require as it is verified in totalSupplyGTEqSumOfVotingPower. 21 | require toVotingPowerBefore <= totalSupply(); 22 | 23 | mint@withrevert(e, to, amount); 24 | assert lastReverted <=> e.msg.sender != owner() || to == 0 || e.msg.value != 0 || totalSupplyBefore + amount > max_uint256; 25 | } 26 | 27 | // Check the revert conditions for the burn function. 28 | rule burnRevertConditions(env e, uint256 amount) { 29 | uint256 balanceOfSenderBefore = balanceOf(e.msg.sender); 30 | uint256 delegateeVotingPowerBefore = delegatedVotingPower(delegatee(e.msg.sender)); 31 | 32 | // Safe require as zero address can't possibly delegate voting power which is verified in zeroAddressNoVotingPower . 33 | require delegatee(0) == 0; 34 | 35 | // Safe require as it is verified in delegatedLTEqDelegateeVP. 36 | require delegateeVotingPowerBefore >= balanceOfSenderBefore; 37 | 38 | burn@withrevert(e, amount); 39 | assert lastReverted <=> e.msg.sender == 0 || balanceOfSenderBefore < amount || e.msg.value != 0; 40 | } 41 | -------------------------------------------------------------------------------- /test/helpers/BaseTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test} from "../../lib/forge-std/src/Test.sol"; 5 | import {MorphoTokenEthereum} from "../../src/MorphoTokenEthereum.sol"; 6 | import {Wrapper} from "../../src/Wrapper.sol"; 7 | import { 8 | ERC1967Proxy 9 | } from "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 10 | import {UUPSUpgradeable} from "../../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 11 | 12 | // TODO: Test the following: 13 | // - Test every paths 14 | // - Test migration flow 15 | // - Test bundler wrapping 16 | // - Test access control 17 | // - Test voting 18 | // - Test delegation 19 | contract BaseTest is Test { 20 | address public constant MORPHO_DAO = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; 21 | 22 | MorphoTokenEthereum public tokenImplem; 23 | MorphoTokenEthereum public newMorpho; 24 | ERC1967Proxy public tokenProxy; 25 | Wrapper public wrapper; 26 | 27 | uint256 internal constant MIN_TEST_AMOUNT = 100; 28 | uint256 internal constant MAX_TEST_AMOUNT = 1e28; 29 | 30 | function setUp() public virtual { 31 | // DEPLOYMENTS 32 | tokenImplem = new MorphoTokenEthereum(); 33 | tokenProxy = new ERC1967Proxy(address(tokenImplem), hex""); 34 | wrapper = new Wrapper(address(tokenProxy)); 35 | 36 | newMorpho = MorphoTokenEthereum(payable(address(tokenProxy))); 37 | newMorpho.initialize(MORPHO_DAO, address(wrapper)); 38 | } 39 | 40 | function _validateAddresses(address[] memory addresses) internal view { 41 | for (uint256 i = 0; i < addresses.length; i++) { 42 | vm.assume(addresses[i] != address(0)); 43 | vm.assume(addresses[i] != MORPHO_DAO); 44 | vm.assume(addresses[i] != address(wrapper)); 45 | assumeNotPrecompile(addresses[i]); 46 | for (uint256 j = i + 1; j < addresses.length; j++) { 47 | vm.assume(addresses[i] != addresses[j]); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Morpho token 2 | 3 | This repository contains the Morpho protocol's ERC20 token. 4 | It is designed to be upgradable and support onchain delegation. 5 | Additionally, it ships a wrapper contract to simplify the migration of assets from the previous token contract to the new Morpho token contract. 6 | 7 | ## Features 8 | 9 | ### Upgradeability 10 | 11 | The Morpho token complies with [EIP-1967](https://eips.ethereum.org/EIPS/eip-1967) to support upgradeability. 12 | 13 | ### Delegation 14 | 15 | The Morpho token supports onchain voting and voting power delegation. 16 | 17 | ### Role-based permission 18 | 19 | The new Morpho token does not have role-based permission of functions. 20 | 21 | ### Burning tokens 22 | 23 | In the legacy Morpho token, it was possible to transfer tokens to the zero address. 24 | This is no longer possible in the new Morpho token, but it's possible for users to burn tokens by calling the `burn` function. 25 | 26 | ## Migration 27 | 28 | ### Wrapper contract 29 | 30 | The `Wrapper` contract enables the migration of legacy tokens to the new token version at a one-to-one ratio. 31 | With the functions `depositFor` and `withdrawTo`, this contract ensures compliance with `ERC20WrapperBundler` from the [Morpho bundler](https://github.com/morpho-org/morpho-blue-bundlers) contracts, enabling one-click migrations. 32 | The `Wrapper` contract will hold the migrated legacy tokens. 33 | 34 | ### Migration flow 35 | 36 | During contract initialization, 1 billion tokens will be minted for the `Wrapper` contract, which will initially hold the entire supply. 37 | Any legacy token holder will then be able to migrate their tokens provided that the migration amount is approved for the wrapper. 38 | Migrated legacy tokens may be recovered in order to revert a migration. 39 | 40 | ## Audits 41 | 42 | All audits are stored in the audits' folder. 43 | 44 | ## Getting started 45 | 46 | ### Install dependencies 47 | 48 | ```shell 49 | $ forge install 50 | ``` 51 | 52 | ### Test 53 | 54 | ```shell 55 | $ forge test 56 | ``` 57 | 58 | ## License 59 | 60 | The Morpho token is licensed under `GPL-2.0-or-later`, see [`LICENSE`](./LICENSE). 61 | -------------------------------------------------------------------------------- /src/Wrapper.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import { 5 | IERC20 6 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 7 | 8 | /// @title Wrapper 9 | /// @author Morpho Association 10 | /// @custom:security-contact security@morpho.org 11 | /// @notice The Wrapper contract to migrate from legacy MORPHO tokens. 12 | contract Wrapper { 13 | /* CONSTANTS */ 14 | 15 | /// @notice The address of the legacy Morpho token. 16 | address public constant LEGACY_MORPHO = 0x9994E35Db50125E0DF82e4c2dde62496CE330999; 17 | 18 | /* IMMUTABLES */ 19 | 20 | /// @notice The address of the new Morpho token. 21 | address public immutable NEW_MORPHO; 22 | 23 | /* ERRORS */ 24 | 25 | /// @notice Reverts if the address is the zero address. 26 | error ZeroAddress(); 27 | 28 | /// @notice Reverts if the address is the contract address. 29 | error SelfAddress(); 30 | 31 | /* CONSTRUCTOR */ 32 | 33 | /// @dev morphoToken address can be precomputed using create2. 34 | constructor(address morphoToken) { 35 | require(morphoToken != address(0), ZeroAddress()); 36 | 37 | NEW_MORPHO = morphoToken; 38 | } 39 | 40 | /* EXTERNAL */ 41 | 42 | /// @dev Compliant to `ERC20Wrapper` contract from OZ for convenience. 43 | function depositFor(address account, uint256 value) external returns (bool) { 44 | require(account != address(0), ZeroAddress()); 45 | require(account != address(this), SelfAddress()); 46 | 47 | IERC20(LEGACY_MORPHO).transferFrom(msg.sender, address(this), value); 48 | IERC20(NEW_MORPHO).transfer(account, value); 49 | return true; 50 | } 51 | 52 | /// @dev Compliant to `ERC20Wrapper` contract from OZ for convenience. 53 | function withdrawTo(address account, uint256 value) external returns (bool) { 54 | require(account != address(0), ZeroAddress()); 55 | require(account != address(this), SelfAddress()); 56 | 57 | IERC20(NEW_MORPHO).transferFrom(msg.sender, address(this), value); 58 | IERC20(LEGACY_MORPHO).transfer(account, value); 59 | return true; 60 | } 61 | 62 | /// @dev To ease wrapping via the bundler contract: 63 | /// https://github.com/morpho-org/morpho-blue-bundlers/blob/main/src/ERC20WrapperBundler.sol 64 | function underlying() external pure returns (address) { 65 | return LEGACY_MORPHO; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/helpers/SigUtils.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import { 5 | IERC5267 6 | } from "../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC5267.sol"; 7 | import {Delegation, Signature} from "../../src/DelegationToken.sol"; 8 | 9 | struct Permit { 10 | address owner; 11 | address spender; 12 | uint256 value; 13 | uint256 nonce; 14 | uint256 deadline; 15 | } 16 | 17 | library SigUtils { 18 | bytes32 internal constant DELEGATION_TYPEHASH = 19 | keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); 20 | 21 | bytes32 internal constant PERMIT_TYPEHASH = 22 | keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); 23 | 24 | bytes32 internal constant TYPE_HASH = 25 | keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); 26 | 27 | /// @dev Computes the hash of the EIP-712 encoded data. 28 | function getDelegationTypedDataHash(Delegation memory delegation, address contractAddress) 29 | internal 30 | view 31 | returns (bytes32) 32 | { 33 | (, string memory name, string memory version,,,,) = IERC5267(contractAddress).eip712Domain(); 34 | return keccak256( 35 | bytes.concat("\x19\x01", domainSeparator(contractAddress, name, version), delegationHashStruct(delegation)) 36 | ); 37 | } 38 | 39 | function getPermitTypedDataHash(Permit memory permit, address contractAddress) internal view returns (bytes32) { 40 | (, string memory name, string memory version,,,,) = IERC5267(contractAddress).eip712Domain(); 41 | return 42 | keccak256( 43 | bytes.concat("\x19\x01", domainSeparator(contractAddress, name, version), permitHashStruct(permit)) 44 | ); 45 | } 46 | 47 | function delegationHashStruct(Delegation memory delegation) internal pure returns (bytes32) { 48 | return keccak256(abi.encode(DELEGATION_TYPEHASH, delegation.delegatee, delegation.nonce, delegation.expiry)); 49 | } 50 | 51 | function permitHashStruct(Permit memory permit) internal pure returns (bytes32) { 52 | return keccak256( 53 | abi.encode(PERMIT_TYPEHASH, permit.owner, permit.spender, permit.value, permit.nonce, permit.deadline) 54 | ); 55 | } 56 | 57 | function domainSeparator(address contractAddress, string memory name, string memory version) 58 | internal 59 | view 60 | returns (bytes32) 61 | { 62 | return keccak256( 63 | abi.encode(TYPE_HASH, keccak256(bytes(name)), keccak256(bytes(version)), block.chainid, contractAddress) 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /certora/applyMunging.patch: -------------------------------------------------------------------------------- 1 | diff -ruN DelegationToken.sol DelegationToken.sol 2 | --- DelegationToken.sol 2024-11-19 09:16:52.491545137 +0100 3 | +++ DelegationToken.sol 2024-12-04 18:39:10.493804864 +0100 4 | @@ -44,6 +44,9 @@ 5 | mapping(address => uint256) _delegationNonce; 6 | } 7 | 8 | + // A ghost variable to track the theoretical voting power of address zero. 9 | + uint256 _zeroVirtualVotingPower = 0; 10 | + 11 | /* ERRORS */ 12 | 13 | /// @notice The signature used has expired. 14 | @@ -148,12 +151,16 @@ 15 | uint256 newValue = oldValue - amount; 16 | $._delegatedVotingPower[from] = newValue; 17 | emit DelegatedVotingPowerChanged(from, oldValue, newValue); 18 | + } else { 19 | + _zeroVirtualVotingPower -= amount; 20 | } 21 | if (to != address(0)) { 22 | uint256 oldValue = $._delegatedVotingPower[to]; 23 | uint256 newValue = oldValue + amount; 24 | $._delegatedVotingPower[to] = newValue; 25 | emit DelegatedVotingPowerChanged(to, oldValue, newValue); 26 | + } else { 27 | + _zeroVirtualVotingPower += amount; 28 | } 29 | } 30 | } 31 | diff -ruN MorphoTokenEthereum.sol MorphoTokenEthereum.sol 32 | --- MorphoTokenEthereum.sol 2024-11-08 00:23:18.514227368 +0100 33 | +++ MorphoTokenEthereum.sol 2024-12-04 18:39:10.493804864 +0100 34 | @@ -26,16 +26,19 @@ 35 | __ERC20Permit_init(NAME); 36 | __Ownable_init(owner); 37 | 38 | + _zeroVirtualVotingPower = 1_000_000_000e18; 39 | _mint(wrapper, 1_000_000_000e18); // Mint 1B to the wrapper contract. 40 | } 41 | 42 | /// @notice Mints tokens. 43 | function mint(address to, uint256 amount) external onlyOwner { 44 | + _zeroVirtualVotingPower += amount; 45 | _mint(to, amount); 46 | } 47 | 48 | /// @notice Burns sender's tokens. 49 | function burn(uint256 amount) external { 50 | _burn(_msgSender(), amount); 51 | + _zeroVirtualVotingPower -= amount; 52 | } 53 | } 54 | diff -ruN MorphoTokenOptimism.sol MorphoTokenOptimism.sol 55 | --- MorphoTokenOptimism.sol 2024-11-08 00:23:18.514227368 +0100 56 | +++ MorphoTokenOptimism.sol 2024-12-04 18:39:10.493804864 +0100 57 | @@ -69,12 +69,14 @@ 58 | 59 | /// @dev Allows the StandardBridge on this network to mint tokens. 60 | function mint(address to, uint256 amount) external onlyBridge { 61 | + _zeroVirtualVotingPower += amount; 62 | _mint(to, amount); 63 | } 64 | 65 | /// @dev Allows the StandardBridge on this network to burn tokens. 66 | function burn(address from, uint256 amount) external onlyBridge { 67 | _burn(from, amount); 68 | + _zeroVirtualVotingPower -= amount; 69 | } 70 | 71 | /// @notice ERC165 interface check function. 72 | -------------------------------------------------------------------------------- /certora/specs/RevertsERC20.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | methods { 4 | function totalSupply() external returns uint256 envfree; 5 | function balanceOf(address) external returns uint256 envfree; 6 | function allowance(address, address) external returns uint256 envfree; 7 | function delegatee(address) external returns address envfree; 8 | function delegatedVotingPower(address) external returns uint256 envfree; 9 | } 10 | 11 | // Check the revert conditions for the transfer function. 12 | rule transferRevertConditions(env e, address to, uint256 amount) { 13 | uint256 balanceOfSenderBefore = balanceOf(e.msg.sender); 14 | uint256 senderVotingPowerBefore = delegatedVotingPower(delegatee(e.msg.sender)); 15 | uint256 recipientVotingPowerBefore = delegatedVotingPower(delegatee(to)); 16 | 17 | // Safe require as it is verified in delegatedLTEqDelegateeVP. 18 | require senderVotingPowerBefore >= balanceOfSenderBefore; 19 | // Safe require that follows from sumOfTwoDelegatedVPLTEqTotalVP() and totalSupplyIsSumOfVirtualVotingPower(). 20 | require delegatee(to) != delegatee(e.msg.sender) => recipientVotingPowerBefore + senderVotingPowerBefore <= totalSupply(); 21 | 22 | transfer@withrevert(e, to, amount); 23 | assert lastReverted <=> e.msg.sender == 0 || to == 0 || balanceOfSenderBefore < amount || e.msg.value != 0; 24 | } 25 | 26 | // Check the revert conditions for the transferFrom function. 27 | rule transferFromRevertConditions(env e, address from, address to, uint256 amount) { 28 | uint256 allowanceOfSenderBefore = allowance(from, e.msg.sender); 29 | uint256 balanceOfHolderBefore = balanceOf(from); 30 | uint256 holderVotingPowerBefore = delegatedVotingPower(delegatee(from)); 31 | uint256 recipientVotingPowerBefore = delegatedVotingPower(delegatee(to)); 32 | 33 | // Safe require as it is verified in delegatedLTEqDelegateeVP. 34 | require holderVotingPowerBefore >= balanceOfHolderBefore; 35 | // Safe require that follows from sumOfTwoDelegatedVPLTEqTotalVP() and totalSupplyIsSumOfVirtualVotingPower(). 36 | require delegatee(to) != delegatee(from) => recipientVotingPowerBefore + holderVotingPowerBefore <= totalSupply(); 37 | 38 | transferFrom@withrevert(e, from, to, amount); 39 | 40 | bool generalRevertConditions = from == 0 || to == 0 || balanceOfHolderBefore < amount || e.msg.value != 0; 41 | 42 | if (allowanceOfSenderBefore != max_uint256) { 43 | assert lastReverted <=> e.msg.sender == 0 || allowanceOfSenderBefore < amount || generalRevertConditions; 44 | } else { 45 | assert lastReverted <=> generalRevertConditions; 46 | } 47 | 48 | } 49 | 50 | // Check the revert conditions for the approve function. 51 | rule approveRevertConditions(env e, address to, uint256 value) { 52 | approve@withrevert(e, to, value); 53 | assert lastReverted <=> e.msg.sender == 0 || to == 0 || e.msg.value != 0; 54 | } 55 | -------------------------------------------------------------------------------- /src/MorphoTokenOptimism.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.27; 3 | 4 | import {IOptimismMintableERC20} from "./interfaces/IOptimismMintableERC20.sol"; 5 | import { 6 | IERC165 7 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol"; 8 | 9 | import {DelegationToken} from "./DelegationToken.sol"; 10 | 11 | /// @title MorphoTokenOptimism 12 | /// @author Morpho Association 13 | /// @custom:security-contact security@morpho.org 14 | /// @notice The Morpho token contract for Optimism networks. 15 | contract MorphoTokenOptimism is DelegationToken, IOptimismMintableERC20 { 16 | /* CONSTANTS */ 17 | 18 | /// @dev The name of the token. 19 | string internal constant NAME = "Morpho Token"; 20 | 21 | /// @dev The symbol of the token. 22 | string internal constant SYMBOL = "MORPHO"; 23 | 24 | /// @notice The Morpho token on Ethereum. 25 | /// @dev Does not follow our classic naming convention to suits Optimism' standard. 26 | address public immutable remoteToken; 27 | 28 | /// @notice The StandardBridge. 29 | /// @dev Does not follow our classic naming convention to suits Optimism' standard. 30 | address public immutable bridge; 31 | 32 | /* ERRORS */ 33 | 34 | /// @notice Thrown if the address is the zero address. 35 | error ZeroAddress(); 36 | 37 | /// @notice Thrown if the caller is not the bridge. 38 | error NotBridge(); 39 | 40 | /* CONSTRUCTOR */ 41 | 42 | /// @notice Construct the contract. 43 | /// @param newRemoteToken The remote token address. 44 | /// @param newBridge The bridge address. 45 | constructor(address newRemoteToken, address newBridge) { 46 | require(newRemoteToken != address(0), ZeroAddress()); 47 | require(newBridge != address(0), ZeroAddress()); 48 | 49 | remoteToken = newRemoteToken; 50 | bridge = newBridge; 51 | } 52 | 53 | /* MODIFIERS */ 54 | 55 | /// @dev A modifier that only allows the bridge to call. 56 | modifier onlyBridge() { 57 | require(_msgSender() == bridge, NotBridge()); 58 | _; 59 | } 60 | 61 | /* EXTERNAL */ 62 | 63 | /// @notice Initializes the contract. 64 | /// @param owner The new owner. 65 | function initialize(address owner) external initializer { 66 | __ERC20_init(NAME, SYMBOL); 67 | __ERC20Permit_init(NAME); 68 | __Ownable_init(owner); 69 | } 70 | 71 | /// @dev Allows the StandardBridge on this network to mint tokens. 72 | function mint(address to, uint256 amount) external onlyBridge { 73 | _mint(to, amount); 74 | } 75 | 76 | /// @dev Allows the StandardBridge on this network to burn tokens. 77 | function burn(address from, uint256 amount) external onlyBridge { 78 | _burn(from, amount); 79 | } 80 | 81 | /// @notice ERC165 interface check function. 82 | /// @param _interfaceId Interface ID to check. 83 | /// @return Whether or not the interface is supported by this contract. 84 | function supportsInterface(bytes4 _interfaceId) external pure returns (bool) { 85 | return _interfaceId == type(IERC165).interfaceId || _interfaceId == type(IOptimismMintableERC20).interfaceId; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/DelegationTokenInternalTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "../lib/forge-std/src/Test.sol"; 5 | import {DelegationToken} from "../src/DelegationToken.sol"; 6 | 7 | contract DelegationTokenInternalTest is Test, DelegationToken { 8 | uint256 internal constant MAX_TEST_AMOUNT = 1e28; 9 | 10 | function __getInitializableStorage() internal pure returns (InitializableStorage storage $) { 11 | assembly { 12 | $.slot := 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00 13 | } 14 | } 15 | 16 | function _delegatedVotingPower(address account) internal view returns (uint256) { 17 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 18 | return $._delegatedVotingPower[account]; 19 | } 20 | 21 | function testDisabledInitializers() public view { 22 | InitializableStorage storage $ = __getInitializableStorage(); 23 | assertEq($._initialized, type(uint64).max, "Initializers not disabled"); 24 | } 25 | 26 | function testDelegationTokenStorageLocation() public pure { 27 | bytes32 expectedSlot = 28 | keccak256(abi.encode(uint256(keccak256("morpho.storage.DelegationToken")) - 1)) & ~bytes32(uint256(0xff)); 29 | bytes32 usedSlot = DELEGATION_TOKEN_STORAGE_LOCATION; 30 | assertEq(expectedSlot, usedSlot, "Wrong slot used"); 31 | } 32 | 33 | function testMoveDelegateVotesDifferentAccounts( 34 | address from, 35 | address to, 36 | uint256 initialVoteFrom, 37 | uint256 initialVoteTo, 38 | uint256 amount 39 | ) public { 40 | vm.assume(from != to); 41 | // Setup 42 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 43 | initialVoteFrom = bound(initialVoteFrom, 0, MAX_TEST_AMOUNT); 44 | $._delegatedVotingPower[from] = initialVoteFrom; 45 | initialVoteTo = bound(initialVoteTo, 0, MAX_TEST_AMOUNT); 46 | $._delegatedVotingPower[to] = initialVoteTo; 47 | 48 | assertEq(_delegatedVotingPower(from), initialVoteFrom); 49 | assertEq(_delegatedVotingPower(to), initialVoteTo); 50 | 51 | // Test 52 | amount = bound(amount, 0, initialVoteFrom); 53 | _moveDelegateVotes(from, to, amount); 54 | 55 | uint256 expectedVoteFrom = from == address(0) ? initialVoteFrom : initialVoteFrom - amount; 56 | uint256 expectedVoteTo = to == address(0) ? initialVoteTo : initialVoteTo + amount; 57 | 58 | assertEq(_delegatedVotingPower(from), expectedVoteFrom, "move delegate from"); 59 | assertEq(_delegatedVotingPower(to), expectedVoteTo, "move delegate to"); 60 | } 61 | 62 | function testMoveDelegateVotesSameAccounts(address account, uint256 initialVote, uint256 amount) public { 63 | // Setup 64 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 65 | $._delegatedVotingPower[account] = initialVote; 66 | assertEq(_delegatedVotingPower(account), initialVote); 67 | 68 | // Test 69 | _moveDelegateVotes(account, account, amount); 70 | 71 | assertEq(_delegatedVotingPower(account), initialVote, "unchanged delegate account"); 72 | } 73 | 74 | function testDelegate(address delegator, address oldDelegatee, address newDelegatee) public { 75 | // Setup 76 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 77 | $._delegatee[delegator] = oldDelegatee; 78 | assertEq(delegatee(delegator), oldDelegatee); 79 | 80 | // Test 81 | _delegate(delegator, newDelegatee); 82 | assertEq(delegatee(delegator), newDelegatee); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /certora/README.md: -------------------------------------------------------------------------------- 1 | # Morpho token contract formal verification 2 | 3 | This folder contains the [CVL](https://docs.certora.com/en/latest/docs/cvl/index.html) specification and verification setup for the [MorphoTokenEthereum](../src/MorphoTokenEthereum.sol) and [MorphoTokenOptimism](../src/MorphoTokenOptimism.sol) contracts. 4 | 5 | ## Getting started 6 | 7 | The verification is performed on modified source files, which can generated with the command: 8 | 9 | ``` 10 | make -C certora munged 11 | ``` 12 | 13 | This project depends on [Solidity](https://soliditylang.org/) which is required for running the verification. 14 | The compiler binary should be available in the path: 15 | 16 | - `solc-0.8.27` for the solidity compiler version `0.8.27`. 17 | 18 | To verify a specification, run the command `certoraRun Spec.conf` where `Spec.conf` is the configuration file of the matching CVL specification. 19 | Configuration files are available in [`certora/confs`](confs). 20 | Please ensure that `CERTORAKEY` is set up in your environment. 21 | 22 | ## Overview 23 | 24 | These Morpho token is an ERC20 token with support for delegation of voting power, upgradeability and cross-chain interactions. 25 | Despite the contract being upgradeable, we verify however that the implementation doesn't perform delegate calls, which implies that the implementation is immutable. 26 | 27 | Note: the compiled contracts may include loops related to handling strings from the EIP712, for this reason the verification is carried with the option `optimistic_loop` set to `true` in order to avoid related counterexamples. 28 | 29 | ### External calls 30 | 31 | This is checked in [`ExternalCalls.spec`](specs/ExternalCalls.spec). 32 | 33 | ### ERC20 Compliance and Correctness 34 | 35 | This is checked in [`ERC20.spec`](specs/ERC20.spec), [`ERC20Invariants.spec`](specs/ERC20Invariants.spec), [`MintBurnEthereum.spec`](specs/MintBurnEthereum.spec) and [`MintBurnOptimism.spec`](specs/MintBurnOptimism.spec). 36 | 37 | ### Delegation Correctness 38 | 39 | This is checked in [`Delegation.spec`](specs/Delegation.spec). 40 | 41 | ### Reverts 42 | 43 | This is checks in [`RevertsERC20.spec`](specs/RevertsERC20.spec), [`RevertsMintBurnEthereum.spec`](specs/RevertsMintBurnEthereum.spec) and [`RevertsMintBurnOptimism.spec`](specs/RevertsMintBurnOptimism.spec). 44 | 45 | ## Verification architecture 46 | 47 | ### Folders and file structure 48 | 49 | The [`certora/specs`](specs) folder contains the following files: 50 | 51 | - [`ExternalCalls.spec`](specs/ExternalCalls.spec) checks that the Morpho token implementation is reentrancy safe by ensuring that no function is making and external calls and, that the implementation is immutable as it doesn't perform any delegate call; 52 | - [`ERC20Invariants.spec`](specs/ERC20Invariants.spec) common hooks and invariants to be shared in different specs; 53 | - [`ERC20.spec`](specs/ERC20.spec) ensures that the Morpho token is compliant with the [ERC20](https://eips.ethereum.org/EIPS/eip-20) specification, we also check Morpho token `burn` and `mint` functions in [`MintBurnEthereum`](specs/MintBurnEthereum.spec) and [`MintBurnOptimism`](specs/MintBurnOptimism.spec); 54 | - [`Delegation.spec`](specs/Delegation.spec) checks the logic for voting power delegation. 55 | - [`RevertsERC20.spec`](specs/RevertsERC20.spec), [`RevertsMintBurnEthereum.spec`](specs/RevertsMintBurnEthereum.spec) and [`RevertsMintBurnOptimism.spec`](specs/RevertsMintBurnOptimism.spec) check that conditions for reverts and inputs are correctly validated. 56 | 57 | The [`certora/confs`](confs) folder contains a configuration file for each corresponding specification file for both the Ethereum and the Optimism version. 58 | 59 | The [`certora/Makefile`](Makefile) is used to track and perform the required modifications on source files. 60 | -------------------------------------------------------------------------------- /certora/specs/MintBurnEthereum.spec: -------------------------------------------------------------------------------- 1 | // This is spec is taken from the Open Zeppelin repositories at https://github.com/OpenZeppelin/openzeppelin-contracts/blob/448efeea6640bbbc09373f03fbc9c88e280147ba/certora/specs/ERC20.spec, and patched to support the DelegationToken. 2 | 3 | import "Delegation.spec"; 4 | 5 | /* 6 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │ Rules: only the token holder or an approved third party can reduce an account's balance │ 8 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | rule onlyAuthorizedCanTransfer(env e, method f) { 11 | requireInvariant totalSupplyIsSumOfBalances(); 12 | requireInvariant balancesLTEqTotalSupply(); 13 | requireInvariant twoBalancesLTEqTotalSupply(); 14 | 15 | calldataarg args; 16 | address account; 17 | 18 | uint256 allowanceBefore = allowance(account, e.msg.sender); 19 | uint256 balanceBefore = balanceOf(account); 20 | f(e, args); 21 | uint256 balanceAfter = balanceOf(account); 22 | 23 | assert ( 24 | balanceAfter < balanceBefore 25 | ) => ( 26 | e.msg.sender == account || 27 | f.selector == sig:transferFrom(address, address, uint256).selector && balanceBefore - balanceAfter <= to_mathint(allowanceBefore) 28 | ); 29 | } 30 | 31 | /* 32 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 33 | │ Rules: only mint and burn can change total supply │ 34 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 35 | */ 36 | rule noChangeTotalSupply(env e) { 37 | requireInvariant totalSupplyIsSumOfBalances(); 38 | requireInvariant balancesLTEqTotalSupply(); 39 | 40 | method f; 41 | calldataarg args; 42 | 43 | uint256 totalSupplyBefore = totalSupply(); 44 | f(e, args); 45 | uint256 totalSupplyAfter = totalSupply(); 46 | 47 | assert totalSupplyAfter > totalSupplyBefore => f.selector == sig:mint(address,uint256).selector || f.selector == sig:initialize(address, address).selector; 48 | assert totalSupplyAfter < totalSupplyBefore => f.selector == sig:burn(uint256).selector; 49 | } 50 | 51 | /* 52 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 53 | │ Rules: mint behavior and side effects │ 54 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 55 | */ 56 | rule mint(env e) { 57 | requireInvariant totalSupplyIsSumOfBalances(); 58 | requireInvariant balancesLTEqTotalSupply(); 59 | requireInvariant delegatedVotingPowerLTEqTotalVotingPower(); 60 | assert isTotalSupplyGTEqSumOfVotingPower(); 61 | requireInvariant zeroAddressNoVotingPower(); 62 | require nonpayable(e); 63 | 64 | address to; 65 | address other; 66 | uint256 amount; 67 | 68 | // cache state 69 | uint256 toBalanceBefore = balanceOf(to); 70 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(to)); 71 | uint256 otherBalanceBefore = balanceOf(other); 72 | uint256 totalSupplyBefore = totalSupply(); 73 | 74 | // run transaction 75 | mint@withrevert(e, to, amount); 76 | 77 | // check outcome 78 | if (lastReverted) { 79 | assert e.msg.sender != owner() || to == 0 || totalSupplyBefore + amount > max_uint256 || 80 | toVotingPowerBefore + amount > max_uint256; 81 | } else { 82 | // updates balance and totalSupply 83 | assert e.msg.sender == owner(); 84 | assert to_mathint(balanceOf(to)) == toBalanceBefore + amount; 85 | assert to_mathint(totalSupply()) == totalSupplyBefore + amount; 86 | 87 | // no other balance is modified 88 | assert balanceOf(other) != otherBalanceBefore => other == to; 89 | } 90 | } 91 | 92 | /* 93 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 94 | │ Rules: burn behavior and side effects │ 95 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 96 | */ 97 | rule burn(env e) { 98 | requireInvariant balancesLTEqTotalSupply(); 99 | assert isTotalSupplyGTEqSumOfVotingPower(); 100 | requireInvariant zeroAddressNoVotingPower(); 101 | requireInvariant delegatedVotingPowerLTEqTotalVotingPower(); 102 | require nonpayable(e); 103 | 104 | address from; 105 | address other; 106 | uint256 amount; 107 | require from == e.msg.sender; 108 | // cache state 109 | uint256 fromBalanceBefore = balanceOf(from); 110 | uint256 fromVotingPowerBefore = delegatedVotingPower(delegatee(from)); 111 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(0x0)); 112 | uint256 otherBalanceBefore = balanceOf(other); 113 | uint256 totalSupplyBefore = totalSupply(); 114 | 115 | // run transaction 116 | burn@withrevert(e, amount); 117 | 118 | // check outcome 119 | if (lastReverted) { 120 | assert e.msg.sender == 0x0 || fromBalanceBefore < amount || fromVotingPowerBefore < amount ; 121 | } else { 122 | // updates balance and totalSupply 123 | assert to_mathint(balanceOf(from)) == fromBalanceBefore - amount; 124 | assert to_mathint(totalSupply()) == totalSupplyBefore - amount; 125 | 126 | // no other balance is modified 127 | assert balanceOf(other) != otherBalanceBefore => other == from; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /certora/specs/MintBurnOptimism.spec: -------------------------------------------------------------------------------- 1 | // This is spec is taken from the Open Zeppelin repositories at https://github.com/OpenZeppelin/openzeppelin-contracts/blob/448efeea6640bbbc09373f03fbc9c88e280147ba/certora/specs/ERC20.spec, and patched to support the DelegationToken. 2 | 3 | import "Delegation.spec"; 4 | 5 | /* 6 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 7 | │ Rules: only the token holder or an approved third party can reduce an account's balance │ 8 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 9 | */ 10 | rule onlyAuthorizedCanTransfer(env e, method f) { 11 | requireInvariant totalSupplyIsSumOfBalances(); 12 | requireInvariant balancesLTEqTotalSupply(); 13 | requireInvariant twoBalancesLTEqTotalSupply(); 14 | 15 | calldataarg args; 16 | address account; 17 | 18 | uint256 allowanceBefore = allowance(account, e.msg.sender); 19 | uint256 balanceBefore = balanceOf(account); 20 | f(e, args); 21 | uint256 balanceAfter = balanceOf(account); 22 | 23 | assert ( 24 | balanceAfter < balanceBefore 25 | ) => ( 26 | f.selector == sig:burn(address, uint256).selector || 27 | e.msg.sender == account || 28 | f.selector == sig:transferFrom(address, address, uint256).selector && balanceBefore - balanceAfter <= to_mathint(allowanceBefore) 29 | ); 30 | } 31 | 32 | /* 33 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 34 | │ Rules: only mint and burn can change total supply │ 35 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 36 | */ 37 | rule noChangeTotalSupply(env e) { 38 | requireInvariant totalSupplyIsSumOfBalances(); 39 | requireInvariant balancesLTEqTotalSupply(); 40 | 41 | method f; 42 | calldataarg args; 43 | 44 | uint256 totalSupplyBefore = totalSupply(); 45 | f(e, args); 46 | uint256 totalSupplyAfter = totalSupply(); 47 | 48 | assert totalSupplyAfter > totalSupplyBefore => f.selector == sig:mint(address, uint256).selector; 49 | assert totalSupplyAfter < totalSupplyBefore => f.selector == sig:burn(address, uint256).selector; 50 | } 51 | 52 | /* 53 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 54 | │ Rules: mint behavior and side effects │ 55 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 56 | */ 57 | rule mint(env e) { 58 | requireInvariant totalSupplyIsSumOfBalances(); 59 | requireInvariant balancesLTEqTotalSupply(); 60 | assert isTotalSupplyGTEqSumOfVotingPower(); 61 | requireInvariant delegatedVotingPowerLTEqTotalVotingPower(); 62 | requireInvariant zeroAddressNoVotingPower(); 63 | require nonpayable(e); 64 | 65 | address to; 66 | address other; 67 | uint256 amount; 68 | 69 | // cache state 70 | uint256 toBalanceBefore = balanceOf(to); 71 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(to)); 72 | uint256 otherBalanceBefore = balanceOf(other); 73 | uint256 totalSupplyBefore = totalSupply(); 74 | 75 | // run transaction 76 | mint@withrevert(e, to, amount); 77 | 78 | // check outcome 79 | if (lastReverted) { 80 | assert e.msg.sender != owner() || to == 0 || totalSupplyBefore + amount > max_uint256 || 81 | toVotingPowerBefore + amount > max_uint256 || e.msg.sender != currentContract.bridge; 82 | } else { 83 | // updates balance and totalSupply 84 | assert e.msg.sender == currentContract.bridge; 85 | assert to_mathint(balanceOf(to)) == toBalanceBefore + amount; 86 | assert to_mathint(totalSupply()) == totalSupplyBefore + amount; 87 | 88 | // no other balance is modified 89 | assert balanceOf(other) != otherBalanceBefore => other == to; 90 | } 91 | } 92 | 93 | /* 94 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 95 | │ Rules: burn behavior and side effects │ 96 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 97 | */ 98 | rule burn(env e) { 99 | requireInvariant balancesLTEqTotalSupply(); 100 | assert isTotalSupplyGTEqSumOfVotingPower(); 101 | requireInvariant delegatedVotingPowerLTEqTotalVotingPower(); 102 | requireInvariant zeroAddressNoVotingPower(); 103 | require nonpayable(e); 104 | 105 | address from; 106 | address other; 107 | uint256 amount; 108 | require from == e.msg.sender; 109 | // cache state 110 | uint256 fromBalanceBefore = balanceOf(from); 111 | uint256 fromVotingPowerBefore = delegatedVotingPower(delegatee(from)); 112 | uint256 toVotingPowerBefore = delegatedVotingPower(delegatee(0x0)); 113 | uint256 otherBalanceBefore = balanceOf(other); 114 | uint256 totalSupplyBefore = totalSupply(); 115 | 116 | // run transaction 117 | burn@withrevert(e, from, amount); 118 | 119 | // check outcome 120 | if (lastReverted) { 121 | assert e.msg.sender == 0x0 || fromBalanceBefore < amount || fromVotingPowerBefore < amount 122 | || e.msg.sender != currentContract.bridge; 123 | } else { 124 | // updates balance and totalSupply 125 | assert to_mathint(balanceOf(from)) == fromBalanceBefore - amount; 126 | assert to_mathint(totalSupply()) == totalSupplyBefore - amount; 127 | 128 | // no other balance is modified 129 | assert balanceOf(other) != otherBalanceBefore => other == from; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/MorphoTokenOptimismTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {Test, console} from "../lib/forge-std/src/Test.sol"; 5 | import {MorphoTokenOptimism} from "../src/MorphoTokenOptimism.sol"; 6 | import {DelegationToken} from "../src/DelegationToken.sol"; 7 | import { 8 | ERC1967Proxy 9 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 10 | import {UUPSUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 11 | import {OwnableUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; 12 | import { 13 | IERC1967 14 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; 15 | import { 16 | IERC20 17 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 18 | import {IOptimismMintableERC20, IERC165} from "../src/interfaces/IOptimismMintableERC20.sol"; 19 | 20 | contract MorphoTokenOptimismTest is Test { 21 | address internal constant MORPHO_DAO = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa; 22 | address internal REMOTE_TOKEN; 23 | address internal BRIDGE; 24 | 25 | MorphoTokenOptimism public tokenImplem; 26 | MorphoTokenOptimism public morphoOptimism; 27 | ERC1967Proxy public tokenProxy; 28 | 29 | uint256 internal constant MIN_TEST_AMOUNT = 100; 30 | uint256 internal constant MAX_TEST_AMOUNT = 1e28; 31 | 32 | function setUp() public virtual { 33 | REMOTE_TOKEN = makeAddr("RemoteToken"); 34 | BRIDGE = makeAddr("Bridge"); 35 | 36 | // DEPLOYMENTS 37 | tokenImplem = new MorphoTokenOptimism(REMOTE_TOKEN, BRIDGE); 38 | tokenProxy = new ERC1967Proxy(address(tokenImplem), hex""); 39 | 40 | morphoOptimism = MorphoTokenOptimism(payable(address(tokenProxy))); 41 | morphoOptimism.initialize(MORPHO_DAO); 42 | } 43 | 44 | function testDeployImplemZeroAddress(address randomAddress) public { 45 | vm.assume(randomAddress != address(0)); 46 | 47 | vm.expectRevert(MorphoTokenOptimism.ZeroAddress.selector); 48 | tokenImplem = new MorphoTokenOptimism(address(0), randomAddress); 49 | 50 | vm.expectRevert(MorphoTokenOptimism.ZeroAddress.selector); 51 | tokenImplem = new MorphoTokenOptimism(randomAddress, address(0)); 52 | } 53 | 54 | function testInitializeZeroAddress(address randomAddress) public { 55 | vm.assume(randomAddress != address(0)); 56 | 57 | address proxy = address(new ERC1967Proxy(address(tokenImplem), hex"")); 58 | 59 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0))); 60 | MorphoTokenOptimism(proxy).initialize(address(0)); 61 | } 62 | 63 | function testUpgradeNotOwner(address updater) public { 64 | vm.assume(updater != address(0)); 65 | vm.assume(updater != MORPHO_DAO); 66 | 67 | address newImplem = address(new MorphoTokenOptimism(REMOTE_TOKEN, BRIDGE)); 68 | 69 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, updater)); 70 | vm.prank(updater); 71 | morphoOptimism.upgradeToAndCall(newImplem, hex""); 72 | } 73 | 74 | function testUpgrade() public { 75 | address newImplem = address(new MorphoTokenOptimism(REMOTE_TOKEN, BRIDGE)); 76 | 77 | vm.expectEmit(address(morphoOptimism)); 78 | emit IERC1967.Upgraded(newImplem); 79 | vm.prank(MORPHO_DAO); 80 | morphoOptimism.upgradeToAndCall(newImplem, hex""); 81 | } 82 | 83 | function testGetters() public view { 84 | assertEq(morphoOptimism.remoteToken(), REMOTE_TOKEN, "remoteToken"); 85 | assertEq(morphoOptimism.bridge(), BRIDGE, "bridge"); 86 | assertEq(morphoOptimism.owner(), MORPHO_DAO, "owner"); 87 | } 88 | 89 | function testMintNoBridge(address account, address to, uint256 amount) public { 90 | vm.assume(account != address(0)); 91 | vm.assume(to != address(0)); 92 | vm.assume(account != BRIDGE); 93 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 94 | 95 | vm.expectRevert(MorphoTokenOptimism.NotBridge.selector); 96 | vm.prank(account); 97 | morphoOptimism.mint(to, amount); 98 | } 99 | 100 | function testMint(address to, uint256 amount) public { 101 | vm.assume(to != address(0)); 102 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 103 | 104 | assertEq(morphoOptimism.totalSupply(), 0, "totalSupply"); 105 | assertEq(morphoOptimism.balanceOf(to), 0, "balanceOf(account)"); 106 | 107 | vm.expectEmit(address(morphoOptimism)); 108 | emit IERC20.Transfer(address(0), to, amount); 109 | vm.prank(BRIDGE); 110 | morphoOptimism.mint(to, amount); 111 | 112 | assertEq(morphoOptimism.totalSupply(), amount, "totalSupply"); 113 | assertEq(morphoOptimism.balanceOf(to), amount, "balanceOf(account)"); 114 | } 115 | 116 | function testBurnNoBridge(address account, address from, uint256 amount) public { 117 | vm.assume(account != address(0)); 118 | vm.assume(from != address(0)); 119 | vm.assume(account != BRIDGE); 120 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 121 | 122 | vm.expectRevert(MorphoTokenOptimism.NotBridge.selector); 123 | vm.prank(account); 124 | morphoOptimism.burn(from, amount); 125 | } 126 | 127 | function testBurn(address from, uint256 amountMinted, uint256 amountBurned) public { 128 | vm.assume(from != address(0)); 129 | amountMinted = bound(amountMinted, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 130 | amountBurned = bound(amountBurned, MIN_TEST_AMOUNT, amountMinted); 131 | 132 | vm.startPrank(BRIDGE); 133 | morphoOptimism.mint(from, amountMinted); 134 | 135 | vm.expectEmit(address(morphoOptimism)); 136 | emit IERC20.Transfer(from, address(0), amountBurned); 137 | morphoOptimism.burn(from, amountBurned); 138 | vm.stopPrank(); 139 | 140 | assertEq(morphoOptimism.totalSupply(), amountMinted - amountBurned, "totalSupply"); 141 | assertEq(morphoOptimism.balanceOf(from), amountMinted - amountBurned, "balanceOf(account)"); 142 | } 143 | 144 | function testSupportsInterface(bytes4 randomInterface) public view { 145 | vm.assume(randomInterface != type(IERC165).interfaceId); 146 | vm.assume(randomInterface != type(IOptimismMintableERC20).interfaceId); 147 | 148 | assertFalse(morphoOptimism.supportsInterface(randomInterface), "supports random interface"); 149 | assertTrue(morphoOptimism.supportsInterface(type(IERC165).interfaceId), "doesn't support IERC165"); 150 | assertTrue( 151 | morphoOptimism.supportsInterface(type(IOptimismMintableERC20).interfaceId), 152 | "doesn't support IOptimismMintableERC20" 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/DelegationToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.27; 3 | 4 | import {IDelegation, Signature, Delegation} from "./interfaces/IDelegation.sol"; 5 | 6 | import { 7 | ERC20PermitUpgradeable 8 | } from "../lib/openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; 9 | import { 10 | ECDSA 11 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; 12 | import { 13 | Ownable2StepUpgradeable 14 | } from "../lib/openzeppelin-contracts-upgradeable/contracts/access/Ownable2StepUpgradeable.sol"; 15 | import {UUPSUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol"; 16 | import { 17 | ERC1967Utils 18 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; 19 | 20 | /// @title DelegationToken 21 | /// @author Morpho Association 22 | /// @custom:security-contact security@morpho.org 23 | /// @dev Extension of ERC20 to support token delegation. 24 | /// 25 | /// This extension keeps track of the current voting power delegated to each account. Voting power can be delegated 26 | /// either by calling the `delegate` function directly, or by providing a signature to be used with `delegateBySig`. 27 | /// 28 | /// This enables onchain votes on external voting smart contracts leveraging storage proofs. 29 | /// 30 | /// By default, token balance does not account for voting power. This makes transfers cheaper. Whether an account 31 | /// has to self-delegate to vote depends on the voting contract implementation. 32 | abstract contract DelegationToken is IDelegation, ERC20PermitUpgradeable, Ownable2StepUpgradeable, UUPSUpgradeable { 33 | /* CONSTANTS */ 34 | 35 | bytes32 internal constant DELEGATION_TYPEHASH = 36 | keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); 37 | 38 | // keccak256(abi.encode(uint256(keccak256("morpho.storage.DelegationToken")) - 1)) & ~bytes32(uint256(0xff)) 39 | bytes32 internal constant DELEGATION_TOKEN_STORAGE_LOCATION = 40 | 0x669be2f4ee1b0b5f3858e4135f31064efe8fa923b09bf21bf538f64f2c3e1100; 41 | 42 | /* STORAGE LAYOUT */ 43 | 44 | /// @custom:storage-location erc7201:morpho.storage.DelegationToken 45 | struct DelegationTokenStorage { 46 | mapping(address => address) _delegatee; 47 | mapping(address => uint256) _delegatedVotingPower; 48 | mapping(address => uint256) _delegationNonce; 49 | } 50 | 51 | /* ERRORS */ 52 | 53 | /// @notice The signature used has expired. 54 | error DelegatesExpiredSignature(); 55 | 56 | /// @notice The delegation nonce used by the signer is not its current delegation nonce. 57 | error InvalidDelegationNonce(); 58 | 59 | /* EVENTS */ 60 | 61 | /// @notice Emitted when an delegator changes their delegatee. 62 | event DelegateeChanged(address indexed delegator, address indexed oldDelegatee, address indexed newDelegatee); 63 | 64 | /// @notice Emitted when a delegatee's delegated voting power changes. 65 | event DelegatedVotingPowerChanged(address indexed delegatee, uint256 oldVotes, uint256 newVotes); 66 | 67 | /* CONSTRUCTOR */ 68 | 69 | /// @dev Disables initializers for the implementation contract. 70 | constructor() { 71 | _disableInitializers(); 72 | } 73 | 74 | /* GETTERS */ 75 | 76 | /// @notice Returns the delegatee that `account` has chosen. 77 | function delegatee(address account) public view returns (address) { 78 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 79 | return $._delegatee[account]; 80 | } 81 | 82 | /// @notice Returns the current voting power delegated to `account`. 83 | function delegatedVotingPower(address account) external view returns (uint256) { 84 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 85 | return $._delegatedVotingPower[account]; 86 | } 87 | 88 | /// @notice Returns the current delegation nonce of `account`. 89 | function delegationNonce(address account) external view returns (uint256) { 90 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 91 | return $._delegationNonce[account]; 92 | } 93 | 94 | /// @notice Returns the contract's current implementation address. 95 | function getImplementation() external view returns (address) { 96 | return ERC1967Utils.getImplementation(); 97 | } 98 | 99 | /* DELEGATE */ 100 | 101 | /// @notice Delegates the balance of the sender to `newDelegatee`. 102 | /// @dev Delegating to the zero address effectively removes the delegation, incidentally making transfers cheaper. 103 | /// @dev Delegating to the previous delegatee does not revert. 104 | function delegate(address newDelegatee) external { 105 | address delegator = _msgSender(); 106 | _delegate(delegator, newDelegatee); 107 | } 108 | 109 | /// @notice Delegates the balance of the signer to `newDelegatee`. 110 | /// @dev Delegating to the zero address effectively removes the delegation, incidentally making transfers cheaper. 111 | /// @dev Delegating to the previous delegatee effectively revokes past signatures with the same nonce. 112 | function delegateWithSig(Delegation calldata delegation, Signature calldata signature) external { 113 | require(block.timestamp <= delegation.expiry, DelegatesExpiredSignature()); 114 | 115 | address delegator = ECDSA.recover( 116 | _hashTypedDataV4(keccak256(abi.encode(DELEGATION_TYPEHASH, delegation))), 117 | signature.v, 118 | signature.r, 119 | signature.s 120 | ); 121 | 122 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 123 | require(delegation.nonce == $._delegationNonce[delegator]++, InvalidDelegationNonce()); 124 | 125 | _delegate(delegator, delegation.delegatee); 126 | } 127 | 128 | /* INTERNAL */ 129 | 130 | /// @dev Delegates the balance of the `delegator` to `newDelegatee`. 131 | function _delegate(address delegator, address newDelegatee) internal { 132 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 133 | address oldDelegatee = $._delegatee[delegator]; 134 | $._delegatee[delegator] = newDelegatee; 135 | 136 | emit DelegateeChanged(delegator, oldDelegatee, newDelegatee); 137 | _moveDelegateVotes(oldDelegatee, newDelegatee, balanceOf(delegator)); 138 | } 139 | 140 | /// @dev Moves voting power when tokens are transferred. 141 | function _update(address from, address to, uint256 value) internal virtual override { 142 | super._update(from, to, value); 143 | _moveDelegateVotes(delegatee(from), delegatee(to), value); 144 | } 145 | 146 | /// @dev Moves delegated votes from one delegate to another. 147 | function _moveDelegateVotes(address from, address to, uint256 amount) internal { 148 | DelegationTokenStorage storage $ = _getDelegationTokenStorage(); 149 | if (from != to && amount > 0) { 150 | if (from != address(0)) { 151 | uint256 oldValue = $._delegatedVotingPower[from]; 152 | uint256 newValue = oldValue - amount; 153 | $._delegatedVotingPower[from] = newValue; 154 | emit DelegatedVotingPowerChanged(from, oldValue, newValue); 155 | } 156 | if (to != address(0)) { 157 | uint256 oldValue = $._delegatedVotingPower[to]; 158 | uint256 newValue = oldValue + amount; 159 | $._delegatedVotingPower[to] = newValue; 160 | emit DelegatedVotingPowerChanged(to, oldValue, newValue); 161 | } 162 | } 163 | } 164 | 165 | /// @dev Returns the DelegationTokenStorage struct. 166 | function _getDelegationTokenStorage() internal pure returns (DelegationTokenStorage storage $) { 167 | assembly { 168 | $.slot := DELEGATION_TOKEN_STORAGE_LOCATION 169 | } 170 | } 171 | 172 | /// @inheritdoc UUPSUpgradeable 173 | function _authorizeUpgrade(address) internal override onlyOwner {} 174 | } 175 | -------------------------------------------------------------------------------- /test/MigrationTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {console} from "../lib/forge-std/src/Test.sol"; 5 | import {BaseTest} from "./helpers/BaseTest.sol"; 6 | import {Wrapper} from "../src/Wrapper.sol"; 7 | import {IMulticall} from "./helpers/interfaces/IMulticall.sol"; 8 | import {EncodeLib} from "./helpers/libraries/EncodeLib.sol"; 9 | import { 10 | IERC20 11 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 12 | 13 | interface RolesAuthority { 14 | function setUserRole(address user, uint8 role, bool enabled) external; 15 | } 16 | 17 | contract MigrationTest is BaseTest { 18 | IMulticall internal bundler = IMulticall(0x4095F064B8d3c3548A3bebfd0Bbfd04750E30077); 19 | IERC20 internal legacyMorpho = IERC20(0x9994E35Db50125E0DF82e4c2dde62496CE330999); 20 | 21 | uint256 internal forkId; 22 | 23 | bytes[] internal bundle; 24 | 25 | function setUp() public virtual override { 26 | _fork(); 27 | 28 | super.setUp(); 29 | 30 | vm.startPrank(MORPHO_DAO); 31 | // The role 0 already has transfer capabilities. 32 | RolesAuthority(address(legacyMorpho)).setUserRole(address(wrapper), 0, true); 33 | RolesAuthority(address(legacyMorpho)).setUserRole(address(bundler), 0, true); 34 | vm.stopPrank(); 35 | } 36 | 37 | function _fork() internal virtual { 38 | string memory rpcUrl = vm.rpcUrl("ethereum"); 39 | 40 | forkId = vm.createSelectFork(rpcUrl); 41 | require(block.chainid == 1, "wrong chain"); 42 | } 43 | 44 | function testDeployWrapperZeroAddress() public { 45 | vm.expectRevert(Wrapper.ZeroAddress.selector); 46 | new Wrapper(address(0)); 47 | } 48 | 49 | function testTotalSupply() public view { 50 | assertEq(newMorpho.totalSupply(), 1_000_000_000e18); 51 | } 52 | 53 | function testInitialWrapperBalances() public view { 54 | assertEq(legacyMorpho.balanceOf(address(wrapper)), 0); 55 | assertEq(newMorpho.balanceOf(address(wrapper)), 1_000_000_000e18); 56 | } 57 | 58 | function testDepositForZeroAddress(uint256 amount) public { 59 | vm.assume(amount != 0); 60 | 61 | vm.expectRevert(Wrapper.ZeroAddress.selector); 62 | wrapper.depositFor(address(0), amount); 63 | } 64 | 65 | function testDepositForSelfAddress(uint256 amount) public { 66 | vm.assume(amount != 0); 67 | 68 | vm.expectRevert(Wrapper.SelfAddress.selector); 69 | wrapper.depositFor(address(wrapper), amount); 70 | } 71 | 72 | function testWithdrawToZeroAddress(uint256 amount) public { 73 | vm.assume(amount != 0); 74 | 75 | vm.expectRevert(Wrapper.ZeroAddress.selector); 76 | wrapper.withdrawTo(address(0), amount); 77 | } 78 | 79 | function testWithdrawToSelfAddress(uint256 amount) public { 80 | vm.assume(amount != 0); 81 | 82 | vm.expectRevert(Wrapper.SelfAddress.selector); 83 | wrapper.withdrawTo(address(wrapper), amount); 84 | } 85 | 86 | function testDAOMigration() public { 87 | uint256 daoTokenAmount = legacyMorpho.balanceOf(MORPHO_DAO); 88 | 89 | bundle.push(EncodeLib._erc20TransferFrom(address(legacyMorpho), daoTokenAmount)); 90 | bundle.push(EncodeLib._erc20WrapperDepositFor(address(wrapper), daoTokenAmount)); 91 | 92 | vm.startPrank(MORPHO_DAO); 93 | legacyMorpho.approve(address(bundler), daoTokenAmount); 94 | 95 | vm.expectEmit(address(legacyMorpho)); 96 | emit IERC20.Transfer(MORPHO_DAO, address(bundler), daoTokenAmount); 97 | vm.expectEmit(address(legacyMorpho)); 98 | emit IERC20.Approval(address(bundler), address(wrapper), type(uint256).max); 99 | vm.expectEmit(address(legacyMorpho)); 100 | emit IERC20.Transfer(address(bundler), address(wrapper), daoTokenAmount); 101 | vm.expectEmit(address(newMorpho)); 102 | emit IERC20.Transfer(address(wrapper), MORPHO_DAO, daoTokenAmount); 103 | bundler.multicall(bundle); 104 | vm.stopPrank(); 105 | 106 | assertEq(legacyMorpho.balanceOf(MORPHO_DAO), 0, "legacyMorpho.balanceOf(MORPHO_DAO)"); 107 | assertEq(legacyMorpho.balanceOf(address(wrapper)), daoTokenAmount, "legacyMorpho.balanceOf(wrapper)"); 108 | assertEq(newMorpho.balanceOf(MORPHO_DAO), daoTokenAmount, "newMorpho.balanceOf(MORPHO_DAO)"); 109 | } 110 | 111 | function testMigration(address migrator, uint256 amount) public { 112 | vm.assume(migrator != address(0)); 113 | // Unset initiator is address(1), so it can't use the bundler. 114 | vm.assume(migrator != address(1)); 115 | vm.assume(migrator != MORPHO_DAO); 116 | vm.assume(migrator != address(wrapper)); 117 | amount = bound(amount, MIN_TEST_AMOUNT, 1_000_000_000e18); 118 | 119 | deal(address(legacyMorpho), migrator, amount); 120 | 121 | bundle.push(EncodeLib._erc20TransferFrom(address(legacyMorpho), amount)); 122 | bundle.push(EncodeLib._erc20WrapperDepositFor(address(wrapper), amount)); 123 | 124 | vm.startPrank(migrator); 125 | legacyMorpho.approve(address(bundler), amount); 126 | 127 | vm.expectEmit(address(legacyMorpho)); 128 | emit IERC20.Transfer(migrator, address(bundler), amount); 129 | vm.expectEmit(address(legacyMorpho)); 130 | emit IERC20.Approval(address(bundler), address(wrapper), type(uint256).max); 131 | vm.expectEmit(address(legacyMorpho)); 132 | emit IERC20.Transfer(address(bundler), address(wrapper), amount); 133 | vm.expectEmit(address(newMorpho)); 134 | emit IERC20.Transfer(address(wrapper), migrator, amount); 135 | bundler.multicall(bundle); 136 | vm.stopPrank(); 137 | 138 | assertEq(legacyMorpho.balanceOf(migrator), 0, "legacyMorpho.balanceOf(migrator)"); 139 | assertEq(legacyMorpho.balanceOf(address(wrapper)), amount, "legacyMorpho.balanceOf(wrapper)"); 140 | assertEq(newMorpho.balanceOf(address(wrapper)), 1_000_000_000e18 - amount, "newMorpho.balanceOf(wrapper)"); 141 | assertEq(newMorpho.balanceOf(migrator), amount, "newMorpho.balanceOf(migrator)"); 142 | } 143 | 144 | function testRevertMigration(address migrator, uint256 migratedAmount, uint256 revertedAmount) public { 145 | vm.assume(migrator != address(0)); 146 | vm.assume(migrator != address(1)); 147 | vm.assume(migrator != MORPHO_DAO); 148 | vm.assume(migrator != address(wrapper)); 149 | migratedAmount = bound(migratedAmount, MIN_TEST_AMOUNT, 1_000_000_000e18); 150 | revertedAmount = bound(revertedAmount, MIN_TEST_AMOUNT, migratedAmount); 151 | 152 | deal(address(legacyMorpho), migrator, migratedAmount); 153 | 154 | bundle.push(EncodeLib._erc20TransferFrom(address(legacyMorpho), migratedAmount)); 155 | bundle.push(EncodeLib._erc20WrapperDepositFor(address(wrapper), migratedAmount)); 156 | 157 | vm.startPrank(migrator); 158 | legacyMorpho.approve(address(bundler), migratedAmount); 159 | bundler.multicall(bundle); 160 | vm.stopPrank(); 161 | 162 | vm.startPrank(migrator); 163 | newMorpho.approve(address(wrapper), revertedAmount); 164 | 165 | vm.expectEmit(address(newMorpho)); 166 | emit IERC20.Transfer(migrator, address(wrapper), revertedAmount); 167 | vm.expectEmit(address(legacyMorpho)); 168 | emit IERC20.Transfer(address(wrapper), migrator, revertedAmount); 169 | wrapper.withdrawTo(migrator, revertedAmount); 170 | vm.stopPrank(); 171 | 172 | assertEq(legacyMorpho.balanceOf(migrator), revertedAmount, "legacyMorpho.balanceOf(migrator)"); 173 | assertEq( 174 | legacyMorpho.balanceOf(address(wrapper)), migratedAmount - revertedAmount, "legacyMorpho.balanceOf(wrapper)" 175 | ); 176 | assertEq( 177 | newMorpho.balanceOf(address(wrapper)), 178 | 1_000_000_000e18 - migratedAmount + revertedAmount, 179 | "newMorpho.balanceOf(wrapper)" 180 | ); 181 | assertEq(newMorpho.balanceOf(migrator), migratedAmount - revertedAmount, "newMorpho.balanceOf(migrator)"); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /certora/specs/ERC20Invariants.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | methods { 4 | function totalSupply() external returns uint256 envfree; 5 | function balanceOf(address) external returns uint256 envfree; 6 | function delegatee(address) external returns address envfree; 7 | function delegatedVotingPower(address) external returns uint256 envfree; 8 | function upgradeToAndCall(address, bytes) external => NONDET DELETE; 9 | } 10 | 11 | // Paramater for any address that is not the zero address. 12 | persistent ghost address A { 13 | axiom A != 0; 14 | } 15 | 16 | // Ghost variable to hold the sum of delegated votes to parameterized address A. 17 | // To reason exhaustively on the value of of delegated voting power we proceed to compute the partial sum of delegated votes to parameter A for each possible address. 18 | // We call the partial sum of votes to parameter A up to an addrress a, to sum of delegated votes to parameter A for all addresses within the range [0..a[. 19 | // Formally, we write ∀ a:address, sumsOfVotesDelegatedToA[a] = Σ balanceOf(i), where the sum ranges over addresses i such that i < a and delegatee(i) = A, provided that the address zero holds no voting power and that it never performs transactions. 20 | // With this approach, we are able to write and check more abstract properties about the computation of the total delegated voting power using universal quantifiers. 21 | // From this follows the property such that, ∀ a:address, delegatee(a) = A ⇒ balanceOf(a) ≤ delegatedVotingPower(A). 22 | // In particular, we have the equality sumsOfVotesDelegatedToA[2^160] = delegatedVotingPower(A). 23 | // Finally, we reason by parametricity to observe since we have ∀ a:address, delegatee(a) = A ⇒ balanceOf(a) ≤ delegatedVotingPower(A). 24 | // We also have ∀ A:address, ∀ a:address, A ≠ 0 ∧ delegatee(a) = A ⇒ balanceOf(a) ≤ delegatedVotingPower(A), which is what we want to show. 25 | 26 | // sumOfvotes[x] = \sum_{i=0}^{x-1} balances[i] when delegatee[i] == A; 27 | ghost mapping(mathint => mathint) sumsOfVotesDelegatedToA { 28 | init_state axiom forall mathint account. sumsOfVotesDelegatedToA[account] == 0; 29 | } 30 | 31 | // Ghost copy of DelegationTokenStorage._delegatee for quantification. 32 | ghost mapping(address => address) ghostDelegatee { 33 | init_state axiom forall address account. ghostDelegatee[account] == 0; 34 | } 35 | 36 | // Slot is DelegationTokenStorage._delegatee. 37 | hook Sload address delegatee (slot 0x669be2f4ee1b0b5f3858e4135f31064efe8fa923b09bf21bf538f64f2c3e1100)[KEY address account] { 38 | require ghostDelegatee[account] == delegatee; 39 | } 40 | 41 | // Slot is DelegationTokenStorage._delegatee. 42 | hook Sstore (slot 0x669be2f4ee1b0b5f3858e4135f31064efe8fa923b09bf21bf538f64f2c3e1100)[KEY address account] address delegatee (address delegateeOld) { 43 | mathint changeOfAccountVotesForA; 44 | // Track delegation changes from the parameterized address. 45 | if (delegateeOld == A && delegatee != A) { 46 | require changeOfAccountVotesForA == - ghostBalances[account]; 47 | // Track delegation changes to the prameterized address. 48 | } else if (delegateeOld != A && delegatee == A) { 49 | require changeOfAccountVotesForA == ghostBalances[account]; 50 | } else { 51 | require changeOfAccountVotesForA == 0; 52 | } 53 | // Update partial sums for x > to_mathint(account) 54 | havoc sumsOfVotesDelegatedToA assuming 55 | forall mathint x. sumsOfVotesDelegatedToA@new[x] == 56 | sumsOfVotesDelegatedToA@old[x] + (to_mathint(account) < x ? changeOfAccountVotesForA : 0); 57 | // Update ghost copy of DelegationTokenStorage._delegatee. 58 | ghostDelegatee[account] = delegatee; 59 | } 60 | 61 | // Ghost variable to hold the sum of balances. 62 | // To reason exhaustively on the value of the sum of balances we proceed to compute the partial sum of balances for each possible address. 63 | // We call the partial sum of balances up to an addrress a, to sum of balances for all addresses within the range [0..a[. 64 | // Formally, we write ∀ a:address, sumOfBalances[a] = Σ balanceOf(i) where the sum ranges over addresses i < a, provided that the address zero holds no token and that it never performs transactions. 65 | // With this approach, we are able to write and check more abstract properties about the computation of the total supply of tokens using universal quantifiers. 66 | // From this follows the property such that, ∀ a:address, balanceOf(a) ≤ totalSupply(). 67 | // In particular we have the equality, sumOfBalances[2^160] = totalSupply() and we are able to to show that the sum of two different balances is lesser than or equal to the total supply. 68 | ghost mapping(mathint => mathint) sumOfBalances { 69 | init_state axiom forall mathint addr. sumOfBalances[addr] == 0; 70 | } 71 | 72 | // Ghost copy of ERC20Storage._balances for quantification. 73 | ghost mapping(address => uint256) ghostBalances { 74 | init_state axiom forall address account. ghostBalances[account] == 0; 75 | } 76 | 77 | // Slot is ERC20Storage._balances slot. 78 | hook Sload uint256 balance (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00)[KEY address account] { 79 | require ghostBalances[account] == balance; 80 | } 81 | 82 | // Slot is ERC20Storage._balances slot 83 | hook Sstore (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00)[KEY address account] uint256 newValue (uint256 oldValue) { 84 | // Update partial sum of balances, for x > to_mathint(account) 85 | // Track balance changes in balances. 86 | havoc sumOfBalances assuming 87 | forall mathint x. sumOfBalances@new[x] == 88 | sumOfBalances@old[x] + (to_mathint(account) < x ? newValue - oldValue : 0); 89 | // Update partial sums of votes delegated to the parameterized address, for x > to_mathint(account) 90 | // Track balance changes when the delegatee is the parameterized address. 91 | if (ghostDelegatee[account] == A) { 92 | havoc sumsOfVotesDelegatedToA assuming 93 | forall mathint x. sumsOfVotesDelegatedToA@new[x] == 94 | sumsOfVotesDelegatedToA@old[x] + (to_mathint(account) < x ? newValue - oldValue : 0); 95 | } 96 | // Update ghost copy of ERC20Storage._balances. 97 | ghostBalances[account] = newValue; 98 | } 99 | 100 | invariant sumOfBalancesStartsAtZero() 101 | sumOfBalances[0] == 0; 102 | 103 | invariant sumOfBalancesGrowsCorrectly() 104 | forall address addr. sumOfBalances[to_mathint(addr) + 1] == 105 | sumOfBalances[to_mathint(addr)] + ghostBalances[addr]; 106 | 107 | invariant sumOfBalancesMonotone() 108 | forall mathint i. forall mathint j. i <= j => sumOfBalances[i] <= sumOfBalances[j] 109 | { 110 | preserved { 111 | requireInvariant sumOfBalancesStartsAtZero(); 112 | requireInvariant sumOfBalancesGrowsCorrectly(); 113 | } 114 | } 115 | 116 | // Check that the sum of balances equals the total supply. 117 | invariant totalSupplyIsSumOfBalances() 118 | sumOfBalances[2^160] == to_mathint(totalSupply()) 119 | { 120 | preserved { 121 | requireInvariant sumOfBalancesStartsAtZero(); 122 | requireInvariant sumOfBalancesGrowsCorrectly(); 123 | requireInvariant sumOfBalancesMonotone(); 124 | } 125 | } 126 | 127 | invariant balancesLTEqTotalSupply() 128 | forall address a. ghostBalances[a] <= sumOfBalances[2^160] 129 | { 130 | preserved { 131 | requireInvariant sumOfBalancesStartsAtZero(); 132 | requireInvariant sumOfBalancesGrowsCorrectly(); 133 | requireInvariant sumOfBalancesMonotone(); 134 | requireInvariant totalSupplyIsSumOfBalances(); 135 | } 136 | } 137 | 138 | invariant twoBalancesLTEqTotalSupply() 139 | forall address a. forall address b. a != b => ghostBalances[a] + ghostBalances[b] <= sumOfBalances[2^160] 140 | { 141 | preserved { 142 | requireInvariant balancesLTEqTotalSupply(); 143 | requireInvariant sumOfBalancesStartsAtZero(); 144 | requireInvariant sumOfBalancesGrowsCorrectly(); 145 | requireInvariant sumOfBalancesMonotone(); 146 | requireInvariant totalSupplyIsSumOfBalances(); 147 | } 148 | } 149 | 150 | // Check that zero address's balance is equal to zero. 151 | invariant zeroAddressNoBalance() 152 | balanceOf(0) == 0; 153 | -------------------------------------------------------------------------------- /certora/specs/Delegation.spec: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | import "ERC20.spec"; 3 | 4 | methods { 5 | function delegatorFromSig(DelegationToken.Delegation, DelegationToken.Signature) external returns address envfree; 6 | function delegationNonce(address) external returns uint256 envfree; 7 | } 8 | 9 | // Ghost variable to hold the sum of voting power. 10 | // To reason exhaustively on the value of the sum of voting power we proceed to compute the partial sum of voting power for each possible address. 11 | // We call the partial sum of balances up to an addrress a, to sum of balances for all addresses within the range [0..a[. 12 | // Formally, we write ∀ a:address, sumOfVotes[a] = Σ delegatedVotingPower(i) where the sum ranges over addresses i < a, provided that the address zero holds no token and that it never performs transactions. 13 | // With this approach, we are able to write and check more abstract properties about the computation of the total voting power using universal quantifiers. 14 | // From this follows the property such that, ∀ a:address, delegatedVotingpower(a) ≤ total sum of votes. 15 | // In particular, we are able to to show that the sum voting powers of two different accounts is lesser than or equal to the total sum of votes. 16 | ghost mapping (mathint => mathint) sumOfVotes { 17 | init_state axiom forall mathint account. sumOfVotes[account] == 0; 18 | } 19 | 20 | // Ghost copy of DelegationTokenStorage._delegatedVotingPower. 21 | ghost mapping(address => uint256) ghostDelegatedVotingPower { 22 | init_state axiom forall address account. ghostDelegatedVotingPower[account] == 0; 23 | } 24 | 25 | hook Sload uint256 votingPower (slot 0x669be2f4ee1b0b5f3858e4135f31064efe8fa923b09bf21bf538f64f2c3e1101)[KEY address account] { 26 | require ghostDelegatedVotingPower[account] == votingPower; 27 | } 28 | 29 | // Slot is DelegationTokenStorage._delegatedVotingPower. 30 | hook Sstore (slot 0x669be2f4ee1b0b5f3858e4135f31064efe8fa923b09bf21bf538f64f2c3e1101)[KEY address account] uint256 votingPower (uint256 votingPowerOld) { 31 | // Update DelegationTokenStorage._delegatedVotingPower 32 | ghostDelegatedVotingPower[account] = votingPower; 33 | // Track balance changes in sum of votes. 34 | havoc sumOfVotes assuming 35 | forall mathint x. sumOfVotes@new[x] == 36 | sumOfVotes@old[x] + (to_mathint(account) < x ? votingPower - votingPowerOld : 0); 37 | } 38 | 39 | // Check that zero address has no voting power assuming that zero address can't make transactions. 40 | invariant zeroAddressNoVotingPower() 41 | delegatee(0x0) == 0x0 && delegatedVotingPower(0x0) == 0 42 | { preserved with (env e) { require e.msg.sender != 0; } } 43 | 44 | // Check that initially zero votes are delegated to parameterized address A. 45 | invariant sumOfVotesDelegatedToAStartsAtZero() 46 | sumsOfVotesDelegatedToA[0] == 0; 47 | 48 | invariant sumOfVotesDelegatedToAGrowsCorrectly() 49 | forall address account. sumsOfVotesDelegatedToA[to_mathint(account) + 1] == 50 | sumsOfVotesDelegatedToA[to_mathint(account)] + (ghostDelegatee[account] == A ? ghostBalances[account] : 0) ; 51 | 52 | invariant sumOfVotesDelegatedToAMonotone() 53 | forall mathint i. forall mathint j. i <= j => sumsOfVotesDelegatedToA[i] <= sumsOfVotesDelegatedToA[j] 54 | { 55 | preserved { 56 | requireInvariant sumOfVotesDelegatedToAStartsAtZero(); 57 | requireInvariant sumOfVotesDelegatedToAGrowsCorrectly(); 58 | } 59 | } 60 | 61 | invariant delegatedLTEqPartialSum() 62 | forall address account. ghostDelegatee[account] == A => 63 | ghostBalances[account] <= sumsOfVotesDelegatedToA[to_mathint(account)+1] 64 | { 65 | preserved { 66 | requireInvariant sumOfVotesDelegatedToAStartsAtZero(); 67 | requireInvariant sumOfVotesDelegatedToAGrowsCorrectly(); 68 | requireInvariant sumOfVotesDelegatedToAMonotone(); 69 | } 70 | } 71 | 72 | 73 | invariant sumOfVotesDelegatedToAIsDelegatedToA() 74 | sumsOfVotesDelegatedToA[2^160] == ghostDelegatedVotingPower[A] 75 | { 76 | preserved { 77 | requireInvariant zeroAddressNoVotingPower(); 78 | requireInvariant sumOfVotesDelegatedToAStartsAtZero(); 79 | requireInvariant sumOfVotesDelegatedToAGrowsCorrectly(); 80 | requireInvariant sumOfVotesDelegatedToAMonotone(); 81 | } 82 | } 83 | 84 | invariant delegatedLTEqDelegateeVP() 85 | forall address account. 86 | ghostDelegatee[account] == A => 87 | ghostBalances[account] <= ghostDelegatedVotingPower[A] 88 | { 89 | preserved with (env e){ 90 | requireInvariant zeroAddressNoVotingPower(); 91 | requireInvariant sumOfVotesDelegatedToAStartsAtZero(); 92 | requireInvariant sumOfVotesDelegatedToAGrowsCorrectly(); 93 | requireInvariant sumOfVotesDelegatedToAMonotone(); 94 | requireInvariant delegatedLTEqPartialSum(); 95 | requireInvariant sumOfVotesDelegatedToAIsDelegatedToA(); 96 | } 97 | } 98 | 99 | invariant sumOfVotesStartsAtZero() 100 | sumOfVotes[0] == 0; 101 | 102 | invariant sumOfVotesGrowsCorrectly() 103 | forall address addr. sumOfVotes[to_mathint(addr) + 1] == 104 | sumOfVotes[to_mathint(addr)] + ghostDelegatedVotingPower[addr]; 105 | 106 | invariant sumOfVotesMonotone() 107 | forall mathint i. forall mathint j. i <= j => sumOfVotes[i] <= sumOfVotes[j] 108 | { 109 | preserved { 110 | requireInvariant sumOfVotesStartsAtZero(); 111 | requireInvariant sumOfVotesGrowsCorrectly(); 112 | } 113 | } 114 | 115 | // Check that the voting power plus the virtual voting power of address zero is equal to the total supply of tokens. 116 | invariant totalSupplyIsSumOfVirtualVotingPower() 117 | sumOfVotes[2^160] + currentContract._zeroVirtualVotingPower == to_mathint(totalSupply()) 118 | { 119 | preserved { 120 | requireInvariant sumOfBalancesStartsAtZero(); 121 | requireInvariant sumOfBalancesGrowsCorrectly(); 122 | requireInvariant sumOfBalancesMonotone(); 123 | requireInvariant totalSupplyIsSumOfBalances(); 124 | requireInvariant zeroAddressNoVotingPower(); 125 | requireInvariant balancesLTEqTotalSupply(); 126 | 127 | } 128 | preserved MorphoTokenOptimismHarness.initialize(address _) with (env e) { 129 | // Safe require because the proxy contract should be initialized right after construction. 130 | require totalSupply() == 0; 131 | } 132 | preserved MorphoTokenEthereumHarness.initialize(address _, address _) with (env e) { 133 | // Safe requires because the proxy contract should be initialized right after construction. 134 | require totalSupply() == 0; 135 | require forall mathint account. sumOfVotes[account] == 0; 136 | } 137 | } 138 | 139 | invariant delegatedVotingPowerLTEqTotalVotingPower() 140 | forall address a. ghostDelegatedVotingPower[a] <= sumOfVotes[2^160] 141 | { 142 | preserved { 143 | requireInvariant sumOfVotesStartsAtZero(); 144 | requireInvariant sumOfVotesGrowsCorrectly(); 145 | requireInvariant sumOfVotesMonotone(); 146 | requireInvariant totalSupplyIsSumOfVirtualVotingPower(); 147 | } 148 | } 149 | 150 | invariant sumOfTwoDelegatedVPLTEqTotalVP() 151 | forall address a. forall address b. a != b => ghostDelegatedVotingPower[a] + ghostDelegatedVotingPower[b] <= sumOfVotes[2^160] 152 | { 153 | preserved { 154 | requireInvariant delegatedVotingPowerLTEqTotalVotingPower(); 155 | requireInvariant sumOfVotesStartsAtZero(); 156 | requireInvariant sumOfVotesGrowsCorrectly(); 157 | requireInvariant sumOfVotesMonotone(); 158 | requireInvariant totalSupplyIsSumOfVirtualVotingPower(); 159 | } 160 | } 161 | 162 | 163 | function isTotalSupplyGTEqSumOfVotingPower() returns bool { 164 | requireInvariant totalSupplyIsSumOfVirtualVotingPower(); 165 | return totalSupply() >= sumOfVotes[2^160]; 166 | } 167 | 168 | // Check that the total supply of tokens is greater than or equal to the sum of voting power. 169 | rule totalSupplyGTEqSumOfVotingPower { 170 | assert isTotalSupplyGTEqSumOfVotingPower(); 171 | } 172 | 173 | // Check that users can delegate their voting power. 174 | rule delegatingUpdatesVotingPower(env e, address newDelegatee) { 175 | requireInvariant zeroAddressNoVotingPower(); 176 | assert isTotalSupplyGTEqSumOfVotingPower(); 177 | 178 | address oldDelegatee = delegatee(e.msg.sender); 179 | 180 | mathint delegatedVotingPowerBefore = delegatedVotingPower(newDelegatee); 181 | 182 | delegate(e, newDelegatee); 183 | 184 | // Check that, if the delegatee changed and it's not the zero address then its voting power is greater than or equal to the delegator's balance, otherwise its voting power remains unchanged. 185 | if ((newDelegatee == 0) || (newDelegatee == oldDelegatee)) { 186 | assert delegatedVotingPower(newDelegatee) == delegatedVotingPowerBefore; 187 | } else { 188 | assert delegatedVotingPower(newDelegatee) == delegatedVotingPowerBefore + balanceOf(e.msg.sender); 189 | } 190 | } 191 | 192 | // Check that users can delegate their voting power. 193 | rule delegatingWithSigUpdatesVotingPower(env e, DelegationToken.Delegation delegation, DelegationToken.Signature signature) { 194 | requireInvariant zeroAddressNoVotingPower(); 195 | assert isTotalSupplyGTEqSumOfVotingPower(); 196 | 197 | address delegator = delegatorFromSig(delegation, signature); 198 | 199 | address oldDelegatee = delegatee(delegator); 200 | mathint delegationNonceBefore = delegationNonce(delegator); 201 | 202 | mathint delegatedVotingPowerBefore = delegatedVotingPower(delegation.delegatee); 203 | 204 | delegateWithSig(e, delegation, signature); 205 | 206 | // Check that the delegation's nonce matches the delegator's nonce. 207 | assert delegation.nonce == delegationNonceBefore; 208 | // Check that the current block timestamp is not later than the delegation's expiry timestamp. 209 | assert e.block.timestamp <= delegation.expiry; 210 | 211 | // Check that, if the delegatee changed and it's not the zero address then its voting power is greater than or equal to the delegator's balance, otherwise its voting power remains unchanged. 212 | if ((delegation.delegatee == 0) || (delegation.delegatee == oldDelegatee)) { 213 | assert delegatedVotingPower(delegation.delegatee) == delegatedVotingPowerBefore; 214 | } else { 215 | assert delegatedVotingPower(delegation.delegatee) == delegatedVotingPowerBefore + balanceOf(delegator); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /certora/specs/ERC20.spec: -------------------------------------------------------------------------------- 1 | // This is spec is taken from the Open Zeppelin repositories at https://github.com/OpenZeppelin/openzeppelin-contracts/blob/448efeea6640bbbc09373f03fbc9c88e280147ba/certora/specs/ERC20.spec, and patched to support the DelegationToken. 2 | // Note: the scope of this specification encompases only the implementation contract and not the interactions with the implementation contract and the proxy. 3 | 4 | import "ERC20Invariants.spec"; 5 | 6 | definition nonpayable(env e) returns bool = e.msg.value == 0; 7 | definition nonzerosender(env e) returns bool = e.msg.sender != 0; 8 | 9 | methods { 10 | function name() external returns (string) envfree; 11 | function symbol() external returns (string) envfree; 12 | function decimals() external returns (uint8) envfree; 13 | function allowance(address,address) external returns (uint256) envfree; 14 | function owner() external returns address envfree; 15 | function nonces(address) external returns (uint256) envfree; 16 | function DOMAIN_SEPARATOR() external returns (bytes32) envfree; 17 | } 18 | 19 | /* 20 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 21 | │ Invariant: totalSupply is the sum of all balances │ 22 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 23 | */ 24 | 25 | use invariant totalSupplyIsSumOfBalances; 26 | 27 | /* 28 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 29 | │ Invariant: balance of address(0) is 0 │ 30 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 31 | */ 32 | 33 | use invariant zeroAddressNoBalance; 34 | 35 | /* 36 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 37 | │ Rules: only the token holder (or a permit) can increase allowance. The spender can decrease it by using it │ 38 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 39 | */ 40 | rule onlyHolderOrSpenderCanChangeAllowance(env e, method f){ 41 | requireInvariant totalSupplyIsSumOfBalances(); 42 | 43 | calldataarg args; 44 | address holder; 45 | address spender; 46 | 47 | uint256 allowanceBefore = allowance(holder, spender); 48 | f(e, args); 49 | uint256 allowanceAfter = allowance(holder, spender); 50 | 51 | assert ( 52 | allowanceAfter > allowanceBefore 53 | ) => ( 54 | (f.selector == sig:approve(address,uint256).selector && e.msg.sender == holder) || 55 | (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) 56 | ); 57 | 58 | assert ( 59 | allowanceAfter < allowanceBefore 60 | ) => ( 61 | (f.selector == sig:transferFrom(address,address,uint256).selector && e.msg.sender == spender) || 62 | (f.selector == sig:approve(address,uint256).selector && e.msg.sender == holder ) || 63 | (f.selector == sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) 64 | ); 65 | } 66 | 67 | /* 68 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 69 | │ Rule: transfer behavior and side effects │ 70 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 71 | */ 72 | rule transfer(env e) { 73 | requireInvariant totalSupplyIsSumOfBalances(); 74 | requireInvariant twoBalancesLTEqTotalSupply(); 75 | require nonpayable(e); 76 | 77 | 78 | address holder = e.msg.sender; 79 | address recipient; 80 | address other; 81 | uint256 amount; 82 | 83 | // cache state 84 | uint256 holderBalanceBefore = balanceOf(holder); 85 | uint256 holderVotingPowerBefore = delegatedVotingPower(delegatee(holder)); 86 | uint256 recipientBalanceBefore = balanceOf(recipient); 87 | uint256 recipientVotingPowerBefore = delegatedVotingPower(delegatee(recipient)); 88 | uint256 otherBalanceBefore = balanceOf(other); 89 | 90 | // run transaction 91 | transfer@withrevert(e, recipient, amount); 92 | 93 | // check outcome 94 | if (lastReverted) { 95 | assert holder == 0 || recipient == 0 || amount > holderBalanceBefore || 96 | // Handle overflows in delegation, should not be possible. 97 | recipientVotingPowerBefore + amount > max_uint256 || holderVotingPowerBefore < amount ; 98 | } else { 99 | // balances of holder and recipient are updated 100 | assert to_mathint(balanceOf(holder)) == holderBalanceBefore - (holder == recipient ? 0 : amount); 101 | assert to_mathint(balanceOf(recipient)) == recipientBalanceBefore + (holder == recipient ? 0 : amount); 102 | 103 | // no other balance is modified 104 | assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); 105 | } 106 | } 107 | 108 | /* 109 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 110 | │ Rule: transferFrom behavior and side effects │ 111 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 112 | */ 113 | rule transferFrom(env e) { 114 | requireInvariant totalSupplyIsSumOfBalances(); 115 | requireInvariant twoBalancesLTEqTotalSupply(); 116 | require nonpayable(e); 117 | 118 | address spender = e.msg.sender; 119 | address holder; 120 | address recipient; 121 | address other; 122 | uint256 amount; 123 | 124 | // cache state 125 | uint256 allowanceBefore = allowance(holder, spender); 126 | uint256 holderBalanceBefore = balanceOf(holder); 127 | uint256 holderVotingPowerBefore = delegatedVotingPower(delegatee(holder)); 128 | uint256 recipientBalanceBefore = balanceOf(recipient); 129 | uint256 recipientVotingPowerBefore = delegatedVotingPower(delegatee(recipient)); 130 | uint256 otherBalanceBefore = balanceOf(other); 131 | 132 | // run transaction 133 | transferFrom@withrevert(e, holder, recipient, amount); 134 | 135 | // check outcome 136 | if (lastReverted) { 137 | assert holder == 0 || recipient == 0 || spender == 0 || amount > holderBalanceBefore || amount > allowanceBefore 138 | // Handle overflows in delegation, should not be possible. 139 | || amount + recipientVotingPowerBefore > max_uint256 || holderVotingPowerBefore < amount ; 140 | } else { 141 | // allowance is valid & updated 142 | assert allowanceBefore >= amount; 143 | assert to_mathint(allowance(holder, spender)) == (allowanceBefore == max_uint256 ? max_uint256 : allowanceBefore - amount); 144 | 145 | // balances of holder and recipient are updated 146 | assert to_mathint(balanceOf(holder)) == holderBalanceBefore - (holder == recipient ? 0 : amount); 147 | assert to_mathint(balanceOf(recipient)) == recipientBalanceBefore + (holder == recipient ? 0 : amount); 148 | 149 | // no other balance is modified 150 | assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); 151 | } 152 | } 153 | 154 | /* 155 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 156 | │ Rule: approve behavior and side effects │ 157 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 158 | */ 159 | rule approve(env e) { 160 | require nonpayable(e); 161 | 162 | address holder = e.msg.sender; 163 | address spender; 164 | address otherHolder; 165 | address otherSpender; 166 | uint256 amount; 167 | 168 | // cache state 169 | uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); 170 | 171 | // run transaction 172 | approve@withrevert(e, spender, amount); 173 | 174 | // check outcome 175 | if (lastReverted) { 176 | assert holder == 0 || spender == 0; 177 | } else { 178 | // allowance is updated 179 | assert allowance(holder, spender) == amount; 180 | 181 | // other allowances are untouched 182 | assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); 183 | } 184 | } 185 | 186 | /* 187 | ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ 188 | │ Rule: permit behavior and side effects │ 189 | └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ 190 | */ 191 | rule permit(env e) { 192 | require nonpayable(e); 193 | 194 | address holder; 195 | address spender; 196 | uint256 amount; 197 | uint256 deadline; 198 | uint8 v; 199 | bytes32 r; 200 | bytes32 s; 201 | 202 | address account1; 203 | address account2; 204 | address account3; 205 | 206 | // cache state 207 | uint256 nonceBefore = nonces(holder); 208 | uint256 otherNonceBefore = nonces(account1); 209 | uint256 otherAllowanceBefore = allowance(account2, account3); 210 | 211 | // sanity: nonce overflow, which possible in theory, is assumed to be impossible in practice 212 | require nonceBefore < max_uint256; 213 | require otherNonceBefore < max_uint256; 214 | 215 | // run transaction 216 | permit@withrevert(e, holder, spender, amount, deadline, v, r, s); 217 | 218 | // check outcome 219 | if (lastReverted) { 220 | // Without formally checking the signature, we can't verify exactly the revert causes 221 | assert true; 222 | } else { 223 | // allowance and nonce are updated 224 | assert allowance(holder, spender) == amount; 225 | assert to_mathint(nonces(holder)) == nonceBefore + 1; 226 | 227 | // deadline was respected 228 | assert deadline >= e.block.timestamp; 229 | 230 | // no other allowance or nonce is modified 231 | assert nonces(account1) != otherNonceBefore => account1 == holder; 232 | assert allowance(account2, account3) != otherAllowanceBefore => (account2 == holder && account3 == spender); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /test/MorphoTokenEthereumTest.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity ^0.8.0; 3 | 4 | import {BaseTest} from "./helpers/BaseTest.sol"; 5 | import {SigUtils, Delegation, Permit, Signature} from "./helpers/SigUtils.sol"; 6 | import {MorphoTokenEthereum} from "../src/MorphoTokenEthereum.sol"; 7 | import {DelegationToken} from "../src/DelegationToken.sol"; 8 | import { 9 | IERC20Errors 10 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol"; 11 | import { 12 | IERC20 13 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; 14 | import {OwnableUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; 15 | import { 16 | IERC1967 17 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol"; 18 | import { 19 | ERC1967Proxy 20 | } from "../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol"; 21 | 22 | contract MorphoTokenEthereumTest is BaseTest { 23 | function testInitilizeZeroAddress(address randomAddress) public { 24 | vm.assume(randomAddress != address(0)); 25 | 26 | address proxy = address(new ERC1967Proxy(address(tokenImplem), hex"")); 27 | 28 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableInvalidOwner.selector, address(0))); 29 | MorphoTokenEthereum(proxy).initialize(address(0), randomAddress); 30 | 31 | vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InvalidReceiver.selector, address(0))); 32 | MorphoTokenEthereum(proxy).initialize(randomAddress, address(0)); 33 | } 34 | 35 | function testUpgradeNotOwner(address updater) public { 36 | vm.assume(updater != address(0)); 37 | vm.assume(updater != MORPHO_DAO); 38 | 39 | address newImplem = address(new MorphoTokenEthereum()); 40 | 41 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, updater)); 42 | vm.prank(updater); 43 | newMorpho.upgradeToAndCall(newImplem, hex""); 44 | } 45 | 46 | function testUpgrade() public { 47 | assertEq(newMorpho.getImplementation(), address(tokenImplem)); 48 | 49 | address newImplem = address(new MorphoTokenEthereum()); 50 | 51 | vm.expectEmit(address(newMorpho)); 52 | emit IERC1967.Upgraded(newImplem); 53 | vm.prank(MORPHO_DAO); 54 | newMorpho.upgradeToAndCall(newImplem, hex""); 55 | 56 | assertEq(newMorpho.getImplementation(), newImplem); 57 | } 58 | 59 | function testOwnDelegation(address delegator, uint256 amount) public { 60 | vm.assume(delegator != address(0)); 61 | vm.assume(delegator != MORPHO_DAO); 62 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 63 | 64 | deal(address(newMorpho), delegator, amount); 65 | 66 | vm.prank(delegator); 67 | newMorpho.delegate(delegator); 68 | 69 | assertEq(newMorpho.delegatee(delegator), delegator); 70 | assertEq(newMorpho.delegatedVotingPower(delegator), amount); 71 | } 72 | 73 | function testDelegate(address delegator, address delegatee, uint256 amount) public { 74 | address[] memory addresses = new address[](2); 75 | addresses[0] = delegator; 76 | addresses[1] = delegatee; 77 | _validateAddresses(addresses); 78 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 79 | 80 | deal(address(newMorpho), delegator, amount); 81 | 82 | vm.expectEmit(address(newMorpho)); 83 | emit DelegationToken.DelegateeChanged(delegator, address(0), delegatee); 84 | vm.expectEmit(address(newMorpho)); 85 | emit DelegationToken.DelegatedVotingPowerChanged(delegatee, 0, amount); 86 | vm.prank(delegator); 87 | newMorpho.delegate(delegatee); 88 | 89 | assertEq(newMorpho.delegatee(delegator), delegatee); 90 | assertEq(newMorpho.delegatedVotingPower(delegator), 0); 91 | assertEq(newMorpho.delegatedVotingPower(delegatee), amount); 92 | } 93 | 94 | function testDelegateWithSigExpired(Delegation memory delegation, uint256 privateKey) public { 95 | delegation.expiry = bound(delegation.expiry, 0, type(uint32).max); 96 | privateKey = bound(privateKey, 1, type(uint32).max); 97 | address delegator = vm.addr(privateKey); 98 | 99 | address[] memory addresses = new address[](2); 100 | addresses[0] = delegator; 101 | addresses[1] = delegation.delegatee; 102 | _validateAddresses(addresses); 103 | 104 | delegation.nonce = 0; 105 | 106 | Signature memory sig; 107 | bytes32 digest = SigUtils.getDelegationTypedDataHash(delegation, address(newMorpho)); 108 | (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); 109 | 110 | vm.warp(delegation.expiry + 1); 111 | 112 | vm.expectRevert(DelegationToken.DelegatesExpiredSignature.selector); 113 | newMorpho.delegateWithSig(delegation, sig); 114 | } 115 | 116 | function testDelegateWithSigWrongNonce(Delegation memory delegation, uint256 privateKey, uint256 nounce) public { 117 | vm.assume(nounce != 0); 118 | privateKey = bound(privateKey, 1, type(uint32).max); 119 | address delegator = vm.addr(privateKey); 120 | 121 | address[] memory addresses = new address[](2); 122 | addresses[0] = delegator; 123 | addresses[1] = delegation.delegatee; 124 | _validateAddresses(addresses); 125 | 126 | delegation.expiry = bound(delegation.expiry, block.timestamp, type(uint32).max); 127 | delegation.nonce = nounce; 128 | 129 | Signature memory sig; 130 | bytes32 digest = SigUtils.getDelegationTypedDataHash(delegation, address(newMorpho)); 131 | (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); 132 | 133 | vm.expectRevert(DelegationToken.InvalidDelegationNonce.selector); 134 | newMorpho.delegateWithSig(delegation, sig); 135 | } 136 | 137 | function testDelegateWithSig(Delegation memory delegation, uint256 privateKey, uint256 amount) public { 138 | privateKey = bound(privateKey, 1, type(uint32).max); 139 | address delegator = vm.addr(privateKey); 140 | 141 | address[] memory addresses = new address[](2); 142 | addresses[0] = delegator; 143 | addresses[1] = delegation.delegatee; 144 | _validateAddresses(addresses); 145 | vm.assume(newMorpho.delegationNonce(delegator) == 0); 146 | 147 | delegation.expiry = bound(delegation.expiry, block.timestamp, type(uint32).max); 148 | delegation.nonce = 0; 149 | 150 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 151 | deal(address(newMorpho), delegator, amount); 152 | 153 | Signature memory sig; 154 | bytes32 digest = SigUtils.getDelegationTypedDataHash(delegation, address(newMorpho)); 155 | (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); 156 | 157 | vm.expectEmit(address(newMorpho)); 158 | emit DelegationToken.DelegateeChanged(delegator, address(0), delegation.delegatee); 159 | vm.expectEmit(address(newMorpho)); 160 | emit DelegationToken.DelegatedVotingPowerChanged(delegation.delegatee, 0, amount); 161 | newMorpho.delegateWithSig(delegation, sig); 162 | 163 | assertEq(newMorpho.delegatee(delegator), delegation.delegatee); 164 | assertEq(newMorpho.delegatedVotingPower(delegator), 0); 165 | assertEq(newMorpho.delegatedVotingPower(delegation.delegatee), amount); 166 | assertEq(newMorpho.delegationNonce(delegator), 1); 167 | assertEq(newMorpho.nonces(delegator), 0); 168 | } 169 | 170 | function testPermitNotIncrementingNonce(Permit memory permit, uint256 privateKey) public { 171 | privateKey = bound(privateKey, 1, type(uint32).max); 172 | permit.owner = vm.addr(privateKey); 173 | 174 | address[] memory addresses = new address[](2); 175 | addresses[0] = permit.owner; 176 | addresses[1] = permit.spender; 177 | _validateAddresses(addresses); 178 | vm.assume(newMorpho.delegationNonce(permit.owner) == 0); 179 | vm.assume(newMorpho.nonces(permit.owner) == 0); 180 | 181 | permit.deadline = bound(permit.deadline, block.timestamp, type(uint256).max); 182 | permit.nonce = 0; 183 | 184 | permit.value = bound(permit.value, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 185 | 186 | Signature memory sig; 187 | bytes32 digest = SigUtils.getPermitTypedDataHash(permit, address(newMorpho)); 188 | (sig.v, sig.r, sig.s) = vm.sign(privateKey, digest); 189 | 190 | newMorpho.permit(permit.owner, permit.spender, permit.value, permit.deadline, sig.v, sig.r, sig.s); 191 | 192 | assertEq(newMorpho.delegationNonce(permit.owner), 0); 193 | assertEq(newMorpho.nonces(permit.owner), 1); 194 | } 195 | 196 | function testMultipleDelegations( 197 | address delegator1, 198 | address delegator2, 199 | address delegatee, 200 | uint256 amount1, 201 | uint256 amount2 202 | ) public { 203 | address[] memory addresses = new address[](3); 204 | addresses[0] = delegator1; 205 | addresses[1] = delegator2; 206 | addresses[2] = delegatee; 207 | _validateAddresses(addresses); 208 | amount1 = bound(amount1, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 209 | amount2 = bound(amount2, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 210 | 211 | deal(address(newMorpho), delegator1, amount1); 212 | deal(address(newMorpho), delegator2, amount2); 213 | 214 | vm.expectEmit(address(newMorpho)); 215 | emit DelegationToken.DelegateeChanged(delegator1, address(0), delegatee); 216 | vm.expectEmit(address(newMorpho)); 217 | emit DelegationToken.DelegatedVotingPowerChanged(delegatee, 0, amount1); 218 | vm.prank(delegator1); 219 | newMorpho.delegate(delegatee); 220 | 221 | vm.expectEmit(address(newMorpho)); 222 | emit DelegationToken.DelegateeChanged(delegator2, address(0), delegatee); 223 | vm.expectEmit(address(newMorpho)); 224 | emit DelegationToken.DelegatedVotingPowerChanged(delegatee, amount1, amount1 + amount2); 225 | vm.prank(delegator2); 226 | newMorpho.delegate(delegatee); 227 | 228 | assertEq(newMorpho.delegatedVotingPower(delegatee), amount1 + amount2); 229 | } 230 | 231 | function testTransferVotingPower( 232 | address delegator1, 233 | address delegator2, 234 | address delegatee1, 235 | address delegatee2, 236 | uint256 initialAmount, 237 | uint256 transferredAmount 238 | ) public { 239 | address[] memory addresses = new address[](4); 240 | addresses[0] = delegator1; 241 | addresses[1] = delegator2; 242 | addresses[2] = delegatee1; 243 | addresses[3] = delegatee2; 244 | _validateAddresses(addresses); 245 | initialAmount = bound(initialAmount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 246 | transferredAmount = bound(transferredAmount, MIN_TEST_AMOUNT, initialAmount); 247 | 248 | deal(address(newMorpho), delegator1, initialAmount); 249 | 250 | vm.prank(delegator2); 251 | newMorpho.delegate(delegatee2); 252 | 253 | vm.startPrank(delegator1); 254 | newMorpho.delegate(delegatee1); 255 | 256 | vm.expectEmit(address(newMorpho)); 257 | emit DelegationToken.DelegatedVotingPowerChanged(delegatee1, initialAmount, initialAmount - transferredAmount); 258 | vm.expectEmit(address(newMorpho)); 259 | emit DelegationToken.DelegatedVotingPowerChanged(delegatee2, 0, transferredAmount); 260 | newMorpho.transfer(delegator2, transferredAmount); 261 | vm.stopPrank(); 262 | 263 | assertEq(newMorpho.delegatedVotingPower(delegatee1), initialAmount - transferredAmount); 264 | assertEq(newMorpho.delegatedVotingPower(delegatee2), transferredAmount); 265 | } 266 | 267 | function testMint(address to, uint256 amount) public { 268 | vm.assume(to != address(0)); 269 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 270 | 271 | uint256 initialTotalSupply = newMorpho.totalSupply(); 272 | uint256 initialAmount = newMorpho.balanceOf(to); 273 | 274 | vm.expectEmit(address(newMorpho)); 275 | emit IERC20.Transfer(address(0), to, amount); 276 | vm.prank(MORPHO_DAO); 277 | newMorpho.mint(to, amount); 278 | 279 | assertEq(newMorpho.totalSupply(), initialTotalSupply + amount); 280 | assertEq(newMorpho.balanceOf(to), initialAmount + amount); 281 | } 282 | 283 | function testMintOverflow(address to, uint256 amount) public { 284 | vm.assume(to != address(0)); 285 | amount = bound(amount, type(uint256).max - newMorpho.totalSupply() + 1, type(uint256).max); 286 | 287 | vm.prank(MORPHO_DAO); 288 | vm.expectRevert(); 289 | newMorpho.mint(to, amount); 290 | } 291 | 292 | function testMintAccess(address account, address to, uint256 amount) public { 293 | vm.assume(to != address(0)); 294 | vm.assume(account != MORPHO_DAO); 295 | amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 296 | 297 | vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, account)); 298 | vm.prank(account); 299 | newMorpho.mint(to, amount); 300 | } 301 | 302 | function testBurn(address from, uint256 amountMinted, uint256 amountBurned) public { 303 | vm.assume(from != address(0)); 304 | amountMinted = bound(amountMinted, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT); 305 | amountBurned = bound(amountBurned, MIN_TEST_AMOUNT, amountMinted); 306 | 307 | uint256 initialTotalSupply = newMorpho.totalSupply(); 308 | uint256 initialAmount = newMorpho.balanceOf(from); 309 | 310 | vm.prank(MORPHO_DAO); 311 | newMorpho.mint(from, amountMinted); 312 | 313 | vm.expectEmit(address(newMorpho)); 314 | emit IERC20.Transfer(from, address(0), amountBurned); 315 | vm.prank(from); 316 | newMorpho.burn(amountBurned); 317 | 318 | assertEq(newMorpho.totalSupply(), initialTotalSupply + amountMinted - amountBurned); 319 | assertEq(newMorpho.balanceOf(from), initialAmount + amountMinted - amountBurned); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is available under your choice of the GNU General Public 2 | License, version 2 or later, or the Business Source License, as set 3 | forth below. 4 | 5 | GNU GENERAL PUBLIC LICENSE 6 | Version 2, June 1991 7 | 8 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 9 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 10 | Everyone is permitted to copy and distribute verbatim copies 11 | of this license document, but changing it is not allowed. 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | License is intended to guarantee your freedom to share and change free 18 | software--to make sure the software is free for all its users. This 19 | General Public License applies to most of the Free Software 20 | Foundation's software and to any other program whose authors commit to 21 | using it. (Some other Free Software Foundation software is covered by 22 | the GNU Lesser General Public License instead.) You can apply it to 23 | your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | this service if you wish), that you receive source code or can get it 29 | if you want it, that you can change the software or use pieces of it 30 | in new free programs; and that you know you can do these things. 31 | 32 | To protect your rights, we need to make restrictions that forbid 33 | anyone to deny you these rights or to ask you to surrender the rights. 34 | These restrictions translate to certain responsibilities for you if you 35 | distribute copies of the software, or if you modify it. 36 | 37 | For example, if you distribute copies of such a program, whether 38 | gratis or for a fee, you must give the recipients all the rights that 39 | you have. You must make sure that they, too, receive or can get the 40 | source code. And you must show them these terms so they know their 41 | rights. 42 | 43 | We protect your rights with two steps: (1) copyright the software, and 44 | (2) offer you this license which gives you legal permission to copy, 45 | distribute and/or modify the software. 46 | 47 | Also, for each author's protection and ours, we want to make certain 48 | that everyone understands that there is no warranty for this free 49 | software. If the software is modified by someone else and passed on, we 50 | want its recipients to know that what they have is not the original, so 51 | that any problems introduced by others will not reflect on the original 52 | authors' reputations. 53 | 54 | Finally, any free program is threatened constantly by software 55 | patents. We wish to avoid the danger that redistributors of a free 56 | program will individually obtain patent licenses, in effect making the 57 | program proprietary. To prevent this, we have made it clear that any 58 | patent must be licensed for everyone's free use or not licensed at all. 59 | 60 | The precise terms and conditions for copying, distribution and 61 | modification follow. 62 | 63 | GNU GENERAL PUBLIC LICENSE 64 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 65 | 66 | 0. This License applies to any program or other work which contains 67 | a notice placed by the copyright holder saying it may be distributed 68 | under the terms of this General Public License. The "Program", below, 69 | refers to any such program or work, and a "work based on the Program" 70 | means either the Program or any derivative work under copyright law: 71 | that is to say, a work containing the Program or a portion of it, 72 | either verbatim or with modifications and/or translated into another 73 | language. (Hereinafter, translation is included without limitation in 74 | the term "modification".) Each licensee is addressed as "you". 75 | 76 | Activities other than copying, distribution and modification are not 77 | covered by this License; they are outside its scope. The act of 78 | running the Program is not restricted, and the output from the Program 79 | is covered only if its contents constitute a work based on the 80 | Program (independent of having been made by running the Program). 81 | Whether that is true depends on what the Program does. 82 | 83 | 1. You may copy and distribute verbatim copies of the Program's 84 | source code as you receive it, in any medium, provided that you 85 | conspicuously and appropriately publish on each copy an appropriate 86 | copyright notice and disclaimer of warranty; keep intact all the 87 | notices that refer to this License and to the absence of any warranty; 88 | and give any other recipients of the Program a copy of this License 89 | along with the Program. 90 | 91 | You may charge a fee for the physical act of transferring a copy, and 92 | you may at your option offer warranty protection in exchange for a fee. 93 | 94 | 2. You may modify your copy or copies of the Program or any portion 95 | of it, thus forming a work based on the Program, and copy and 96 | distribute such modifications or work under the terms of Section 1 97 | above, provided that you also meet all of these conditions: 98 | 99 | a) You must cause the modified files to carry prominent notices 100 | stating that you changed the files and the date of any change. 101 | 102 | b) You must cause any work that you distribute or publish, that in 103 | whole or in part contains or is derived from the Program or any 104 | part thereof, to be licensed as a whole at no charge to all third 105 | parties under the terms of this License. 106 | 107 | c) If the modified program normally reads commands interactively 108 | when run, you must cause it, when started running for such 109 | interactive use in the most ordinary way, to print or display an 110 | announcement including an appropriate copyright notice and a 111 | notice that there is no warranty (or else, saying that you provide 112 | a warranty) and that users may redistribute the program under 113 | these conditions, and telling the user how to view a copy of this 114 | License. (Exception: if the Program itself is interactive but 115 | does not normally print such an announcement, your work based on 116 | the Program is not required to print an announcement.) 117 | 118 | These requirements apply to the modified work as a whole. If 119 | identifiable sections of that work are not derived from the Program, 120 | and can be reasonably considered independent and separate works in 121 | themselves, then this License, and its terms, do not apply to those 122 | sections when you distribute them as separate works. But when you 123 | distribute the same sections as part of a whole which is a work based 124 | on the Program, the distribution of the whole must be on the terms of 125 | this License, whose permissions for other licensees extend to the 126 | entire whole, and thus to each and every part regardless of who wrote it. 127 | 128 | Thus, it is not the intent of this section to claim rights or contest 129 | your rights to work written entirely by you; rather, the intent is to 130 | exercise the right to control the distribution of derivative or 131 | collective works based on the Program. 132 | 133 | In addition, mere aggregation of another work not based on the Program 134 | with the Program (or with a work based on the Program) on a volume of 135 | a storage or distribution medium does not bring the other work under 136 | the scope of this License. 137 | 138 | 3. You may copy and distribute the Program (or a work based on it, 139 | under Section 2) in object code or executable form under the terms of 140 | Sections 1 and 2 above provided that you also do one of the following: 141 | 142 | a) Accompany it with the complete corresponding machine-readable 143 | source code, which must be distributed under the terms of Sections 144 | 1 and 2 above on a medium customarily used for software interchange; or, 145 | 146 | b) Accompany it with a written offer, valid for at least three 147 | years, to give any third party, for a charge no more than your 148 | cost of physically performing source distribution, a complete 149 | machine-readable copy of the corresponding source code, to be 150 | distributed under the terms of Sections 1 and 2 above on a medium 151 | customarily used for software interchange; or, 152 | 153 | c) Accompany it with the information you received as to the offer 154 | to distribute corresponding source code. (This alternative is 155 | allowed only for noncommercial distribution and only if you 156 | received the program in object code or executable form with such 157 | an offer, in accord with Subsection b above.) 158 | 159 | The source code for a work means the preferred form of the work for 160 | making modifications to it. For an executable work, complete source 161 | code means all the source code for all modules it contains, plus any 162 | associated interface definition files, plus the scripts used to 163 | control compilation and installation of the executable. However, as a 164 | special exception, the source code distributed need not include 165 | anything that is normally distributed (in either source or binary 166 | form) with the major components (compiler, kernel, and so on) of the 167 | operating system on which the executable runs, unless that component 168 | itself accompanies the executable. 169 | 170 | If distribution of executable or object code is made by offering 171 | access to copy from a designated place, then offering equivalent 172 | access to copy the source code from the same place counts as 173 | distribution of the source code, even though third parties are not 174 | compelled to copy the source along with the object code. 175 | 176 | 4. You may not copy, modify, sublicense, or distribute the Program 177 | except as expressly provided under this License. Any attempt 178 | otherwise to copy, modify, sublicense or distribute the Program is 179 | void, and will automatically terminate your rights under this License. 180 | However, parties who have received copies, or rights, from you under 181 | this License will not have their licenses terminated so long as such 182 | parties remain in full compliance. 183 | 184 | 5. You are not required to accept this License, since you have not 185 | signed it. However, nothing else grants you permission to modify or 186 | distribute the Program or its derivative works. These actions are 187 | prohibited by law if you do not accept this License. Therefore, by 188 | modifying or distributing the Program (or any work based on the 189 | Program), you indicate your acceptance of this License to do so, and 190 | all its terms and conditions for copying, distributing or modifying 191 | the Program or works based on it. 192 | 193 | 6. Each time you redistribute the Program (or any work based on the 194 | Program), the recipient automatically receives a license from the 195 | original licensor to copy, distribute or modify the Program subject to 196 | these terms and conditions. You may not impose any further 197 | restrictions on the recipients' exercise of the rights granted herein. 198 | You are not responsible for enforcing compliance by third parties to 199 | this License. 200 | 201 | 7. If, as a consequence of a court judgment or allegation of patent 202 | infringement or for any other reason (not limited to patent issues), 203 | conditions are imposed on you (whether by court order, agreement or 204 | otherwise) that contradict the conditions of this License, they do not 205 | excuse you from the conditions of this License. If you cannot 206 | distribute so as to satisfy simultaneously your obligations under this 207 | License and any other pertinent obligations, then as a consequence you 208 | may not distribute the Program at all. For example, if a patent 209 | license would not permit royalty-free redistribution of the Program by 210 | all those who receive copies directly or indirectly through you, then 211 | the only way you could satisfy both it and this License would be to 212 | refrain entirely from distribution of the Program. 213 | 214 | If any portion of this section is held invalid or unenforceable under 215 | any particular circumstance, the balance of the section is intended to 216 | apply and the section as a whole is intended to apply in other 217 | circumstances. 218 | 219 | It is not the purpose of this section to induce you to infringe any 220 | patents or other property right claims or to contest validity of any 221 | such claims; this section has the sole purpose of protecting the 222 | integrity of the free software distribution system, which is 223 | implemented by public license practices. Many people have made 224 | generous contributions to the wide range of software distributed 225 | through that system in reliance on consistent application of that 226 | system; it is up to the author/donor to decide if he or she is willing 227 | to distribute software through any other system and a licensee cannot 228 | impose that choice. 229 | 230 | This section is intended to make thoroughly clear what is believed to 231 | be a consequence of the rest of this License. 232 | 233 | 8. If the distribution and/or use of the Program is restricted in 234 | certain countries either by patents or by copyrighted interfaces, the 235 | original copyright holder who places the Program under this License 236 | may add an explicit geographical distribution limitation excluding 237 | those countries, so that distribution is permitted only in or among 238 | countries not thus excluded. In such case, this License incorporates 239 | the limitation as if written in the body of this License. 240 | 241 | 9. The Free Software Foundation may publish revised and/or new versions 242 | of the General Public License from time to time. Such new versions will 243 | be similar in spirit to the present version, but may differ in detail to 244 | address new problems or concerns. 245 | 246 | Each version is given a distinguishing version number. If the Program 247 | specifies a version number of this License which applies to it and "any 248 | later version", you have the option of following the terms and conditions 249 | either of that version or of any later version published by the Free 250 | Software Foundation. If the Program does not specify a version number of 251 | this License, you may choose any version ever published by the Free Software 252 | Foundation. 253 | 254 | 10. If you wish to incorporate parts of the Program into other free 255 | programs whose distribution conditions are different, write to the author 256 | to ask for permission. For software which is copyrighted by the Free 257 | Software Foundation, write to the Free Software Foundation; we sometimes 258 | make exceptions for this. Our decision will be guided by the two goals 259 | of preserving the free status of all derivatives of our free software and 260 | of promoting the sharing and reuse of software generally. 261 | 262 | NO WARRANTY 263 | 264 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 265 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 266 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 267 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 268 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 269 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 270 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 271 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 272 | REPAIR OR CORRECTION. 273 | 274 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 275 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 276 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 277 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 278 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 279 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 280 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 281 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 282 | POSSIBILITY OF SUCH DAMAGES. 283 | 284 | END OF TERMS AND CONDITIONS --------------------------------------------------------------------------------