diff --git a/.env.example b/.env.example index 3d7b8724..0f8f543e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1 @@ -API_KEY= -INFURA_KEY= -POLYGONSCAN_API_KEY= -TATARA_TOKEN= -KATANA_TOKEN= \ No newline at end of file +ETHERSCAN_API_KEY= \ No newline at end of file diff --git a/.github/workflows/certora.yml b/.github/workflows/certora.yml new file mode 100644 index 00000000..947e3753 --- /dev/null +++ b/.github/workflows/certora.yml @@ -0,0 +1,68 @@ +name: Certora - Verification + +on: + pull_request: + branches: + - staging + +env: + FOUNDRY_PROFILE: ci + CONFIGS: | + certora/confs/GenericVaultBridgeToken.conf + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule netCollectedYieldAccounted --rule netCollectedYieldLimited + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule minimumReservePercentageLimit --rule reserveBacked + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule vaultBridgeTokenSolvency --rule vaultBridgeTokenSolvency_simple + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule assetsMoreThanSupply --rule noSupplyIfNoAssets + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule zeroAllowanceOnAssets --rule zeroAllowanceOnShares + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_allowedChanges.spec + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_integrity.spec + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GVBTBalances.spec + certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/tokenMockBalances.spec + certora/confs/GenericNativeConverter.conf + certora/confs/base/MigrationManager.conf --rule onMsgReceived_doesntAlwaysRevert +jobs: + check: + runs-on: ubuntu-latest + permissions: + contents: read + statuses: write + pull-requests: write + id-token: write + steps: + - name: checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: install foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: install bun + uses: oven-sh/setup-bun@v2 + + - name: bun install + run: | + forge soldeer install + bun install || true + + - name: setup bridge repo + run: | + git clone https://github.com/0xPolygonHermez/zkevm-contracts.git + cd zkevm-contracts/ + git checkout v10.1.0-rc.1 + npm install + + - name: Certora munge + run: ./certora/scripts/munge.sh + + - name: run configs + uses: Certora/certora-run-action@v2 + with: + cli-release: beta + configurations: ${{ env.CONFIGS }} + solc-versions: 0.8.28 0.8.29 + solc-remove-version-prefix: "0." + job-name: "Verified Rules" + certora-key: ${{ secrets.CERTORAKEY }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0c9b86c..e4bb1c66 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,14 +29,13 @@ jobs: run: | forge --version - - name: Run Forge fmt - run: | - forge fmt --check - id: fmt + # - name: Run Forge fmt + # run: | + # forge fmt --check + # id: fmt - name: Run Forge build run: | - forge --version forge soldeer install forge build --sizes id: build @@ -54,6 +53,4 @@ jobs: run: | npm install forge test --no-match-contract "Generic" -vvv - env: - TATARA_TOKEN: ${{ secrets.TATARA_TOKEN }} id: test diff --git a/.gitignore b/.gitignore index 0c6514c7..510dbe2e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,9 @@ docs/ coverage lcov.info -.DS_Store \ No newline at end of file +.DS_Store + +# certora +.certora_internal/ +zkevm-contracts/ +emv-*/* \ No newline at end of file diff --git a/LICENSE-OPEN-LICENSE b/LICENSE-OPEN-LICENSE deleted file mode 100644 index 1b36bc2e..00000000 --- a/LICENSE-OPEN-LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution - -OPEN LICENSE (SIMILAR TO MIT LICENSE) - -Copyright (c) 2025 PT Services DMCC - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the “Software”), to deal in the Software without restriction -(except as set forth below), including without limitation the rights to use, copy, modify, merge, -publish, distribute, and/or sublicense, the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following conditions: - -The Software may only be used under this license in connection with cryptoasset deposits into -the Agglayer’s unified LxLy bridge (contract address: -0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe). - -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. - -TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE IS PROVIDED -“AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSE-SOURCE-AVAILABLE-LICENSE b/LICENSE-SOURCE-AVAILABLE similarity index 98% rename from LICENSE-SOURCE-AVAILABLE-LICENSE rename to LICENSE-SOURCE-AVAILABLE index 96060cf6..f2b677e2 100644 --- a/LICENSE-SOURCE-AVAILABLE-LICENSE +++ b/LICENSE-SOURCE-AVAILABLE @@ -3,7 +3,7 @@ SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available SOURCE AVAILABLE LICENSE Licensor: PT Services DMCC -Licensed Work: VaultBridge +Licensed Work: Vault Bridge The Licensed Work is Copyright (c) 2025 PT Services DMCC Permitted Use: The Licensor hereby grants you the right to copy, modify, create derivative diff --git a/README.md b/README.md index 3e8ea3ee..93a85222 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,139 @@ +
+ +# Vault Bridge + +**[⛓️ Deployments](#deployments)** +**   [📙 Documentation](#documentation)** +**   [🧭 Website](https://www.agglayer.dev/agglayer-vaultbridge)** +**   [🐈‍⬛ GitHub](https://github.com/agglayer/vault-bridge/)** +**   [🦙 DefiLllama](https://defillama.com/protocol/vault-bridge)** + +
+ +
+ > [!IMPORTANT] > You are viewing a development version of the codebase. -# Vault Bridge +## Contents -Vault Bridge Token is the core of the Vault Bridge protocol. Built from the ground up to be reusable, it offers full Vault Bridge functionality out of the box, allowing you to create vbTokens in just a few lines of code. +- [Contents](#contents) +- [Overview](#overview) + - [TL;DR](#tldr) + - [Vault Bridge Token](#vault-bridge-token) + - [Migration Manager](#migration-manager) + - [Custom Token](#custom-token) + - [Native Converter](#native-converter) +- [Get Started](#get-started) +- [Documentation](#documentation) +- [Deployments](#deployments) +- [Usage](#usage) +- [License](#license) + +
## Overview -The Vault Bridge protocol is comprised of: +Vault Bridge enables chains and apps to generate native yield on TVL by putting bridged assets to work. + +The protocol is comprised of: -- Layer X (the main network) - - [Vault Bridge Token](#vault-bridge-token-) - - [Migration Manager (singleton)](#migration-manager-singleton-) -- Layer Y (other networks) - - [Custom Token](#custom-token-) - - [Native Converter](#native-converter-) +- One Primary Chain + - [Vault Bridge Token](#vault-bridge-token) + - [Migration Manager](#migration-manager) +- Many Secondary Chains + - [Custom Token](#custom-token) + - [Native Converter](#native-converter) -### Vault Bridge Token [↗](src/VaultBridgeToken.sol) +### TL;DR -A Vault Bridge Token is an +Select assets are bridged from Primary Chain to Secondary Chain. These assets are deposited into Vault Bridge Token contract on Primary Chain, which mints and bridges vbToken to Secondary Chain. Deposited assets are used to generate yield on Primary Chain, while bridged vbTokens are used in DeFi on Secondary Chain. Generated yield gets distributed to chains and apps participating in the revenue sharing program. + +Native Converter contract can be deployed on Secondary Chain to enable acquisition of vbToken on Secondary Chain without having to bridge from Primary Chain. Accumulated backing in Native Converter on Secondary Chain gets migrated to Primary Chain and deposited into Vault Bridge Token contract. + +### Vault Bridge Token + +A Vault Bridge Token is: - [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token - [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault -- [LxLy Bridge](https://github.com/0xPolygonHermez/zkevm-contracts) extension +- [Agglayer Bridge](https://github.com/agglayer/agglayer-contracts) extension -enabling bridging of select assets, such as WBTC, WETH, USDT, USDC, and USDS, while producing yield. +Assets in high demand with available yield strategies, such as WETH and USDC, can get their versions of vbTokens. The underlying asset is deposited into Vault Bridge Token contract, and vbToken is minted in a 1:1 ratio. The same can be withdrawn by burning vbToken. Vault Bridge Token contract doubles a pseudo bridge, so vbToken can be minted and bridged, or claimed and redeemed, in a single call. Deposited underlying assets are put into an external, ERC-4626 compatible vault ("yield vault") where they generate yield. Yield is distributed to chains and apps that participate in the revenue sharing program. Vault Bridge Token contracts also includes functionality that enables minting of vbToken directly on Secondary Chain via Native Converter, with backing migration to Primary Chain via Migration Manager. -### Migration Manager (singleton) [↗](src/MigrationManager.sol) +### Migration Manager -The Migration Manager is a +The Migration Manager is: -- [Vault Bridge Token](#vault-bridge-token-) dependency +- [Vault Bridge Token](#vault-bridge-token) dependency -handling migration of backing from Native Converters. +vbTokens can be minted directly on Secondary Chain. In order for an underlying asset that backs vbToken minted on Secondary Chain to be deposited in Vault Bridge Token contract on Primary Chain, backing is migrated to Primary Chain via Native Converter and Migration Manager. Migration Manager completes migrations by interacting with Vault Bridge Token contract. All vbTokens share the same Migration Manager contract. -### Custom Token [↗](src/CustomToken.sol) +### Custom Token -A Custom Token is an +A Custom Token is: - [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token -an upgrade for [LxLy Bridge](https://github.com/0xPolygonHermez/zkevm-contracts)'s generic wrapped token. +Bridged vbToken can be upgraded to Custom Token on Secondary Chain. This enables custom behavior, such as bridged vbETH to integrate WETH9 interface, replacing WETH on Secondary Chain. + +### Native Converter + +A Native Converter is: -### Native Converter [↗](src/NativeConverter.sol) +- [Vault Bridge Token](#vault-bridge-token) extension +- [Agglayer Bridge](https://github.com/agglayer/agglayer-contracts) extension -A Native Converter is a +Native Converter can be deployed on Secondary Chain to enable minting of vbToken directly on Secondary Chain by converting the bridged underlying asset, in a 1:1 ratio. The same can be deconverted to by burning bridged vbToken. Accumulated backing in Native Converter on Secondary Chain can be migrated to Primary Chain to be deposited into Vault Bridge Token contract via Migration Manger. For this reason, liqudity for deconverting to the bridged underlying token on Secondary Chain is guaranteed only up to a certain percentage. Native Converter doubles a bridge extension, so vbToken can be deconverted and bridged in a single call. -- pseudo [ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) vault -- [LxLy Bridge](https://github.com/0xPolygonHermez/zkevm-contracts) extension +## Get Started -allowing conversion to, and deconversion of, Custom Token, as well as migration of backing to Vault Bridge Token. +Getting started should be easy as Vault Bridge Token contracts follow the ERC-4626 interface. Variants of the standard ERC-4626 functions include `depositAndBridge` and `claimAndRedeem`. Please see [Documentation](#documentation) for more information. + +If your chain is part of Agglayer, you can start using the official vbTokens immediately. Please note that you will get vbToken when bridging, not the underlying token, therefore activity should be incentivized in vbToken. You must participate in the revenue sharing program in order to receive yield. [Contact our team](https://info.polygon.technology/vaultbridge-intake-form) if interested in revenue sharing. + +If your chain is not part of Agglayer, you can start using the official vbTokens immediately. Please note that you will need to use a third-party bridge to bridge vbTokens to your chain, and Native Converter functionality will not be supported. You must participate in the revenue sharing program in order to receive yield. [Contact our team](https://info.polygon.technology/vaultbridge-intake-form) if interested in revenue sharing. + +Full support for non-Agglayer chains, third-party bridges, as well as non-EVM chains is coming soon. [Contact our team](https://info.polygon.technology/vaultbridge-intake-form) to register interest. ## Documentation -Please see NatSpec documentation inside of the files. +- [General Documentation](https://docs.agglayer.dev/) +- [Technical Reference](https://agglayer.github.io/vault-bridge/) +- [Source Code](https://github.com/agglayer/vault-bridge/tree/main/src): In addition to General Documentation and Technical Reference, the Source Code is 100% documented and you are encouraged to take a look. + - Pay attention to the following bookmarks: `@note CAUTION!`, `@note IMPORTANT:`, `@note (ATTENTION)`. + +## Deployments -Please see `@note` documentation for important information. +See [`broadcast/README.md`](https://github.com/agglayer/vault-bridge/blob/main/broadcast/README.md). ## Usage -#### Prerequisite +Clone: ``` -foundryup +git clone git@github.com:agglayer/vault-bridge.git ``` -#### Install +Install: ``` forge soldeer install & npm install ``` -#### Build +Build: ``` forge build ``` -#### Test +Test: ``` forge test ``` -#### Coverage +Coverage: ``` forge coverage --ir-minimum --report lcov && genhtml -o coverage lcov.info @@ -91,14 +141,8 @@ forge coverage --ir-minimum --report lcov && genhtml -o coverage lcov.info ## License -This codebase is licensed under a dual license model: +This codebase is licensed under Source Available License. -1. Open Attribution License – this license is similar to the MIT License and permits broad -use (including for commercial purposes), but is only available when the codebase is used in -connection with cryptoasset deposits into the Agglayer’s unified LxLy bridge. See: [LICENSE-OPEN-LICENSE]() -2. Source Available License – for all other use cases, including cryptoasset deposits into -elsewhere than the Agglayer’s unified LxLy bridge, you may use the codebase under the Source -Available License. See: [LICENSE-SOURCE-AVAILABLE-LICENSE]() +See [`LICENSE-SOURCE-AVAILABLE`](https://github.com/agglayer/vault-bridge/blob/main/LICENSE-SOURCE-AVAILABLE). -Your use of this software constitutes acceptance of these license terms. If you are unsure whether -your use qualifies under the Open Attribution license, please contact: legal@polygon.technology \ No newline at end of file +Your use of this software constitutes acceptance of these license terms. \ No newline at end of file diff --git a/broadcast/README.md b/broadcast/README.md new file mode 100644 index 00000000..476e47c0 --- /dev/null +++ b/broadcast/README.md @@ -0,0 +1,21 @@ +# Deployments + +Legend: + +- Primary Chain + - VB: Vault Bridge + - MM: Migration Manager +- Secondary Chain + - CT: Custom Token + - NC: Native Converter + - WT: Wrapped Token + - ST: Standardized Token + +
+ +| Chain Name | Chain ID | Bridge Provider | Vault Bridge Protocol | vbUSDC | vbETH | vbWBTC | vbUSDT | vbUSDS | Migration Manager | +| ---------- | -------- | --------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Ethereum | 1 | Agglayer | 0.5.0 | [VB](https://etherscan.io/address/0x53E82ABbb12638F09d9e624578ccB666217a765e) | [VB](https://etherscan.io/address/0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF) | [VB](http://etherscan.io/address/0x2C24B57e2CCd1f273045Af6A5f632504C432374F) | [VB](http://etherscan.io/address/0x6d4f9f9f8f0155509ecd6Ac6c544fF27999845CC) | [VB](http://etherscan.io/address/0x3DD459dE96F9C28e3a343b831cbDC2B93c8C4855) | [MM](http://etherscan.io/address/0x417d01B64Ea30C4E163873f3a1f77b727c689e02) | +| Katana | 747474 | Agglayer | 0.5.0 | [CT](https://katanascan.com/address/0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36), [NC](https://katanascan.com/address/0x97a3500083348A147F419b8a65717909762c389f) | [CT](https://katanascan.com/address/0xEE7D8BCFb72bC1880D0Cf19822eB0A2e6577aB62), [NC](https://katanascan.com/address/0xa6b0db1293144ebe9478b6a84f75dd651e45914a) | [CT](https://katanascan.com/address/0x0913DA6Da4b42f538B445599b46Bb4622342Cf52), [NC](https://katanascan.com/address/0xb00aa68b87256E2F22058fB2Ba3246EEc54A44fc) | [CT](https://katanascan.com/address/0x2DCa96907fde857dd3D816880A0df407eeB2D2F2), [NC](https://katanascan.com/address/0x053FA9b934b83E1E0ffc7e98a41aAdc3640bB462) | [CT](https://katanascan.com/address/0x62D6A123E8D19d06d68cf0d2294F9A3A0362c6b3), [NC](https://katanascan.com/address/0x639f13D5f30B47c792b6851238c05D0b623C77DE) | +| Ternoa | 752025 | Agglayer | N/A | [WT](https://explorer-mainnet.zkevm.ternoa.network/address/0xac8dDbc1f261eE4632518a1A62c23cD2b5a52BAd) | [WT](https://explorer-mainnet.zkevm.ternoa.network/address/0xABb9adE1Bd2A3bDcCD4c91FB149685936aCb758E) | [WT](https://explorer-mainnet.zkevm.ternoa.network/address/0x593A86cB824e0e3546fAE6bbC56e6f3f3EB6213E) | [WT](https://explorer-mainnet.zkevm.ternoa.network/address/0x5453e111aCE935Ba404b9474E14fe7a883B97519) | [WT](https://explorer-mainnet.zkevm.ternoa.network/address/0x90ea50a8Fe55945833Ef01F5AebAee3a814D2353) | +| Forknet | 8338 | Agglayer | | [WT](https://forkscan.org/address/0x203A662b0BD271A6ed5a60EdFbd04bFce608FD36) | [WT](https://forkscan.org/address/0xEE7D8BCFb72bC1880D0Cf19822eB0A2e6577aB62) | [WT](https://forkscan.org/address/0x0913DA6Da4b42f538B445599b46Bb4622342Cf52) | [WT](https://forkscan.org/address/0x2DCa96907fde857dd3D816880A0df407eeB2D2F2) | [WT](https://forkscan.org/address/0x62D6A123E8D19d06d68cf0d2294F9A3A0362c6b3) | | \ No newline at end of file diff --git a/certora/confs/GenericNativeConverter.conf b/certora/confs/GenericNativeConverter.conf new file mode 100644 index 00000000..c7b0e790 --- /dev/null +++ b/certora/confs/GenericNativeConverter.conf @@ -0,0 +1,62 @@ +{ + "files": [ + "certora/harnesses/StorageExtension.sol", + "certora/mocks/TokenMock.sol", + "src/custom-tokens/GenericNativeConverter.sol", + "src/custom-tokens/GenericCustomToken.sol", + "src/MigrationManager.sol", + "certora/mocks/ILxLyBridgeMock.sol", + ], + "global_timeout": "7200", + "loop_iter": "1", + "msg": "GenericNativeConverter", + "mutations": { + "gambit": [ + { + "filename": "src/custom-tokens/GenericNativeConverter.sol", + "num_mutants": 5 + } + ] + }, + "optimistic_hashing": true, + "parametric_contracts": ["GenericNativeConverter"], + "optimistic_loop": true, + "packages": [ + "@types/bun=node_modules/@types/bun", + "@0xpolygonhermez/zkevm-commonjs=node_modules/@0xpolygonhermez/zkevm-commonjs", + "@openzeppelin-contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-5.1.0", + "@openzeppelin-contracts=dependencies/@openzeppelin-contracts-5.1.0", + "@openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.1.0", + "forge-std=dependencies/forge-std-1.9.4/src", + // for zkmevm-contracts + "@openzeppelin/contracts-upgradeable4=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable4", + "@openzeppelin/contracts-upgradeable5=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable5", + "@zkmcontractsv2=zkevm-contracts/contracts/v2", + ], + "process": "emv", + "prover_args": [ + "-smt_initialSplitDepth 4", + "-depth 10", + "-mediumTimeout 2" + ], + "rule_sanity": "basic", + "solc_map": { + "StorageExtension": "solc8.29", + "GenericNativeConverter": "solc8.29", + "GenericCustomToken": "solc8.29", + "MigrationManager": "solc8.29", + "TokenMock": "solc8.28", + "ILxLyBridgeMock": "solc8.29", + }, + "solc_optimize": "200", + "solc_via_ir": true, + "storage_extension_harnesses": [ + "GenericNativeConverter=StorageExtension", + ], + "struct_link": [ + "GenericNativeConverter:customToken=GenericCustomToken", + "GenericNativeConverter:underlyingToken=TokenMock", + ], + "server": "production", + "verify": "GenericNativeConverter:certora/specs/GenericNativeConverter_invariants.spec" +} diff --git a/certora/confs/GenericVaultBridgeToken.conf b/certora/confs/GenericVaultBridgeToken.conf new file mode 100644 index 00000000..316ad509 --- /dev/null +++ b/certora/confs/GenericVaultBridgeToken.conf @@ -0,0 +1,85 @@ +{ + "assert_autofinder_success": true, + "contract_recursion_limit": "1", + "disable_auto_cache_key_gen": true, + "files": [ + "certora/harnesses/StorageExtension.sol", + "certora/harnesses/GenericVaultBridgeToken.sol", + "certora/mocks/ILxLyBridgeMock.sol", + "certora/mocks/TokenMock.sol", + "certora/mocks/VaultMock.sol", + "src/VaultBridgeTokenInitializer.sol", + "src/VaultBridgeTokenPart2.sol", + ], + "server": "production", + "global_timeout": "7200", + "smt_timeout": "7200", + "loop_iter": "2", + "link": [ + "VaultMock:asset=TokenMock", + "GenericVaultBridgeToken:PART2=VaultBridgeTokenPart2", + ], + "msg": "GenericVaultBridgeToken", + "mutations": { + "gambit": [ + { + "filename": "src/vault-bridge-tokens/GenericVaultBridgeToken.sol", + "num_mutants": 5 + } + ] + }, + "optimistic_contract_recursion": true, + "optimistic_hashing": true, + "optimistic_loop": true, + "optimistic_summary_recursion": true, + "packages": [ + "@types/bun=node_modules/@types/bun", + "@0xpolygonhermez/zkevm-commonjs=node_modules/@0xpolygonhermez/zkevm-commonjs", + "@openzeppelin-contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-5.1.0", + "@openzeppelin-contracts=dependencies/@openzeppelin-contracts-5.1.0", + "@openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.1.0", + "forge-std=dependencies/forge-std-1.9.4/src", + // for zkmevm-contracts + "@openzeppelin/contracts-upgradeable4=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable4", + "@openzeppelin/contracts-upgradeable5=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable5", + "@zkmcontractsv2=zkevm-contracts/contracts/v2", + ], + "process": "emv", + "prover_args": [ + "-maxDecompiledCommandCount 5000000", + "-maxBlockCount 500000", + ], + "rule_sanity": "basic", + "parametric_contracts": ["GenericVaultBridgeToken" ], + "solc_map": { + "StorageExtension": "solc8.29", + "GenericVaultBridgeToken": "solc8.29", + "MigrationManager": "solc8.29", + "VaultBridgeTokenInitializer": "solc8.29", + "VaultBridgeTokenPart2": "solc8.29", + "ILxLyBridgeMock": "solc8.29", + "VaultMock": "solc8.29", + "TokenMock": "solc8.28", + }, + "solc_optimize": "200", + //"build_cache": true, + "solc_via_ir": true, + "storage_extension_harnesses": [ + "GenericVaultBridgeToken=StorageExtension", + "VaultBridgeTokenInitializer=StorageExtension", + "VaultBridgeTokenPart2=StorageExtension" + ], + "struct_link": [ + "GenericVaultBridgeToken:lxlyBridge=ILxLyBridgeMock", + "GenericVaultBridgeToken:underlyingToken=TokenMock", + "GenericVaultBridgeToken:yieldVault=VaultMock", + "GenericVaultBridgeToken:_vaultBridgeTokenPart2=VaultBridgeTokenPart2", + "VaultBridgeTokenInitializer:underlyingToken=TokenMock", + "VaultBridgeTokenInitializer:yieldVault=VaultMock", + "VaultBridgeTokenPart2:lxlyBridge=ILxLyBridgeMock", + "VaultBridgeTokenPart2:underlyingToken=TokenMock", + "VaultBridgeTokenPart2:yieldVault=VaultMock", + ], + "summary_recursion_limit": "2", + "verify": "GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_ERC4626.spec" +} diff --git a/certora/confs/MigrationManager-generic.conf b/certora/confs/MigrationManager-generic.conf new file mode 100644 index 00000000..c26bc924 --- /dev/null +++ b/certora/confs/MigrationManager-generic.conf @@ -0,0 +1,17 @@ +{ + "override_base_config": "certora/confs/base/MigrationManager.conf", + "msg": "MigrationManager with GenericVaultBridgeToken", + "struct_link": [ + "MigrationManager:underlyingToken=TokenMock", + "MigrationManager:vbToken=GenericVaultBridgeToken", + "GenericVaultBridgeToken:underlyingToken=TokenMock", + "GenericVaultBridgeToken:yieldVault=VaultMock", + "GenericVaultBridgeToken:_vaultBridgeTokenPart2=VaultBridgeTokenPart2", + "VbETH:underlyingToken=TokenMock", + "VbETH:yieldVault=VaultMock", + "VbETH:_vaultBridgeTokenPart2=VaultBridgeTokenPart2", + "VaultBridgeTokenPart2:underlyingToken=TokenMock", + "VaultBridgeTokenPart2:yieldVault=VaultMock", + ], + "verify": "MigrationManager:certora/specs/MigrationManager-generic.spec" +} \ No newline at end of file diff --git a/certora/confs/base/MigrationManager.conf b/certora/confs/base/MigrationManager.conf new file mode 100644 index 00000000..586ea709 --- /dev/null +++ b/certora/confs/base/MigrationManager.conf @@ -0,0 +1,63 @@ +{ + "files": [ + "certora/harnesses/StorageExtension.sol", + "certora/mocks/TokenMock.sol", + "certora/mocks/VaultMock.sol", + "src/MigrationManager.sol", + "src/VaultBridgeTokenPart2.sol", + "src/vault-bridge-tokens/GenericVaultBridgeToken.sol", + "src/vault-bridge-tokens/vbETH/VbETH.sol", + ], + "global_timeout": "7200", + "loop_iter": "2", + "msg": "MigrationManager", + "optimistic_hashing": true, + "optimistic_loop": true, + "packages": [ + "@types/bun=node_modules/@types/bun", + "@0xpolygonhermez/zkevm-commonjs=node_modules/@0xpolygonhermez/zkevm-commonjs", + "@openzeppelin-contracts-upgradeable=dependencies/@openzeppelin-contracts-upgradeable-5.1.0", + "@openzeppelin-contracts=dependencies/@openzeppelin-contracts-5.1.0", + "@openzeppelin/contracts=dependencies/@openzeppelin-contracts-5.1.0", + "forge-std=dependencies/forge-std-1.9.4/src", + // for zkmevm-contracts + "@openzeppelin/contracts-upgradeable4=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable4", + "@openzeppelin/contracts-upgradeable5=zkevm-contracts/node_modules/@openzeppelin/contracts-upgradeable5", + "@zkmcontractsv2=zkevm-contracts/contracts/v2", + ], + "struct_link": [ + "MigrationManager:underlyingToken=TokenMock", + "MigrationManager:vbToken=GenericVaultBridgeToken", + "GenericVaultBridgeToken:underlyingToken=TokenMock", + "GenericVaultBridgeToken:yieldVault=VaultMock", + "GenericVaultBridgeToken:_vaultBridgeTokenPart2=VaultBridgeTokenPart2", + "VbETH:underlyingToken=TokenMock", + "VbETH:yieldVault=VaultMock", + "VbETH:_vaultBridgeTokenPart2=VaultBridgeTokenPart2", + "VaultBridgeTokenPart2:underlyingToken=TokenMock", + "VaultBridgeTokenPart2:yieldVault=VaultMock", + ], + "process": "emv", + "prover_args": [ + + ], + "rule_sanity": "basic", + "solc_map": { + "StorageExtension": "solc8.29", + "MigrationManager": "solc8.29", + "VaultBridgeTokenPart2": "solc8.29", + "GenericVaultBridgeToken": "solc8.29", + "VbETH": "solc8.29", + "VaultMock": "solc8.29", + "TokenMock": "solc8.28", + }, + "solc_optimize": "200", + "solc_via_ir": true, + "server": "production", + "storage_extension_harnesses": [ + "MigrationManager=StorageExtension", + "GenericVaultBridgeToken=StorageExtension", + "VbETH=StorageExtension", + ], + "verify": "MigrationManager:certora/specs/MigrationManager-generic.spec" +} \ No newline at end of file diff --git a/src/vault-bridge-tokens/GenericVaultBridgeToken.sol b/certora/harnesses/GenericVaultBridgeToken.sol similarity index 51% rename from src/vault-bridge-tokens/GenericVaultBridgeToken.sol rename to certora/harnesses/GenericVaultBridgeToken.sol index acfa8edc..c8e2139d 100644 --- a/src/vault-bridge-tokens/GenericVaultBridgeToken.sol +++ b/certora/harnesses/GenericVaultBridgeToken.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (vault-bridge-tokens/GenericVaultBridgeToken.sol) + pragma solidity 0.8.29; // Main functionality. -import {VaultBridgeToken} from "../VaultBridgeToken.sol"; - -// Other functionality. -import {IVersioned} from "../etc/IVersioned.sol"; +import {VaultBridgeToken} from "../../src/VaultBridgeToken.sol"; /// @title Generic Vault Bridge Token /// @author See https://github.com/agglayer/vault-bridge @@ -23,10 +22,22 @@ contract GenericVaultBridgeToken is VaultBridgeToken { __VaultBridgeToken_init(initializer_, initParams); } - // -----================= ::: INFO ::: =================----- + // Harness methods + function rebalanceReserve_harness(bool force, bool allowRebalanceDown) external + { + _rebalanceReserve(force, allowRebalanceDown); + } + + function simulateWithdraw_harness(uint256 assets, bool force) external returns (uint256) + { + return _simulateWithdraw(assets, force); + } - /// @inheritdoc IVersioned - function version() external pure virtual returns (string memory) { - return "0.5.0"; + function depositIntoYieldVault_harness(uint256 assets, bool exact) external returns (uint256) + { + return _depositIntoYieldVault(assets, exact); } + + /// @notice Yield collected getter + } diff --git a/certora/harnesses/StorageExtension.sol b/certora/harnesses/StorageExtension.sol new file mode 100644 index 00000000..b9c9485a --- /dev/null +++ b/certora/harnesses/StorageExtension.sol @@ -0,0 +1,32 @@ +import { CustomToken } from "src/CustomToken.sol"; +import { MigrationManager } from "src/MigrationManager.sol"; +import { NativeConverter } from "src/NativeConverter.sol"; +import { VaultBridgeToken } from "src/VaultBridgeToken.sol"; +import { WETHNativeConverter } from "src/custom-tokens/WETH/WETHNativeConverter.sol"; + +contract StorageExtension { + /** + * @custom:certoralink 0x0300d81ec8b5c42d6bd2cedd81ce26f1003c52753656b7512a8eef168b702500 + */ + CustomToken.CustomTokenStorage customTokenStorage; + + /** + * @custom:certoralink 0x30cf29e424d82bdf294fbec113ef39ac73137edfdb802b37ef3fc9ad433c5000 + */ + MigrationManager.MigrationManagerStorage migrationManagerStorage; + + /** + * @custom:certoralink 0xa14770e0debfe4b8406a01c33ee3a7bbe0acc66b3bde7c71854bf7d080a9c600 + */ + NativeConverter.NativeConverterStorage nativeConverterStorage; + + /** + * @custom:certoralink 0xf082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44700 + */ + VaultBridgeToken.VaultBridgeTokenStorage vaultBridgeTokenStorage; + + /** + * @custom:certoralink 0xf9565ea242552c2a1a216404344b0c8f6a3093382a21dd5bd6f5dc2ff1934d00 + */ + WETHNativeConverter.WETHNativeConverterStorage wETHNativeConverterStorage; +} diff --git a/certora/mocks/ILxLyBridgeMock.sol b/certora/mocks/ILxLyBridgeMock.sol new file mode 100644 index 00000000..e4981b60 --- /dev/null +++ b/certora/mocks/ILxLyBridgeMock.sol @@ -0,0 +1,48 @@ +import { ILxLyBridge } from "src/etc/ILxLyBridge.sol"; + +contract ILxLyBridgeMock is ILxLyBridge { + function networkID() external view returns (uint32) { return 0; } + function gasTokenAddress() external view returns (address) { return address(0); } + function gasTokenNetwork() external view returns (uint32) { return 0; } + function bridgeAsset( + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + address token, + bool forceUpdateGlobalExitRoot, + bytes calldata permitData + ) external payable {} + function claimAsset( + bytes32[32] calldata smtProofLocalExitRoot, + bytes32[32] calldata smtProofRollupExitRoot, + uint256 globalIndex, + bytes32 mainnetExitRoot, + bytes32 rollupExitRoot, + uint32 originNetwork, + address originTokenAddress, + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + bytes calldata metadata + ) external {} + function claimMessage( + bytes32[32] calldata smtProofLocalExitRoot, + bytes32[32] calldata smtProofRollupExitRoot, + uint256 globalIndex, + bytes32 mainnetExitRoot, + bytes32 rollupExitRoot, + uint32 originNetwork, + address originAddress, + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + bytes calldata metadata + ) external {} + function bridgeMessage( + uint32 destinationNetwork, + address destinationAddress, + bool forceUpdateGlobalExitRoot, + bytes calldata metadata + ) external payable {} + function wrappedAddressIsNotMintable(address wrappedAddress) external view returns (bool isNotMintable) { return false; } +} diff --git a/test/etc/TestVault.sol b/certora/mocks/TestVault.sol similarity index 76% rename from test/etc/TestVault.sol rename to certora/mocks/TestVault.sol index 6751d621..3ba5b26a 100644 --- a/test/etc/TestVault.sol +++ b/certora/mocks/TestVault.sol @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available pragma solidity 0.8.29; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -13,7 +12,7 @@ contract TestVault { IERC20 public asset; uint256 public slippageAmount; - bool public slippage; + bool public slippage = false; mapping(address => uint256) public balanceOf; @@ -59,37 +58,23 @@ contract TestVault { { user; } - return _maxWithdraw; + return balanceOf[user]; } function deposit(uint256 amount, address user) external payable returns (uint256) { - if (slippage) { - require(amount > slippageAmount, "TestVault: Slippage amount is too high"); - _receiveAssets(amount - slippageAmount, user); - } else { _receiveAssets(amount, user); - } + return amount; } function withdraw(uint256 amount, address receiver, address user) external returns (uint256) { require(balanceOf[user] >= amount, "TestVault: Insufficient balance"); _sendAssets(amount, receiver, user); - if (slippage) { - require(amount > slippageAmount, "TestVault: Slippage amount is too high"); - return amount + slippageAmount; - } else { return amount; - } } function previewWithdraw(uint256 amount) external view returns (uint256) { - if (slippage) { - require(amount > slippageAmount, "TestVault: Slippage amount is too high"); - return amount + slippageAmount; - } else { return amount; - } } function _receiveAssets(uint256 amount, address user) internal { diff --git a/certora/mocks/TokenMock.sol b/certora/mocks/TokenMock.sol new file mode 100644 index 00000000..50371a72 --- /dev/null +++ b/certora/mocks/TokenMock.sol @@ -0,0 +1,255 @@ +// can't actually import because solc version doesn't match. But we implement it anyway. +//import { IWETH9 } from "src/etc/IWETH9.sol"; +import { IERC20 } from "forge-std/interfaces/IERC20.sol"; +import { ITokenWrappedBridgeUpgradeable } from "@zkmcontractsv2/interfaces/ITokenWrappedBridgeUpgradeable.sol"; +import { IERC20MetadataUpgradeable } from "@openzeppelin/contracts-upgradeable4/token/ERC20/extensions/IERC20MetadataUpgradeable.sol"; + +/// Copied from forge-std MockERC20 +/// @notice This is a mock contract of the ERC20 standard for testing purposes only, it SHOULD NOT be used in production. +/// @dev Forked from: https://github.com/transmissions11/solmate/blob/0384dbaaa4fcb5715738a9254a7c0a4cb62cf458/src/tokens/ERC20.sol +contract TokenMock is IERC20MetadataUpgradeable, ITokenWrappedBridgeUpgradeable { + /*////////////////////////////////////////////////////////////// + METADATA STORAGE + //////////////////////////////////////////////////////////////*/ + + string internal _name; + + string internal _symbol; + + uint8 internal _decimals; + + function name() external view override returns (string memory) { + return _name; + } + + function symbol() external view override returns (string memory) { + return _symbol; + } + + function decimals() external view override returns (uint8) { + return _decimals; + } + + // from IWETH9 + + function deposit() external payable { + _balanceOf[msg.sender] += msg.value; + } + function withdraw(uint256) external {} + + /*////////////////////////////////////////////////////////////// + ERC20 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal _totalSupply; + + mapping(address => uint256) internal _balanceOf; + + mapping(address => mapping(address => uint256)) internal _allowance; + + function totalSupply() external view override returns (uint256) { + return _totalSupply; + } + + function balanceOf(address owner) external view override returns (uint256) { + return _balanceOf[owner]; + } + + function allowance(address owner, address spender) external view override returns (uint256) { + return _allowance[owner][spender]; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 STORAGE + //////////////////////////////////////////////////////////////*/ + + uint256 internal INITIAL_CHAIN_ID; + + bytes32 internal INITIAL_DOMAIN_SEPARATOR; + + mapping(address => uint256) public nonces; + + /*////////////////////////////////////////////////////////////// + INITIALIZE + //////////////////////////////////////////////////////////////*/ + + /// @dev A bool to track whether the contract has been initialized. + bool private initialized; + + /// @dev To hide constructor warnings across solc versions due to different constructor visibility requirements and + /// syntaxes, we add an initialization function that can be called only once. + function initialize(string memory name_, string memory symbol_, uint8 decimals_) public { + require(!initialized, "ALREADY_INITIALIZED"); + + _name = name_; + _symbol = symbol_; + _decimals = decimals_; + + INITIAL_CHAIN_ID = _pureChainId(); + INITIAL_DOMAIN_SEPARATOR = computeDomainSeparator(); + + initialized = true; + } + + /*////////////////////////////////////////////////////////////// + ERC20 LOGIC + //////////////////////////////////////////////////////////////*/ + + function approve(address spender, uint256 amount) public virtual override returns (bool) { + _allowance[msg.sender][spender] = amount; + + emit Approval(msg.sender, spender, amount); + + return true; + } + + function transfer(address to, uint256 amount) public virtual override returns (bool) { + _balanceOf[msg.sender] = _sub(_balanceOf[msg.sender], amount); + _balanceOf[to] = _add(_balanceOf[to], amount); + + emit Transfer(msg.sender, to, amount); + + return true; + } + + function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + uint256 allowed = _allowance[from][msg.sender]; // Saves gas for limited approvals. + + if (allowed != ~uint256(0)) _allowance[from][msg.sender] = _sub(allowed, amount); + + _balanceOf[from] = _sub(_balanceOf[from], amount); + _balanceOf[to] = _add(_balanceOf[to], amount); + + emit Transfer(from, to, amount); + + return true; + } + + /*////////////////////////////////////////////////////////////// + EIP-2612 LOGIC + //////////////////////////////////////////////////////////////*/ + + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + virtual + { + require(deadline >= block.timestamp, "PERMIT_DEADLINE_EXPIRED"); + + address recoveredAddress = ecrecover( + keccak256( + abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ), + owner, + spender, + value, + nonces[owner]++, + deadline + ) + ) + ) + ), + v, + r, + s + ); + + require(recoveredAddress != address(0) && recoveredAddress == owner, "INVALID_SIGNER"); + + _allowance[recoveredAddress][spender] = value; + + emit Approval(owner, spender, value); + } + + // DAI style permit + function permit(address owner, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) public { + permit(owner, spender, nonce, expiry, v, r, s); + } + + function DOMAIN_SEPARATOR() public view virtual returns (bytes32) { + return _pureChainId() == INITIAL_CHAIN_ID ? INITIAL_DOMAIN_SEPARATOR : computeDomainSeparator(); + } + + function computeDomainSeparator() internal view virtual returns (bytes32) { + return keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(_name)), + keccak256("1"), + _pureChainId(), + address(this) + ) + ); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint(address to, uint256 amount) internal virtual { + _totalSupply = _add(_totalSupply, amount); + _balanceOf[to] = _add(_balanceOf[to], amount); + + emit Transfer(address(0), to, amount); + } + + function _burn(address from, uint256 amount) internal virtual { + _balanceOf[from] = _sub(_balanceOf[from], amount); + _totalSupply = _sub(_totalSupply, amount); + + emit Transfer(from, address(0), amount); + } + + function mint(address to, uint256 value) public { + _mint(to, value); + } + function burn(address account, uint256 value) public { + _burn(account, value); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL SAFE MATH LOGIC + //////////////////////////////////////////////////////////////*/ + + function _add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, "ERC20: addition overflow"); + return c; + } + + function _sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(a >= b, "ERC20: subtraction underflow"); + return a - b; + } + + /*////////////////////////////////////////////////////////////// + HELPERS + //////////////////////////////////////////////////////////////*/ + + // We use this complex approach of `_viewChainId` and `_pureChainId` to ensure there are no + // compiler warnings when accessing chain ID in any solidity version supported by forge-std. We + // can't simply access the chain ID in a normal view or pure function because the solc View Pure + // Checker changed `chainid` from pure to view in 0.8.0. + function _viewChainId() private view returns (uint256 chainId) { + // Assembly required since `block.chainid` was introduced in 0.8.0. + assembly { + chainId := chainid() + } + + address(this); // Silence warnings in older Solc versions. + } + + function _pureChainId() private pure returns (uint256 chainId) { + function() internal view returns (uint256) fnIn = _viewChainId; + function() internal pure returns (uint256) pureChainId; + assembly { + pureChainId := fnIn + } + chainId = pureChainId(); + } +} diff --git a/certora/mocks/VaultMock.sol b/certora/mocks/VaultMock.sol new file mode 100644 index 00000000..edf7a0eb --- /dev/null +++ b/certora/mocks/VaultMock.sol @@ -0,0 +1,12 @@ +import { TestVault } from "./TestVault.sol"; + +// extend TestVault with some IERC4626 snippets +contract VaultMock is TestVault { + constructor(address _asset) TestVault(_asset) {} + function maxRedeem(address owner) external view returns (uint256 maxShares) { + return 42; + } + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets) { + return shares; + } +} diff --git a/certora/patches/VaultBridgeToken.patch b/certora/patches/VaultBridgeToken.patch new file mode 100644 index 00000000..3dcae706 --- /dev/null +++ b/certora/patches/VaultBridgeToken.patch @@ -0,0 +1,185 @@ +diff --git a/src/VaultBridgeToken.sol b/src/VaultBridgeToken.sol +index db15f14..25a6009 100644 +--- a/src/VaultBridgeToken.sol ++++ b/src/VaultBridgeToken.sol +@@ -91,6 +91,9 @@ + address vaultBridgeTokenPart2; + } + ++ // Certora: munge to simplify delegation to Part2 ++ address PART2; ++ + // Basic roles. + bytes32 public constant REBALANCER_ROLE = keccak256("REBALANCER_ROLE"); + bytes32 public constant YIELD_COLLECTOR_ROLE = keccak256("YIELD_COLLECTOR_ROLE"); +@@ -893,7 +896,9 @@ + /// @notice This function can be called by a rebalancer only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function rebalanceReserve() external virtual delegatedToPart2 {} ++ function rebalanceReserve() external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("rebalanceReserve()")); ++ } + + /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. + /// @param force Whether to revert if the reserve cannot be rebalanced. +@@ -959,7 +964,9 @@ + /// @dev Increases the net collected yield. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function collectYield() external virtual delegatedToPart2 {} ++ function collectYield() external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("collectYield()")); ++ } + + /// @notice Burns a specific amount of vbToken. + /// @notice This function can be used if the yield recipient has collected an unrealistic amount of yield over time. +@@ -968,9 +975,8 @@ + /// @dev Does not rebalance the reserve after burning to allow usage while the contract is paused. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function burn(uint256 shares) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- shares; ++ function burn(uint256 shares) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("burn(uint256)", shares)); + } + + /// @notice Adds a specific amount of the underlying token to the reserve by transferring it from the sender. +@@ -978,17 +984,15 @@ + /// @notice This function can be called by anyone. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function donateAsYield(uint256 assets) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- assets; ++ function donateAsYield(uint256 assets) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("donateAsYield(uint256)", assets)); + } + + /// @notice Adds a specific amount of the underlying token to a dedicated fund for covering any fees on Layer Y during a migration of backing to Layer X by transferring it from the sender. Please refer to `completeMigration` for more information. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function donateForCompletingMigration(uint256 assets) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- assets; ++ function donateForCompletingMigration(uint256 assets) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("donateForCompletingMigration(uint256)", assets)); + } + + /// @notice Completes a migration of backing from a Layer Y to Layer X by minting and locking the required amount of vbToken in LxLy Bridge. +@@ -1006,12 +1010,8 @@ + function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) + external + virtual +- delegatedToPart2 + { +- // Silence the Solidity compiler. +- originNetwork; +- shares; +- assets; ++ PART2.delegatecall(abi.encodeWithSignature("completeMigration(uint32,uint256,uint256)", originNetwork, shares, assets)); + } + + /// @notice Drains the yield vault by redeeming yield vault shares. Assets will be put into the internal reserve. +@@ -1021,10 +1021,8 @@ + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param shares The amount of the yield vault shares to redeem. + /// @param exact Whether to revert if the exact amount of shares could not be redeemed. +- function drainYieldVault(uint256 shares, bool exact) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- shares; +- exact; ++ function drainYieldVault(uint256 shares, bool exact) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("drainYieldVault(uint256,bool)", shares, exact)); + } + + /// @notice Sets the minimum reserve percentage. +@@ -1033,9 +1031,8 @@ + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumReservePercentage_ `1e18` is 100%. +- function setMinimumReservePercentage(uint256 minimumReservePercentage_) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- minimumReservePercentage_; ++ function setMinimumReservePercentage(uint256 minimumReservePercentage_) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("setMinimumReservePercentage(uint256)", minimumReservePercentage_)); + } + + /// @notice Sets the yield vault. +@@ -1043,9 +1040,8 @@ + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function setYieldVault(address yieldVault_) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- yieldVault_; ++ function setYieldVault(address yieldVault_) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("setYieldVault(address)", yieldVault_)); + } + + /// @notice Sets the yield recipient. +@@ -1053,9 +1049,8 @@ + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function setYieldRecipient(address yieldRecipient_) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- yieldRecipient_; ++ function setYieldRecipient(address yieldRecipient_) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("setYieldRecipient(address)", yieldRecipient_)); + } + + /// @notice The minimum amount of the underlying token that triggers a yield vault deposit. +@@ -1065,9 +1060,8 @@ + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumYieldVaultDeposit_ Set to `0` to disable. +- function setMinimumYieldVaultDeposit(uint256 minimumYieldVaultDeposit_) external virtual delegatedToPart2 { +- // Silence the Solidity compiler. +- minimumYieldVaultDeposit_; ++ function setMinimumYieldVaultDeposit(uint256 minimumYieldVaultDeposit_) external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("setMinimumYieldVaultDeposit(uint256)", minimumYieldVaultDeposit_)); + } + + /// @notice The maximum slippage percentage when depositing into or withdrawing from the yield vault. +@@ -1078,10 +1072,8 @@ + function setYieldVaultMaximumSlippagePercentage(uint256 maximumSlippagePercentage) + external + virtual +- delegatedToPart2 +- { +- // Silence the Solidity compiler. +- maximumSlippagePercentage; ++ { ++ PART2.delegatecall(abi.encodeWithSignature("setYieldVaultMaximumSlippagePercentage(uint256)", maximumSlippagePercentage)); + } + + /// @notice Calculates the amount of assets to reserve (as opposed to depositing into the yield vault) based on the current and minimum reserve percentages. +@@ -1308,14 +1300,24 @@ + /// @notice This function can be called by a pauser only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function pause() external virtual delegatedToPart2 {} ++ function pause() external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("pause()")); ++ } + + /// @notice Allows usage of functions with the `whenNotPaused` modifier. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. +- function unpause() external virtual delegatedToPart2 {} ++ function unpause() external virtual { ++ PART2.delegatecall(abi.encodeWithSignature("unpause()")); ++ } + ++ /// Method added by Certora ++ function getNetCollectedYield() public view returns (uint256) { ++ VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); ++ return $._netCollectedYield; ++ } ++ + // -----================= ::: PART 2 ::: =================----- + + /// @notice Delegates the call to `VaultBridgeTokenPart2`. diff --git a/src/VaultBridgeToken.sol b/certora/patches/VaultBridgeToken_patched.sol similarity index 68% rename from src/VaultBridgeToken.sol rename to certora/patches/VaultBridgeToken_patched.sol index 8216298c..6cb348b8 100644 --- a/src/VaultBridgeToken.sol +++ b/certora/patches/VaultBridgeToken_patched.sol @@ -1,7 +1,7 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (VaultBridgeToken.sol) -// @remind UPDATE DOCUMENTATION. +pragma solidity 0.8.29; // Main functionality. import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; @@ -16,7 +16,7 @@ import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/Pau import {ReentrancyGuardTransientUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; import {ERC20PermitUser} from "./etc/ERC20PermitUser.sol"; -import {IVersioned} from "./etc/IVersioned.sol"; +import {Versioned} from "./etc/Versioned.sol"; // Libraries. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -32,10 +32,9 @@ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IER /// @title Vault Bridge Token /// @author See https://github.com/agglayer/vault-bridge -/// @notice A vbToken is an ERC-20 token, ERC-4626 vault, and LxLy Bridge extension, enabling deposits and bridging of select assets, such as WBTC, WETH, USDT, USDC, and USDS, while producing yield. -/// @dev A base contract used to create vault bridge tokens. -/// @dev @note IMPORTANT: In order to not drive the complexity of the Vault Bridge protocol up, vbToken MUST NOT have transfer, deposit, or withdrawal fees. The underlying token on Layer X MUST NOT have a transfer fee; the contract will revert if a transfer fee is detected. The underlying token and Custom Token on Layer Ys MAY have transfer fees. The yield vault SHOULD NOT have deposit and/or withdrawal fees, and the price of its shares MUST NOT decrease (e.g., the vault does not realize bad debt); still, this contract implements solvency checks for protection. Additionally, the underlying token MUST NOT be a rebasing token, and MUST NOT have transfer hooks (i.e., the token does not enable reentrancy/cross-entrancy). -/// @dev It is expected that generated yield will offset any costs incurred when depositing to and withdrawing from the yield vault for the purpose of generating yield or rebalancing the internal reserve. +/// @notice A vbToken is an ERC-20 token, ERC-4626 vault, and LxLy Bridge extension, enabling deposits and bridging of select assets, such as WBTC, WETH, USDT, USDC, and USDS, while putting the assets to work to produce yield. +/// @dev A base contract used to create vbTokens. +/// @dev @note IMPORTANT: In order to not drive the complexity of the Vault Bridge protocol up, vbToken MUST NOT have transfer, deposit, or withdrawal fees. The underlying token on Layer X MUST NOT have a transfer fee; this contract will revert if it detects a transfer fee. The underlying token and Custom Token on Layer Ys MAY have transfer fees. The yield vault SHOULD NOT have deposit and/or withdrawal fees; however, it is expected that produced yield will offset any costs incurred when depositing to and withdrawing from the yield vault for the purpose of producing yield or rebalancing the internal reserve. The price of the yield vault's shares MUST NOT decrease (e.g., no bad debt realization); still, this contract implements solvency checks for protection with a configurable slippage parameter. Additionally, the underlying token MUST NOT be a rebasing token, and MUST NOT have transfer hooks (i.e., does not enable reentrancy/crossentrancy). abstract contract VaultBridgeToken is Initializable, AccessControlUpgradeable, @@ -44,7 +43,7 @@ abstract contract VaultBridgeToken is IERC4626, ERC20PermitUpgradeable, ERC20PermitUser, - IVersioned + Versioned { // Libraries. using SafeERC20 for IERC20; @@ -69,14 +68,14 @@ abstract contract VaultBridgeToken is address _vaultBridgeTokenPart2; } - /// @remind Document. - /// @dev Used for initializing the contract. - /// @dev @note (ATTENTION) `decimals` will match the underlying token. Defaults to 18 decimals if the underlying token reverts. - /// @param minimumReservePercentage_ 1e18 is 100%. - /// @param yieldVault_ An external, ERC-4246 compatible vault into which the underlying token is deposited to generate yield. - /// @param yieldRecipient_ The address that receives yield generated by the yield vault. The yield collector collects generated yield, while the yield recipient receives it. - /// @param minimumYieldVaultDeposit_ @remind Document. - /// @param transferFeeCalculator_ @remind Redocument. A dedicated fee calculator for covering the underlying token's transfer fees if the underlying token has a transfer fee. If the underlying token does not have a transfer fee, set to address(0). + /// @dev Parameters for initializing Vault Bridge Token contract. + /// @dev @note (ATTENTION) `decimals` will match the underlying token. Defaults to 18 decimals if the underlying token reverts on `decimals`. + /// @param owner (ATTENTION) This address will be granted the `DEFAULT_ADMIN_ROLE`, as well as all basic roles. Roles can be modified at any time. + /// @param minimumReservePercentage vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). `1e18` is 100%. @note (ATTENTION) Automatic reserve rebalancing will be disabled for values greater than `1e18` (100%). + /// @param yieldVault An external, ERC-4246 compliant vault into which the underlying token is deposited to produce yield. + /// @param yieldRecipient The address that receives yield produced by the yield vault. The yield collector collects yield, while the yield recipient receives it. + /// @param minimumYieldVaultDeposit The minimum amount of the underlying token that triggers a yield vault deposit. Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. The limit does not apply when rebalancing the reserve. Set to `0` to disable. + /// @param yieldVaultMaximumSlippagePercentage The maximum slippage percentage when depositing into or withdrawing from the yield vault. @note IMPORTANT: Any losses incurred due to slippage (and not fully covered by produced yield) will need to be covered by whomever is responsible for this contract. `1e18` is 100%. The recommended value is `0.01e18` (1%). struct InitializationParameters { address owner; string name; @@ -92,6 +91,9 @@ abstract contract VaultBridgeToken is address vaultBridgeTokenPart2; } + // Certora: munge to simplify delegation to Part2 + address PART2; + // Basic roles. bytes32 public constant REBALANCER_ROLE = keccak256("REBALANCER_ROLE"); bytes32 public constant YIELD_COLLECTOR_ROLE = keccak256("YIELD_COLLECTOR_ROLE"); @@ -152,7 +154,6 @@ abstract contract VaultBridgeToken is uint256 migrationFeesFundUtilization ); event YieldRecipientSet(address indexed yieldRecipient); - event TransferFeeCalculatorSet(address transferFeeCalculator); event MinimumReservePercentageSet(uint256 minimumReservePercentage); event YieldVaultDrained(uint256 redeemedShares, uint256 receivedAssets); event YieldVaultSet(address yieldVault); @@ -187,9 +188,15 @@ abstract contract VaultBridgeToken is _; } + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + modifier delegatedToPart2() { + _; + _delegateToPart2(); + } + // -----================= ::: SETUP ::: =================----- - // @remind Document. + /// @param initializer_ The address of `VaultBridgeTokenInitializer`. /// @param initParams Please refer to `InitializationParameters` for more information. function __VaultBridgeToken_init(address initializer_, InitializationParameters calldata initParams) internal @@ -198,38 +205,26 @@ abstract contract VaultBridgeToken is // Check the input. require(initializer_ != address(0), InvalidInitializer()); + // Verify the version of the initializer. + // The version string must be the same as that of this contract. + require( + keccak256(bytes(VaultBridgeToken(initializer_).version())) == keccak256(bytes(version())), + InvalidInitializer() + ); + // Initialize the contract using the external initializer. (bool ok, bytes memory data) = initializer_.delegatecall(abi.encodeCall(IVaultBridgeTokenInitializer.initialize, (initParams))); + // Check the result. if (!ok) { + // If the call failed, bubble up the revert data. assembly ("memory-safe") { revert(add(32, data), mload(data)) } } } - // -----================= ::: SOLIDITY ::: =================----- - - // @remind Document. - receive() external payable {} - - // @remind Document (the entire function). - fallback() external payable virtual { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); - - address vaultBridgeTokenPart2 = $._vaultBridgeTokenPart2; - - assembly { - calldatacopy(0, 0, calldatasize()) - let success := delegatecall(gas(), vaultBridgeTokenPart2, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch success - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } - // -----================= ::: STORAGE ::: =================----- /// @notice The underlying token that backs vbToken. @@ -238,36 +233,36 @@ abstract contract VaultBridgeToken is return $.underlyingToken; } - /// @notice The number of decimals of the vault bridge token. - /// @notice The number of decimals is the same as that of the underlying token, or 18 if the underlying token reverted (e.g., does not implement `decimals`). + /// @notice The number of decimals of vbToken. + /// @notice The number of decimals is the same as that of the underlying token, or `18` if the underlying token reverted on `decimals`. function decimals() public view override(ERC20Upgradeable, IERC20Metadata) returns (uint8) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.decimals; } - /// @notice Vault bridge tokens have an internal reserve of the underlying token from which withdrawals are served first. - /// @notice The owner can rebalance the reserve by calling `rebalanceReserve` when it is below or above the `minimumReservePercentage`. + /// @notice vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). + /// @notice The owner can rebalance the reserve by calling `rebalanceReserve` when it is below or above the `minimumReservePercentage`. The reserve may also be rebalanced automatically on deposits and withdrawals. /// @return 1e18 is 100%. function minimumReservePercentage() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.minimumReservePercentage; } - /// @notice Vault bridge tokens have an internal reserve of the underlying token from which withdrawals are served first. - /// @notice The owner can rebalance the reserve by calling `rebalanceReserve` when it is below or above the `minimumReservePercentage`. + /// @notice vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). + /// @notice How much of the underlying token is in the internal reserve. function reservedAssets() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.reservedAssets; } - /// @notice An external, ERC-4246 compatible vault into which the underlying token is deposited to generate yield. + /// @notice An external, ERC-4246 compliant vault into which the underlying token is deposited to produce yield. function yieldVault() public view returns (IERC4626) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.yieldVault; } - /// @notice The address that receives yield generated by the yield vault. - /// @notice The owner collects generated yield, while the yield recipient receives it. + /// @notice The address that receives yield produced by the yield vault. + /// @notice The yield collector collects yield, while the yield recipient receives it. function yieldRecipient() public view returns (address) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.yieldRecipient; @@ -285,34 +280,35 @@ abstract contract VaultBridgeToken is return $.lxlyBridge; } - /// @notice A dedicated fund for covering the underlying token's transfer fees during a migration from a Layer Y. Please refer to `_completeMigration` for more information. + /// @notice A dedicated fund for covering any fees on Layer Y during a migration of backing to Layer X. Please refer to `completeMigration` for more information. function migrationFeesFund() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.migrationFeesFund; } - /// @notice The minimum deposit amount for triggering a yield vault deposit. - /// @notice Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for the user. - /// @notice The limit does not apply when rebalancing the reserve. + /// @notice The minimum amount of the underlying token that triggers a yield vault deposit. + /// @dev Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. + /// @dev The limit does not apply when rebalancing the reserve. function minimumYieldVaultDeposit() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.minimumYieldVaultDeposit; } - // @remind Document. + /// @notice The address of Migration Manager. Please refer to `MigrationManager.sol` for more information. function migrationManager() public view returns (address) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.migrationManager; } - // @remind Document. + /// @notice The maximum slippage percentage when depositing into or withdrawing from the yield vault. + /// @return 1e18 is 100%. function yieldVaultMaximumSlippagePercentage() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.yieldVaultMaximumSlippagePercentage; } /// @dev Returns a pointer to the ERC-7201 storage namespace. - function _getVaultBridgeTokenStorage() internal pure returns (VaultBridgeTokenStorage storage $) { + function _getVaultBridgeTokenStorage() private pure returns (VaultBridgeTokenStorage storage $) { assembly { $.slot := _VAULT_BRIDGE_TOKEN_STORAGE } @@ -326,7 +322,7 @@ abstract contract VaultBridgeToken is return address($.underlyingToken); } - /// @notice The real-time total backing of vbToken in the underlying token. + /// @notice The total backing of vbToken in the underlying token in real-time. function totalAssets() public view returns (uint256 totalManagedAssets) { return stakedAssets() + reservedAssets(); } @@ -365,7 +361,6 @@ abstract contract VaultBridgeToken is } /// @notice Deposit a specific amount of the underlying token, and bridge minted vbToken to another network. - /// @dev If vbToken is custom mapped on LxLy Bridge on the other network, the user will receive Custom Token. Otherwise, they will receive wrapped vbToken. /// @dev The `receiver` in the ERC-4626 `Deposit` event will be this contract. function depositAndBridge( uint256 assets, @@ -381,7 +376,9 @@ abstract contract VaultBridgeToken is (shares,) = _deposit(assets, destinationNetworkId, receiver, forceUpdateGlobalExitRoot, 0); } - // @remind Document (the entire function). + /// @notice Locks the underlying token, mints vbToken, and optionally bridges it to another network. + /// @param maxShares Caps the amount of vbToken that is minted. Unused underlying token will be refunded to the sender. Set to `0` to disable. + /// @dev If bridging to another network, the `receiver` in the ERC-4626 `Deposit` event will be this contract. function _deposit( uint256 assets, uint32 destinationNetworkId, @@ -395,8 +392,8 @@ abstract contract VaultBridgeToken is } /// @notice Locks the underlying token, mints vbToken, and optionally bridges it to another network. + /// @param receiveUnderlyingToken A custom function to use for receiving the underlying token from the sender. @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. @note IMPORTANT: The function MUST detect and revert if there was a transfer fee. /// @param maxShares Caps the amount of vbToken that is minted. Unused underlying token will be refunded to the sender. Set to `0` to disable. - /// @param receiveUnderlyingToken @remind Document. /// @dev If bridging to another network, the `receiver` in the ERC-4626 `Deposit` event will be this contract. function _depositUsingCustomReceivingFunction( function(address, uint256) internal receiveUnderlyingToken, @@ -472,14 +469,15 @@ abstract contract VaultBridgeToken is // Emit the ERC-4626 event. emit IERC4626.Deposit(msg.sender, receiver, assets, shares); - // @remind Document. + // Cache the reserve percentage. uint256 reservePercentage_ = reservePercentage(); - // @remind Document. + // Check if the reserve needs to be rebalanced. if ( $.minimumReservePercentage < 1e18 && reservePercentage_ > 3 * $.minimumReservePercentage && reservePercentage_ > 0.1e18 ) { + // Rebalance the reserve. _rebalanceReserve(false, true); } } @@ -497,7 +495,6 @@ abstract contract VaultBridgeToken is } /// @notice Deposit a specific amount of the underlying token, and bridge minted vbToken to another network. - /// @dev If vbToken is custom mapped on LxLy Bridge on the other network, the user will receive Custom Token. Otherwise, they will receive wrapped vbToken. /// @dev Uses EIP-2612 permit to transfer the underlying token from the sender to self. /// @dev The `receiver` in the ERC-4626 `Deposit` event will be this contract. function depositWithPermitAndBridge( @@ -585,7 +582,7 @@ abstract contract VaultBridgeToken is /// @dev Calculates the amount of the underlying token that could be withdrawn right now. /// @dev This function is used for estimation purposes only. - /// @dev @note IMPORTANT: `reservedAssets` must be up-to-date before using this function. + /// @dev @note IMPORTANT: `reservedAssets` must be up-to-date before using this function! /// @param assets The maximum amount of the underlying token to simulate a withdrawal for. /// @param force Whether to revert if the all of the `assets` would not be withdrawn. function _simulateWithdraw(uint256 assets, bool force) internal view returns (uint256 withdrawnAssets) { @@ -597,38 +594,44 @@ abstract contract VaultBridgeToken is // The amount that cannot be withdrawn at the moment. uint256 remainingAssets = assets; - // Simulate withdrawal from the reserve. + // Simulate a withdrawal from the reserve. if ($.reservedAssets >= remainingAssets) return assets; remainingAssets -= $.reservedAssets; - // Simulate withdrawal from the yield vault. + // Calculate the amount to preview a withdrawal from the yield vault for. uint256 maxWithdraw_ = $.yieldVault.maxWithdraw(address(this)); maxWithdraw_ = remainingAssets > maxWithdraw_ ? maxWithdraw_ : remainingAssets; + + // Simulate a withdrawal from the yield vault. uint256 burnedYieldVaultShares; try $.yieldVault.previewWithdraw(maxWithdraw_) returns (uint256 shares) { + // Capture the amount of the yield vault shares that would be burned. burnedYieldVaultShares = shares; } catch (bytes memory data) { + // If `previewWithdraw` reverted, and all of the `assets` must be withdrawn, bubble up the revert data. if (force) { assembly ("memory-safe") { revert(add(32, data), mload(data)) } - } else { + } + // Otherwise, return the reserved assets. + else { return $.reservedAssets; } } - // @remind Document. + // Perform the same solvency check as `_withdrawFromYieldVault` would. bool solvencyCheckPassed = Math.mulDiv( convertToAssets(totalSupply() + yield()) - reservedAssets(), burnedYieldVaultShares, maxWithdraw_ ) <= Math.mulDiv($.yieldVault.balanceOf(address(this)), 1e18 + $.yieldVaultMaximumSlippagePercentage, 1e18); - // @remind Document. + // Revert if the solvency check failed and all of the `assets` must be withdrawn. if (!solvencyCheckPassed) { if (force) revert ExcessiveYieldVaultSharesBurned(burnedYieldVaultShares, maxWithdraw_); return $.reservedAssets; } - // @remind Document. + // Return if all of the `assets` would be withdrawn. if (remainingAssets == maxWithdraw_) return assets; remainingAssets -= maxWithdraw_; @@ -719,9 +722,10 @@ abstract contract VaultBridgeToken is // Emit the ERC-4626 event. emit IERC4626.Withdraw(msg.sender, receiver, owner, assets, shares); - // @remind Document. + // Check if the reserve needs to be rebalanced. if ($.minimumReservePercentage < 1e18 && reservePercentage() <= 0.01e18 && $.minimumReservePercentage >= 0.1e18) { + // Rebalance the reserve. _rebalanceReserve(false, false); } } @@ -811,7 +815,6 @@ abstract contract VaultBridgeToken is /// @dev Pausable ERC-20 `transfer` function. function transfer(address to, uint256 value) public - virtual override(ERC20Upgradeable, IERC20) whenNotPaused returns (bool) @@ -822,7 +825,6 @@ abstract contract VaultBridgeToken is /// @dev Pausable ERC-20 `transferFrom` function. function transferFrom(address from, address to, uint256 value) public - virtual override(ERC20Upgradeable, IERC20) whenNotPaused returns (bool) @@ -833,7 +835,6 @@ abstract contract VaultBridgeToken is /// @dev Pausable ERC-20 `approve` function. function approve(address spender, uint256 value) public - virtual override(ERC20Upgradeable, IERC20) whenNotPaused returns (bool) @@ -844,7 +845,6 @@ abstract contract VaultBridgeToken is /// @dev Pausable ERC-20 Permit `permit` function. function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public - virtual override whenNotPaused { @@ -853,13 +853,13 @@ abstract contract VaultBridgeToken is // -----================= ::: VAULT BRIDGE TOKEN ::: =================----- - /// @notice The real-time amount of the underlying token in the yield vault, as reported by the yield vault. + /// @notice The amount of the underlying token in the yield vault, as reported by the yield vault in real-time. function stakedAssets() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); return $.yieldVault.convertToAssets($.yieldVault.balanceOf(address(this))); } - /// @notice The real-time reserve percentage. + /// @notice The reserve percentage in real-time. /// @notice The reserve is based on the total supply of vbToken, and does not account for uncompleted migrations of backing from Layer Ys to Layer X. Please refer to `completeMigration` for more information. function reservePercentage() public view returns (uint256) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); @@ -871,7 +871,7 @@ abstract contract VaultBridgeToken is return Math.mulDiv($.reservedAssets, 1e18, convertToAssets(totalSupply())); } - /// @notice The real-time amount of yield available for collection. + /// @notice The amount of yield available for collection. function yield() public view returns (uint256) { // The formula for calculating yield is: // yield = assets reported by yield vault + reserved assets - vbToken total supply in assets @@ -881,7 +881,7 @@ abstract contract VaultBridgeToken is return positive ? convertToShares(difference) : 0; } - /// @notice The real-time difference between the total assets and the minimum assets required to back the total supply of vbToken. + /// @notice The difference between the total assets and the minimum assets required to back the total supply of vbToken in real-time. function backingDifference() public view returns (bool positive, uint256 difference) { // Get the state. uint256 totalAssets_ = totalAssets(); @@ -892,6 +892,14 @@ abstract contract VaultBridgeToken is totalAssets_ >= minimumAssets ? (true, totalAssets_ - minimumAssets) : (false, minimumAssets - totalAssets_); } + /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. + /// @notice This function can be called by a rebalancer only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function rebalanceReserve() external virtual { + PART2.delegatecall(abi.encodeWithSignature("rebalanceReserve()")); + } + /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. /// @param force Whether to revert if the reserve cannot be rebalanced. /// @param allowRebalanceDown Whether to allow the reserve to be rebalanced down (by depositing into the yield vault). @@ -904,7 +912,8 @@ abstract contract VaultBridgeToken is uint256 originalUncollectedYield = yield(); // Calculate the minimum reserve amount. - uint256 minimumReserve = convertToAssets(Math.mulDiv(originalTotalSupply, $.minimumReservePercentage, 1e18)); + uint256 minimumReserve = + convertToAssets(Math.mulDiv(originalTotalSupply, $.minimumReservePercentage, 1e18, Math.Rounding.Ceil)); // Check if the reserve is below, above, or at the minimum threshold. /* Below. */ @@ -917,7 +926,7 @@ abstract contract VaultBridgeToken is shortfall, false, address(this), originalTotalSupply, originalUncollectedYield, originalReservedAssets ); - // @remind Document. + // Revert if the the reserve could not be rebalanced and `force` is set to `true`. if (force && nonWithdrawnAssets == shortfall) revert CannotRebalanceReserve(); // Update the reserve. @@ -934,7 +943,7 @@ abstract contract VaultBridgeToken is // Try to deposit into the yield vault. uint256 nonDepositedAssets = _depositIntoYieldVault(excess, false); - // @remind Document. + // Revert if the the reserve could not be rebalanced and `force` is set to `true`. if (force && nonDepositedAssets == excess) revert CannotRebalanceReserve(); // Update the reserve. @@ -949,10 +958,128 @@ abstract contract VaultBridgeToken is } } - /// @notice Calculates the amount of assets to reserve (as opposed to depositing into the yield vault) based on the current reserve and minimum reserve percentage. - /// @dev @note (ATTENTION) Make any necessary changes to the reserve prior to using this function. + /// @notice Transfers yield produced by the yield vault to the yield recipient in the form of vbToken. + /// @notice Does not rebalance the reserve after collecting yield to allow usage while the contract is paused. + /// @notice This function can be called by a yield collector only. + /// @dev Increases the net collected yield. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function collectYield() external virtual { + PART2.delegatecall(abi.encodeWithSignature("collectYield()")); + } + + /// @notice Burns a specific amount of vbToken. + /// @notice This function can be used if the yield recipient has collected an unrealistic amount of yield over time. + /// @notice This function can be called by the yield recipient only. + /// @dev Decreases the net collected yield. + /// @dev Does not rebalance the reserve after burning to allow usage while the contract is paused. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function burn(uint256 shares) external virtual { + PART2.delegatecall(abi.encodeWithSignature("burn(uint256)", shares)); + } + + /// @notice Adds a specific amount of the underlying token to the reserve by transferring it from the sender. + /// @notice This function can be used to restore backing difference by donating the underlying token. + /// @notice This function can be called by anyone. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function donateAsYield(uint256 assets) external virtual { + PART2.delegatecall(abi.encodeWithSignature("donateAsYield(uint256)", assets)); + } + + /// @notice Adds a specific amount of the underlying token to a dedicated fund for covering any fees on Layer Y during a migration of backing to Layer X by transferring it from the sender. Please refer to `completeMigration` for more information. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function donateForCompletingMigration(uint256 assets) external virtual { + PART2.delegatecall(abi.encodeWithSignature("donateForCompletingMigration(uint256)", assets)); + } + + /// @notice Completes a migration of backing from a Layer Y to Layer X by minting and locking the required amount of vbToken in LxLy Bridge. + /// @notice Anyone can trigger the execution of this function by claiming the asset and message on LxLy Bridge. Please refer to `NativeConverter.sol` for more information. + /// @dev Backing for Custom Token minted by Native Converter on Layer Ys can be migrated to Layer X. + /// @dev When Native Converter migrates backing, it calls both `bridgeAsset` and `bridgeMessage` on LxLy Bridge to `migrateBackingToLayerX`. + /// @dev The asset must be claimed before the message on LxLy Bridge. + /// @dev The message tells vbToken how much Custom Token must be backed by vbToken, which is minted and bridged to address zero on the respective Layer Y. This action provides liquidity when bridging Custom Token to from Layer Ys to Layer X and increments the pessimistic proof. + /// @dev This function can be called by Migraton Manager only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param originNetwork The LxLy ID of Layer Y the backing is being migrated from. + /// @param shares The amount of vbToken required to mint and lock up in LxLy Bridge. Assets from a dedicated migration fees fund may be used to offset any fees incurred on Layer Y during the process. If a migration cannot be completed due to insufficient assets, anyone can donate the underlying token to the migration fees fund. Please refer to `donateForCompletingMigration` for more information. + /// @param assets The amount of the underlying token migrated from Layer Y (after any fees on Layer Y). + function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) + external + virtual + { + PART2.delegatecall(abi.encodeWithSignature("completeMigration(uint32,uint256,uint256)", originNetwork, shares, assets)); + } + + /// @notice Drains the yield vault by redeeming yield vault shares. Assets will be put into the internal reserve. + /// @notice This function may utilize availabe yield to ensure successful draining if there is larger slippage. Consider collecting yield before calling this function to disable this behavior. + /// @dev This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param shares The amount of the yield vault shares to redeem. + /// @param exact Whether to revert if the exact amount of shares could not be redeemed. + function drainYieldVault(uint256 shares, bool exact) external virtual { + PART2.delegatecall(abi.encodeWithSignature("drainYieldVault(uint256,bool)", shares, exact)); + } + + /// @notice Sets the minimum reserve percentage. + /// @notice @note (ATTENTION) Automatic reserve rebalancing will be disabled for values greater than `1e18` (100%). + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumReservePercentage_ `1e18` is 100%. + function setMinimumReservePercentage(uint256 minimumReservePercentage_) external virtual { + PART2.delegatecall(abi.encodeWithSignature("setMinimumReservePercentage(uint256)", minimumReservePercentage_)); + } + + /// @notice Sets the yield vault. + /// @notice @note CAUTION! Use `drainYieldVault` to drain the current yield vault completely before changing it. Any yield vault shares that are not redeemed will not count toward the underlying token backing after changing the yield vault. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function setYieldVault(address yieldVault_) external virtual { + PART2.delegatecall(abi.encodeWithSignature("setYieldVault(address)", yieldVault_)); + } + + /// @notice Sets the yield recipient. + /// @notice Yield will be collected before changing the recipient. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function setYieldRecipient(address yieldRecipient_) external virtual { + PART2.delegatecall(abi.encodeWithSignature("setYieldRecipient(address)", yieldRecipient_)); + } + + /// @notice The minimum amount of the underlying token that triggers a yield vault deposit. + /// @notice Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. + /// @notice The limit does not apply when rebalancing the reserve. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumYieldVaultDeposit_ Set to `0` to disable. + function setMinimumYieldVaultDeposit(uint256 minimumYieldVaultDeposit_) external virtual { + PART2.delegatecall(abi.encodeWithSignature("setMinimumYieldVaultDeposit(uint256)", minimumYieldVaultDeposit_)); + } + + /// @notice The maximum slippage percentage when depositing into or withdrawing from the yield vault. + /// @notice @note IMPORTANT: Any losses incurred due to slippage (and not fully covered by produced yield) will need to be covered by whomever is responsible for this contract. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param maximumSlippagePercentage 1e18 is 100%. The recommended value is `0.01e18` (1%). + function setYieldVaultMaximumSlippagePercentage(uint256 maximumSlippagePercentage) + external + virtual + { + PART2.delegatecall(abi.encodeWithSignature("setYieldVaultMaximumSlippagePercentage(uint256)", maximumSlippagePercentage)); + } + + /// @notice Calculates the amount of assets to reserve (as opposed to depositing into the yield vault) based on the current and minimum reserve percentages. + /// @dev @note (ATTENTION) `reservedAssets` must be up-to-date before using this function. /// @param assets The amount of the underlying token being deposited. - /// @param nonMintedShares The amount of vbToken that will be minted after using this function, as a result of the deposit. + /// @param nonMintedShares The amount of vbToken that will be minted after using this function as a result of the deposit. (Set to `0` if you have already minted all the shares). function _calculateAmountToReserve(uint256 assets, uint256 nonMintedShares) internal view @@ -961,19 +1088,19 @@ abstract contract VaultBridgeToken is VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); // Calculate the minimum reserve. - uint256 minimumReserve = - convertToAssets(Math.mulDiv(totalSupply() + nonMintedShares, $.minimumReservePercentage, 1e18)); + uint256 minimumReserve = convertToAssets( + Math.mulDiv(totalSupply() + nonMintedShares, $.minimumReservePercentage, 1e18, Math.Rounding.Ceil) + ); // Calculate the amount to reserve. assetsToReserve = $.reservedAssets < minimumReserve ? minimumReserve - $.reservedAssets : 0; return assetsToReserve <= assets ? assetsToReserve : assets; } - // @remind Redocument. /// @notice Deposit a specific amount of the underlying token into the yield vault. /// @param assets The amount of the underlying token to deposit into the yield vault. - /// @param exact @remind Document. - /// @return nonDepositedAssets The amount of the underlying token that could not be deposited into the yield vault. The value will be zero if `exact` is set to `true`. + /// @param exact Whether to revert if the exact amount of the underlying token could not be deposited into the yield vault. + /// @return nonDepositedAssets The amount of the underlying token that could not be deposited into the yield vault. The value will be `0` if `exact` is set to `true`. function _depositIntoYieldVault(uint256 assets, bool exact) internal returns (uint256 nonDepositedAssets) { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); @@ -983,13 +1110,13 @@ abstract contract VaultBridgeToken is return assets; } - // @remind Document. + // Cache the original assets. uint256 originalAssets = assets; // Get the yield vault's deposit limit. uint256 maxDeposit_ = $.yieldVault.maxDeposit(address(this)); - // @remind Document. + // Revert if the assets are greater than the deposit limit and `exact` is set to `true`. if (exact) require(assets <= maxDeposit_, YieldVaultDepositFailed(assets, maxDeposit_)); // Set the return value. @@ -998,75 +1125,93 @@ abstract contract VaultBridgeToken is // Calculate the amount to deposit into the yield vault. assets = assets > maxDeposit_ ? maxDeposit_ : assets; - // @remind Document. + // Return if no assets would be deposited. if (assets == 0) return nonDepositedAssets; - // @remind Document. + // Try to deposit into the yield vault. try this.performReversibleYieldVaultDeposit(assets) {} + // If the deposit failed, decode the revert data. catch (bytes memory data) { (bool depositSucceeded, bytes memory depositData, bool solvencyCheckPassed) = abi.decode(data, (bool, bytes, bool)); + // The yield vault deposit failed. if (!depositSucceeded) { + // Revert if the assets must have been put into the yield vault. if (exact) { + // Bubble up the revert data. assembly ("memory-safe") { revert(add(32, depositData), mload(depositData)) } } else { + // Return the amount of non-deposited assets. return originalAssets; } } + // The yield vault deposit succeeded, but the solvency check did not pass. if (!solvencyCheckPassed) { + // Revert if the assets must have been put into the yield vault. if (exact) { + // Revert with the standard solvency check error. uint256 mintedYieldVaultShares = abi.decode(depositData, (uint256)); revert InsufficientYieldVaultSharesMinted(assets, mintedYieldVaultShares); } else { + // Return the amount of non-deposited assets. return originalAssets; } } + // The yield vault deposit succeeded and the solvency check passed but the call still reverted for some reason. (Sanity check - should not happen). revert UnknownError(data); } } - // @remind Document (the entire function). + /// @notice Enables infinte deposits regardless of the behavior of the yield vault. + /// @dev This function reverts if the yield vault deposit fails, or the solvency check does not pass with revert data ABI-encoded in the following format: `abi.encode(depositSucceeded, depositData, solvencyCheckPassed)`, which can be decoded in another function. + /// @notice This function can be called by the this contract only. function performReversibleYieldVaultDeposit(uint256 assets) external whenNotPaused onlySelf { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + // Prepare the variables. bool depositSucceeded; bytes memory depositData; bool solvencyCheckPassed; + // Cache the staked assets before the deposit. uint256 oldStakedAssets = stakedAssets(); + // Try to deposit into the yield vault. (depositSucceeded, depositData) = address($.yieldVault).call(abi.encodeCall(IERC4626.deposit, (assets, address(this)))); + // The deposit succeeded. if (depositSucceeded) { // Check the output. - // This code checks if the minted yield vault shares are worth enough in the underlying token. + // This code checks if the minted yield vault shares are worth enough in the underlying token. Allows slippage. solvencyCheckPassed = stakedAssets() - oldStakedAssets >= Math.mulDiv(assets, 1e18 - $.yieldVaultMaximumSlippagePercentage, 1e18); } + // The deposit failed or the solvency check did not pass. if (!depositSucceeded || !solvencyCheckPassed) { + // Encode the information in the revert data. bytes memory data = abi.encode(depositSucceeded, depositData, solvencyCheckPassed); + // Revert with the encoded data. assembly ("memory-safe") { revert(add(32, data), mload(data)) } } } - // @remind Redocument. - /// @notice Withdraws an exact amount of the underlying token from the yield vault. + /// @notice Withdraws a specific amount of the underlying token from the yield vault. /// @param assets The amount of the underlying token to withdraw from the yield vault. - /// @param exact @remind Document. + /// @param exact Whether to revert if the exact amount of the underlying token could not be withdrawn from the yield vault. /// @param receiver The address to withdraw the underlying token to. - /// @param originalTotalSupply The total supply of vbToken before burning the required amount of vbToken or updating the reserve. - /// @param originalUncollectedYield The uncollected yield before burning the required amount of vbToken or updating the reserve. - /// @return nonWithdrawnAssets The amount of the underlying token that could not be withdrawn from the yield vault. The value will be zero if `exact` is set to `true`. - /// @return receivedAssets The amount of the underlying token actually received (e.g., after a transfer fee). The value will be zero if `receiver` is not `address(this)`. + /// @param originalTotalSupply The total supply of vbToken before burning the required amount of vbToken or updating the reserve. Used for the solvency check. + /// @param originalUncollectedYield The uncollected yield before burning the required amount of vbToken or updating the reserve. Used for the solvency check. + /// @return nonWithdrawnAssets The amount of the underlying token that could not be withdrawn from the yield vault. The value will be `0` if `exact` is set to `true`. + /// @return receivedAssets The amount of the underlying token actually received (e.g., after any fees). The value will be `0` if `receiver` is not `address(this)`. function _withdrawFromYieldVault( uint256 assets, bool exact, @@ -1080,7 +1225,7 @@ abstract contract VaultBridgeToken is // Get the yield vault's withdraw limit. uint256 maxWithdraw_ = $.yieldVault.maxWithdraw(address(this)); - // @remind Document. + // Revert if the assets are greater than the withdraw limit and `exact` is set to `true`. if (exact) require(assets <= maxWithdraw_, YieldVaultWithdrawalFailed(assets, maxWithdraw_)); // Set a return value. @@ -1089,11 +1234,11 @@ abstract contract VaultBridgeToken is // Calculate the amount to withdraw from the yield vault. assets = assets > maxWithdraw_ ? maxWithdraw_ : assets; - // @remind Document. + // Return if no assets would be withdrawn. if (assets == 0) return (nonWithdrawnAssets, 0); // Cache the underlying token balance and yield vault shares balance. - // The underying token balance is only cached when the receiver is the vault bridge token. + // The underying token balance is cached only when the receiver is vbToken. uint256 underlyingTokenBalanceBefore; if (receiver == address(this)) underlyingTokenBalanceBefore = $.underlyingToken.balanceOf(address(this)); uint256 yieldVaultSharesBalanceBefore = $.yieldVault.balanceOf(address(this)); @@ -1101,9 +1246,8 @@ abstract contract VaultBridgeToken is // Withdraw. uint256 burnedYieldVaultShares = $.yieldVault.withdraw(assets, receiver, address(this)); - // @remind Redocument. // Check the output. - // This code checks if the contract would go insolvent if the amount of the underlying token required to back the portion of the total supply (including the uncollected yield) not backed by the reserved assets were withdrawn at this exchange rate. + // This code checks if the contract would go insolvent if the amount of the underlying token required to back the portion of the total supply (including the uncollected yield) not backed by the reserved assets were withdrawn at this exchange rate. Allows slippage. require( Math.mulDiv( convertToAssets(originalTotalSupply + originalUncollectedYield) - originalReservedAssets, @@ -1114,7 +1258,7 @@ abstract contract VaultBridgeToken is ); // Calculate the withdrawn amount. - // The withdrawn amount is only calculated when the receiver is the vault bridge token. + // The withdrawn amount is only calculated when the receiver is vbToken. receivedAssets = receiver == address(this) ? ($.underlyingToken.balanceOf(address(this)) - underlyingTokenBalanceBefore) : 0; } @@ -1122,7 +1266,7 @@ abstract contract VaultBridgeToken is // -----================= ::: UNDERLYING TOKEN ::: =================----- /// @notice Transfers the underlying token from an external account to self. - /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/cross-entrancy vulnerabilities. + /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. function _receiveUnderlyingToken(address from, uint256 value) internal { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); @@ -1141,7 +1285,7 @@ abstract contract VaultBridgeToken is } /// @notice Transfers the underlying token to an external account. - /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/cross-entrancy vulnerabilities. + /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. function _sendUnderlyingToken(address to, uint256 value) internal { VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); @@ -1149,4 +1293,46 @@ abstract contract VaultBridgeToken is // @note IMPORTANT: Make sure the underlying token you are integrating does not enable reentrancy on `transfer`. $.underlyingToken.safeTransfer(to, value); } + + // -----================= ::: ADMIN ::: =================----- + + /// @notice Prevents usage of functions with the `whenNotPaused` modifier. + /// @notice This function can be called by a pauser only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function pause() external virtual { + PART2.delegatecall(abi.encodeWithSignature("pause()")); + } + + /// @notice Allows usage of functions with the `whenNotPaused` modifier. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function unpause() external virtual { + PART2.delegatecall(abi.encodeWithSignature("unpause()")); + } + + /// Method added by Certora + function getNetCollectedYield() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $._netCollectedYield; + } + + // -----================= ::: PART 2 ::: =================----- + + /// @notice Delegates the call to `VaultBridgeTokenPart2`. + function _delegateToPart2() private { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + address vaultBridgeTokenPart2 = $._vaultBridgeTokenPart2; + + assembly { + calldatacopy(0, 0, calldatasize()) + let success := delegatecall(gas(), vaultBridgeTokenPart2, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch success + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } } diff --git a/certora/scripts/munge.sh b/certora/scripts/munge.sh new file mode 100755 index 00000000..33512c4e --- /dev/null +++ b/certora/scripts/munge.sh @@ -0,0 +1 @@ +git apply ./certora/patches/VaultBridgeToken.patch \ No newline at end of file diff --git a/certora/scripts/run.sh b/certora/scripts/run.sh new file mode 100755 index 00000000..a6114c50 --- /dev/null +++ b/certora/scripts/run.sh @@ -0,0 +1,15 @@ +git apply ./certora/patches/VaultBridgeToken.patch +certoraRun certora/confs/GenericVaultBridgeToken.conf --msg erc4626 +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule netCollectedYieldAccounted --rule netCollectedYieldLimited --msg netCollectedYield +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule reserveBacked --msg reserveBacked +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule minimumReservePercentageLimit --msg minimumReservePercentageLimit +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule vaultBridgeTokenSolvency --rule vaultBridgeTokenSolvency_simple --msg vaultBridgeTokenSolvency +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule assetsMoreThanSupply --rule noSupplyIfNoAssets --msg assetsMoreThanSupply +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_invariants.spec --rule zeroAllowanceOnAssets --rule zeroAllowanceOnShares --msg zeroAllowance +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_allowedChanges.spec --msg allowedChanges +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GenericVaultBridgeToken_integrity.spec --msg integrity +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/GVBTBalances.spec --msg GVBTBalances +certoraRun certora/confs/GenericVaultBridgeToken.conf --verify GenericVaultBridgeToken:certora/specs/tokenMockBalances.spec --msg tokenMockBalances +certoraRun certora/confs/GenericNativeConverter.conf --msg converter +certoraRun certora/confs/base/MigrationManager.conf --rule onMsgReceived_doesntAlwaysRevert --msg onMsgReceived_doesntAlwaysRevert +git apply -R ./certora/patches/VaultBridgeToken.patch \ No newline at end of file diff --git a/certora/specs/CustomTokenBalances.spec b/certora/specs/CustomTokenBalances.spec new file mode 100644 index 00000000..8a064be4 --- /dev/null +++ b/certora/specs/CustomTokenBalances.spec @@ -0,0 +1,106 @@ +import "bridgeSummary.spec"; +import "GenericNativeConverter_helpers.spec"; + +methods { + function GenericCustomToken.balanceOf(address) external returns (uint256) envfree; + function GenericCustomToken.totalSupply() external returns (uint256) envfree; + function GenericCustomToken.transfer(address,uint256) external returns (bool); +} + +// Partial sum of balances. +// sumOfBalancesCustomToken[x] = \sum_{i=0}^{x-1} balances[i]; +ghost mapping(mathint => mathint) sumOfBalancesCustomToken { + init_state axiom forall mathint addr. sumOfBalancesCustomToken[addr] == 0; +} + +// ghost copy of balances +ghost mapping(address => uint256) ghost_balancesCustomToken { + init_state axiom forall address addr. ghost_balancesCustomToken[addr] == 0; +} + +hook Sload uint256 _balance (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0)[KEY address account].(offset 0) { + require ghost_balancesCustomToken[account] == _balance; +} + +hook Sstore (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0)[KEY address account].(offset 0) uint256 _balance (uint256 _balance_old) { + // update partial sums for x > to_mathint(account) + havoc sumOfBalancesCustomToken assuming + forall mathint x. sumOfBalancesCustomToken@new[x] == + sumOfBalancesCustomToken@old[x] + (to_mathint(account) < x ? _balance - _balance_old : 0); + ghost_balancesCustomToken[account] = _balance; +} + +// rules and invariant all hold + +invariant sumOfBalancesCustomTokenStartsAtZero() + sumOfBalancesCustomToken[0] == 0 + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + } +} + + +invariant sumOfBalancesCustomTokenGrowsCorrectly() + forall address addr. sumOfBalancesCustomToken[to_mathint(addr) + 1] == + sumOfBalancesCustomToken[to_mathint(addr)] + ghost_balancesCustomToken[addr] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + } +} + +invariant sumOfBalancesCustomTokenMonotone() + forall mathint i. forall mathint j. i <= j => sumOfBalancesCustomToken[i] <= sumOfBalancesCustomToken[j] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesCustomTokenStartsAtZero(); + requireInvariant sumOfBalancesCustomTokenGrowsCorrectly(); + } + } + +invariant sumOfBalancesCustomTokenEqualsTotalSupply() + sumOfBalancesCustomToken[2^160] == to_mathint(customTokenContract.totalSupply()) + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesCustomTokenStartsAtZero(); + requireInvariant sumOfBalancesCustomTokenGrowsCorrectly(); + requireInvariant sumOfBalancesCustomTokenMonotone(); + } + } + +rule twoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + requireLinking(); + requireInvariant sumOfBalancesCustomTokenStartsAtZero(); + requireInvariant sumOfBalancesCustomTokenGrowsCorrectly(); + requireInvariant sumOfBalancesCustomTokenMonotone(); + requireInvariant sumOfBalancesCustomTokenEqualsTotalSupply(); + uint256 balanceA = customTokenContract.balanceOf(accountA); + uint256 balanceB = customTokenContract.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(customTokenContract.totalSupply()); + satisfy(accountA != accountB && balanceA > 0 && balanceB > 0); +} + + +rule threeBalancesCannotExceedTotalSupply(address accountA, address accountB, address accountC) { + requireLinking(); + requireInvariant sumOfBalancesCustomTokenStartsAtZero(); + requireInvariant sumOfBalancesCustomTokenGrowsCorrectly(); + requireInvariant sumOfBalancesCustomTokenMonotone(); + requireInvariant sumOfBalancesCustomTokenEqualsTotalSupply(); + uint256 balanceA = customTokenContract.balanceOf(accountA); + uint256 balanceB = customTokenContract.balanceOf(accountB); + uint256 balanceC = customTokenContract.balanceOf(accountC); + + assert accountA != accountB && accountA != accountC && accountB != accountC => + balanceA + balanceB + balanceC <= to_mathint(customTokenContract.totalSupply()); + satisfy(accountA != accountB && balanceA + balanceB + balanceC > to_mathint(customTokenContract.totalSupply())); +} diff --git a/certora/specs/GVBTBalances.spec b/certora/specs/GVBTBalances.spec new file mode 100644 index 00000000..c4b555b0 --- /dev/null +++ b/certora/specs/GVBTBalances.spec @@ -0,0 +1,103 @@ +import "bridgeSummary.spec"; +import "GenericVaultBridgeToken_helpers.spec"; + +methods { + function GenericVaultBridgeToken.balanceOf(address) external returns (uint256) envfree; + function GenericVaultBridgeToken.totalSupply() external returns (uint256) envfree; + function GenericVaultBridgeToken.transfer(address,uint256) external returns (bool); +} + +// Partial sum of balances. +// sumOfBalancesGVBT[x] = \sum_{i=0}^{x-1} balances[i]; +ghost mapping(mathint => mathint) sumOfBalancesGVBT { + init_state axiom forall mathint addr. sumOfBalancesGVBT[addr] == 0; +} + +// ghost copy of balances +ghost mapping(address => uint256) ghost_balancesGVBT { + init_state axiom forall address addr. ghost_balancesGVBT[addr] == 0; +} + +hook Sload uint256 _balance (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0)[KEY address account].(offset 0) { + require ghost_balancesGVBT[account] == _balance; +} + +hook Sstore (slot 0x52c63247e1f47db19d5ce0460030c497f067ca4cebf71ba98eeadabe20bace00).(offset 0)[KEY address account].(offset 0) uint256 _balance (uint256 _balance_old) { + // update partial sums for x > to_mathint(account) + havoc sumOfBalancesGVBT assuming + forall mathint x. sumOfBalancesGVBT@new[x] == + sumOfBalancesGVBT@old[x] + (to_mathint(account) < x ? _balance - _balance_old : 0); + ghost_balancesGVBT[account] = _balance; +} + +invariant sumOfBalancesGVBTStartsAtZero() + sumOfBalancesGVBT[0] == 0 + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + } +} + +invariant sumOfBalancesGVBTGrowsCorrectly() + forall address addr. sumOfBalancesGVBT[to_mathint(addr) + 1] == + sumOfBalancesGVBT[to_mathint(addr)] + ghost_balancesGVBT[addr] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + } +} + +invariant sumOfBalancesGVBTMonotone() + forall mathint i. forall mathint j. i <= j => sumOfBalancesGVBT[i] <= sumOfBalancesGVBT[j] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesGVBTStartsAtZero(); + requireInvariant sumOfBalancesGVBTGrowsCorrectly(); + } + } + +invariant sumOfBalancesGVBTEqualsTotalSupply() + sumOfBalancesGVBT[2^160] == to_mathint(GenericVaultBridgeToken.totalSupply()) + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesGVBTStartsAtZero(); + requireInvariant sumOfBalancesGVBTGrowsCorrectly(); + requireInvariant sumOfBalancesGVBTMonotone(); + } + } + +rule twoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + requireLinking(); + requireInvariant sumOfBalancesGVBTStartsAtZero(); + requireInvariant sumOfBalancesGVBTGrowsCorrectly(); + requireInvariant sumOfBalancesGVBTMonotone(); + requireInvariant sumOfBalancesGVBTEqualsTotalSupply(); + uint256 balanceA = GenericVaultBridgeToken.balanceOf(accountA); + uint256 balanceB = GenericVaultBridgeToken.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(GenericVaultBridgeToken.totalSupply()); + satisfy(accountA != accountB && balanceA > 0 && balanceB > 0); +} + + +rule threeBalancesCannotExceedTotalSupply(address accountA, address accountB, address accountC) { + requireLinking(); + requireInvariant sumOfBalancesGVBTStartsAtZero(); + requireInvariant sumOfBalancesGVBTGrowsCorrectly(); + requireInvariant sumOfBalancesGVBTMonotone(); + requireInvariant sumOfBalancesGVBTEqualsTotalSupply(); + uint256 balanceA = GenericVaultBridgeToken.balanceOf(accountA); + uint256 balanceB = GenericVaultBridgeToken.balanceOf(accountB); + uint256 balanceC = GenericVaultBridgeToken.balanceOf(accountC); + + assert accountA != accountB && accountA != accountC && accountB != accountC => + balanceA + balanceB + balanceC <= to_mathint(GenericVaultBridgeToken.totalSupply()); + satisfy(accountA != accountB && balanceA + balanceB + balanceC > to_mathint(GenericVaultBridgeToken.totalSupply())); +} diff --git a/certora/specs/GenericNativeConverter_helpers.spec b/certora/specs/GenericNativeConverter_helpers.spec new file mode 100644 index 00000000..8da46d68 --- /dev/null +++ b/certora/specs/GenericNativeConverter_helpers.spec @@ -0,0 +1,15 @@ +import "MathSummaries.spec"; +import "GenericNativeConverter_methods.spec"; + +function requireLinking() +{ + require underlyingToken() == underlyingTokenContract; + require customToken() == customTokenContract; + require migrationManager() == migrationManagerContract; + require lxlyBridge() == ILxLyBridgeContract; +} + +definition excludedMethod(method f) returns bool = + f.isView || f.isFallback +; + diff --git a/certora/specs/GenericNativeConverter_invariants.spec b/certora/specs/GenericNativeConverter_invariants.spec new file mode 100644 index 00000000..2496f9b1 --- /dev/null +++ b/certora/specs/GenericNativeConverter_invariants.spec @@ -0,0 +1,86 @@ +import "bridgeSummary.spec"; +import "GenericNativeConverter_helpers.spec"; +import "./tokenMockBalances2.spec"; + +function requireAllInvariants() +{ + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + + requireInvariant converterSolvency(); + requireInvariant backingMoreThanSupply(); + requireInvariant nonMigratableBackingPercentageLT_E18(); + requireInvariant nonMigratableBackingAlwaysPresent(); +} + +// balance of underlying is at least the backingOnLayerY +invariant converterSolvency() + underlyingTokenContract.balanceOf(currentContract) >= backingOnLayerY() + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +// backing is at least customToken.TotalSupply minus bridged assets +invariant backingMoreThanSupply() + backingOnLayerY() >= require_uint256(customTokenContract.totalSupply() - totalBridged) + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +// non-migratable percentage is at most 10^18 (== 100%) +invariant nonMigratableBackingPercentageLT_E18() + nonMigratableBackingPercentage() <= 10^18 + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +// backingOnLayerY >= nonMigratableBacking, where +// nonMigratableBacking = customToken().totalSupply() * nonMigratableBackingPercentage / 10^18 +invariant nonMigratableBackingAlwaysPresent() + backingOnLayerY() * 10^18 >= customTokenContract.totalSupply() * nonMigratableBackingPercentage() + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +// backingOnLayerY >= nonMigratableBacking, where +// nonMigratableBacking = customToken().totalSupply() * nonMigratableBackingPercentage / 10^18 +// added +1 to cover for rounding errors. +invariant nonMigratableBackingAlwaysPresent_margin1() + (backingOnLayerY() + 1) * 10^18 >= customTokenContract.totalSupply() * nonMigratableBackingPercentage() + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// # helpers and miscellaneous ////////// +//////////////////////////////////////////////////////////////////////////////// + +function safeAssumptions(env e) +{ + require customTokenContract.balanceOf(currentContract) <= customTokenContract.totalSupply(); + require e.msg.sender != currentContract => + require_uint256(customTokenContract.balanceOf(currentContract) + customTokenContract.balanceOf(e.msg.sender)) + <= customTokenContract.totalSupply(); + require totalBridged == 0, "initial state of the ghost variable"; + + requireLinking(); + requireAllInvariants(); +} + diff --git a/certora/specs/GenericNativeConverter_methods.spec b/certora/specs/GenericNativeConverter_methods.spec new file mode 100644 index 00000000..0a148c6a --- /dev/null +++ b/certora/specs/GenericNativeConverter_methods.spec @@ -0,0 +1,37 @@ +using TokenMock as underlyingTokenContract; +using GenericCustomToken as customTokenContract; +using MigrationManager as migrationManagerContract; +using ILxLyBridgeMock as ILxLyBridgeContract; + +/* + Declaration of methods that are used in the rules. envfree indicate that + the method is not dependent on the environment (msg.value, msg.sender). + Methods that are not declared here are assumed to be dependent on env. +*/ +methods { + function customToken() external returns address envfree; + function underlyingToken() external returns address envfree; + function migrationManager() external returns address envfree; + function lxlyBridge() external returns address envfree; + + function lxlyId() external returns uint32 envfree; + function backingOnLayerY() external returns uint256 envfree; + function nonMigratableBackingPercentage() external returns uint256 envfree; + function customTokenContract.totalSupply() external returns uint256 envfree; + function customTokenContract.balanceOf(address account) external returns uint256 envfree; + + + function _.eip712Domain() external => NONDET DELETE; + + + // summarising to avoid the "call" in SafeERC20._callOptionalReturnBool + function _.forceApprove(address token, address spender, uint256 value) internal + => cvlForceApprove(executingContract, token, spender, value) expect void ALL; +} + +function cvlForceApprove(address sender, address token, address spender, uint256 value) +{ + env e; + require e.msg.sender == sender; + token.approve(e, spender, value); +} diff --git a/certora/specs/GenericVaultBridgeToken_ERC4626.spec b/certora/specs/GenericVaultBridgeToken_ERC4626.spec new file mode 100644 index 00000000..bb1741b5 --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_ERC4626.spec @@ -0,0 +1,262 @@ +import "GenericVaultBridgeToken_invariants.spec"; + +persistent ghost bool callMade; +persistent ghost bool delegatecallMade; + +hook CALL(uint g, address addr, uint value, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + if (addr != currentContract.asset() && // these are trusted contracts + addr != currentContract.yieldVault() && + addr != currentContract.lxlyBridge() && + addr != currentContract + ) + { + callMade = true; + } +} + +hook DELEGATECALL(uint g, address addr, uint argsOffset, uint argsLength, uint retOffset, uint retLength) uint rc { + if ( + addr != currentContract && + addr != VBTpart2) + { + delegatecallMade = true; + } +} + +// There are no dynamic calls to untrusted contracts. +rule noDynamicCalls(method f, env e) + filtered { f -> !excludedMethod(f) } +{ + requireLinking(); + require !callMade && !delegatecallMade; + + calldataarg args; + f(e, args); + + assert !callMade && !delegatecallMade; +} + +// convertTo{Aseets|Share}(0) == 0 +rule conversionOfZero { + uint256 convertZeroShares = convertToAssets(0); + uint256 convertZeroAssets = convertToShares(0); + + assert convertZeroShares == 0, + "converting zero shares must return zero assets"; + assert convertZeroAssets == 0, + "converting zero assets must return zero shares"; +} + +// convertToAssets(A) + convertToAssets(B) <= convertToAssets(A+B) +rule convertToAssetsWeakAdditivity() { + uint256 sharesA; uint256 sharesB; + require sharesA + sharesB < max_uint128 + && convertToAssets(sharesA) + convertToAssets(sharesB) < max_uint256 + && convertToAssets(require_uint256(sharesA + sharesB)) < max_uint256; + assert convertToAssets(sharesA) + convertToAssets(sharesB) <= convertToAssets(require_uint256(sharesA + sharesB)), + "converting sharesA and sharesB to assets then summing them must yield a smaller or equal result to summing them then converting"; +} + +// convertToShares(A) + convertToShares(B) <= convertToShares(A+B) +rule convertToSharesWeakAdditivity() { + uint256 assetsA; uint256 assetsB; + require assetsA + assetsB < max_uint128 + && convertToAssets(assetsA) + convertToAssets(assetsB) < max_uint256 + && convertToAssets(require_uint256(assetsA + assetsB)) < max_uint256; + assert convertToAssets(assetsA) + convertToAssets(assetsB) <= convertToAssets(require_uint256(assetsA + assetsB)), + "converting assetsA and assetsB to shares then summing them must yield a smaller or equal result to summing them then converting"; +} + +// A < B => convertToAssets(A) <= convertToAssets(B) and the same for convertToShares +rule conversionWeakMonotonicity { + uint256 smallerShares; uint256 largerShares; + uint256 smallerAssets; uint256 largerAssets; + + assert smallerShares < largerShares => convertToAssets(smallerShares) <= convertToAssets(largerShares), + "converting more shares must yield equal or greater assets"; + assert smallerAssets < largerAssets => convertToShares(smallerAssets) <= convertToShares(largerAssets), + "converting more assets must yield equal or greater shares"; +} + +// convertToShares(convertToAssets(X)) <= X and also the other order +rule conversionWeakIntegrity() { + uint256 sharesOrAssets; + assert convertToShares(convertToAssets(sharesOrAssets)) <= sharesOrAssets, + "converting shares to assets then back to shares must return shares less than or equal to the original amount"; + assert convertToAssets(convertToShares(sharesOrAssets)) <= sharesOrAssets, + "converting assets to shares then back to assets must return assets less than or equal to the original amount"; +} + +// A < B => deposit(A) gives less or equal shares than deposit(B) +rule depositMonotonicity(env e) +{ + storage start = lastStorage; + + uint256 smallerAssets; uint256 largerAssets; + address receiver; + require currentContract != e.msg.sender && currentContract != receiver; + + safeAssumptions(e); + + deposit(e, smallerAssets, receiver); + uint256 smallerShares = balanceOf(receiver) ; + + deposit(e, largerAssets, receiver) at start; + uint256 largerShares = balanceOf(receiver) ; + + assert smallerAssets < largerAssets => smallerShares <= largerShares, + "when supply tokens outnumber asset tokens, a larger deposit of assets must produce an equal or greater number of shares"; +} + +// deposit(x) == 0 <=> x == 0 +rule zeroDepositZeroShares(env e) +{ + uint assets; + address receiver; + uint shares = deposit(e, assets, receiver); + assert shares == 0 <=> assets == 0; +} + +// address of asset() never changes +rule underlyingCannotChange(method f, env e) +filtered { + f -> !excludedMethod(f) + } +{ + safeAssumptions(e); + address originalAsset = asset(); + + calldataarg args; + f(e, args); + + address newAsset = asset(); + + assert originalAsset == newAsset, + "the underlying asset of a contract must not change"; +} + +// redeem(deposit(x)) doesn't decrease balance of the contract +rule dustFavorsTheHouse(env e) +{ + safeAssumptions(e); + uint assetsIn; + uint256 totalSupplyBefore = totalSupply(); + + uint balanceBefore = require_uint256(ERC20a.balanceOf(currentContract) + stakedAssets()); + + uint shares = deposit(e, assetsIn, e.msg.sender); + uint assetsOut = redeem(e, shares, e.msg.sender, e.msg.sender); + + uint balanceAfter = require_uint256(ERC20a.balanceOf(currentContract) + stakedAssets()); + + assert balanceAfter >= balanceBefore; +} + +// After redeeming the entire balance, the user's balance is zero +rule redeemingAllValidity(env e) { + address owner; + uint256 shares; require shares == balanceOf(owner); + + safeAssumptions(e); + redeem(e, shares, _, owner); + uint256 ownerBalanceAfter = balanceOf(owner); + assert ownerBalanceAfter == 0; +} + +// +rule contributingProducesShares(env e, method f) +filtered { + f -> f.selector == sig:deposit(uint256,address).selector + || f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector + || f.selector == sig:depositWithPermit(uint256,address,bytes).selector + || f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector + || f.selector == sig:mint(uint256,address).selector +} +{ + uint256 assets; uint256 shares; + address contributor = e.msg.sender; + address receiver; + require currentContract != contributor + && currentContract != receiver + && yieldVaultContract != contributor + && yieldVaultContract != receiver; + + safeAssumptions(e); + mathint totalBridgedBefore = totalBridged; + + uint256 contributorAssetsBefore = userAssets(contributor); + uint256 receiverSharesBefore = balanceOf(receiver); + + callContributionMethods(e, f, assets, shares, receiver); + + uint256 contributorAssetsAfter = userAssets(contributor); + uint256 receiverSharesAfter = balanceOf(receiver); + mathint totalBridgedAfter = totalBridged; + + assert contributorAssetsBefore > contributorAssetsAfter <=> + (receiverSharesBefore < receiverSharesAfter || + totalBridgedBefore < totalBridgedAfter); +} + +rule onlyContributionMethodsReduceAssets(env e, method f) + filtered { f -> !excludedMethod(f) } +{ + // user CAN be msg.sender + address user; + require user != currentContract; + require user != yieldVaultContract; + + safeAssumptions(e); + + uint256 userAssetsBefore = userAssets(user); + + calldataarg args; + f(e, args); + + uint256 userAssetsAfter = userAssets(user); + + assert userAssetsBefore > userAssetsAfter => + ( + f.selector == sig:deposit(uint256,address).selector || + f.selector == sig:mint(uint256,address).selector || + f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector || + f.selector == sig:depositWithPermit(uint256,address,bytes).selector || + f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector || + f.contract == ERC20a || + // these methods also send away assets on purpose + f.selector == sig:donateForCompletingMigration(uint256).selector || + f.selector == sig:donateAsYield(uint256).selector || + f.selector == sig:completeMigration(uint32,uint256,uint256).selector + ), + "a user's assets must not go down except on calls to contribution methods or calls directly to the asset."; +} + +rule reclaimingProducesAssets(env e, method f) +filtered { + f -> f.selector == sig:withdraw(uint256,address,address).selector + || f.selector == sig:redeem(uint256,address,address).selector + || f.selector == sig:claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes).selector +} +{ + uint256 assets; uint256 shares; + address receiver; address owner; + + require currentContract != e.msg.sender + && currentContract != receiver + && currentContract != owner + && yieldVaultContract != receiver + && yieldVaultContract != owner; + + safeAssumptions(e); + + uint256 ownerSharesBefore = balanceOf(owner); + uint256 receiverAssetsBefore = userAssets(receiver); + + callReclaimingMethods(e, f, assets, shares, receiver, owner); + + uint256 ownerSharesAfter = balanceOf(owner); + uint256 receiverAssetsAfter = userAssets(receiver); + + assert ownerSharesBefore > ownerSharesAfter <=> receiverAssetsBefore < receiverAssetsAfter, + "an owner's shares must decrease if and only if the receiver's assets increase"; +} diff --git a/certora/specs/GenericVaultBridgeToken_allowedChanges.spec b/certora/specs/GenericVaultBridgeToken_allowedChanges.spec new file mode 100644 index 00000000..a142ec41 --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_allowedChanges.spec @@ -0,0 +1,113 @@ +import "GenericVaultBridgeToken_ERC4626.spec"; + +rule onlyAllowedMethodsMayChangeTotalAssets(method f, env e) + filtered { f -> !excludedMethod(f) && + f.selector != sig:performReversibleYieldVaultDeposit(uint256).selector // not supposed to be called directly + } +{ + safeAssumptions(e); + uint256 totalAssetsBefore = totalAssets(); + + calldataarg args; + f(e, args); + + uint256 totalAssetsAfter = totalAssets(); + assert totalAssetsAfter > totalAssetsBefore => canIncreaseTotalAssets(f); + assert totalAssetsAfter < totalAssetsBefore => canDecreaseTotalAssets(f); +} + +definition canDecreaseTotalAssets(method f) returns bool = + f.selector == sig:claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes).selector || + f.selector == sig:redeem(uint256,address,address).selector || + f.selector == sig:withdraw(uint256,address,address).selector; + +definition canIncreaseTotalAssets(method f) returns bool = + f.selector == sig:deposit(uint256,address).selector || + f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector || + f.selector == sig:depositWithPermit(uint256,address,bytes).selector || + f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector || + f.selector == sig:donateAsYield(uint256).selector || + f.selector == sig:completeMigration(uint32,uint256,uint256).selector || + f.selector == sig:mint(uint256,address).selector; + +rule onlyAllowedMethodsMayChangeTotalSupply(method f, env e) + filtered {f -> !excludedMethod(f) } +{ + safeAssumptions(e); + + uint256 totalSupplyBefore = totalSupply(); + calldataarg args; + f(e, args); + + uint256 totalSupplyAfter = totalSupply(); + assert totalSupplyAfter > totalSupplyBefore => canIncreaseTotalSupply(f); + assert totalSupplyAfter < totalSupplyBefore => canDecreaseTotalSupply(f); +} + +definition canDecreaseTotalSupply(method f) returns bool = + f.selector == sig:claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes).selector || + f.selector == sig:redeem(uint256,address,address).selector || + f.selector == sig:burn(uint256).selector || + f.selector == sig:completeMigration(uint32,uint256,uint256).selector || + + f.selector == sig:withdraw(uint256,address,address).selector; + +definition canIncreaseTotalSupply(method f) returns bool = + f.selector == sig:deposit(uint256,address).selector || + f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector || + f.selector == sig:depositWithPermit(uint256,address,bytes).selector || + f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector || + f.selector == sig:collectYield().selector || + f.selector == sig:setYieldRecipient(address).selector || + f.selector == sig:completeMigration(uint32, uint256, uint256).selector || + f.selector == sig:mint(uint256,address).selector; + +rule onlyAllowedMethodsMayChangeStakedAssets(method f, env e) + filtered {f -> !excludedMethod(f) } +{ + safeAssumptions(e); + + uint256 stakedAssetsBefore = stakedAssets(); + calldataarg args; + f(e, args); + + uint256 stakedAssetsAfter = stakedAssets(); + assert stakedAssetsAfter > stakedAssetsBefore => canIncreaseStakedAssets(f); + assert stakedAssetsAfter < stakedAssetsBefore => canDecreaseStakedAssets(f); +} + +definition canDecreaseStakedAssets(method f) returns bool = + f.selector == sig:claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes).selector || + f.selector == sig:redeem(uint256,address,address).selector || + f.selector == sig:withdraw(uint256,address,address).selector || + f.selector == sig:rebalanceReserve().selector; + +definition canIncreaseStakedAssets(method f) returns bool = + f.selector == sig:deposit(uint256,address).selector || + f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector || + f.selector == sig:depositWithPermit(uint256,address,bytes).selector || + f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector || + f.selector == sig:performReversibleYieldVaultDeposit(uint256).selector || + f.selector == sig:completeMigration(uint32, uint256, uint256).selector || + f.selector == sig:mint(uint256,address).selector|| + f.selector == sig:rebalanceReserve().selector; + +rule onlyAllowedMethodsMayChangeMigrationFeesFund(method f, env e) + filtered {f -> !excludedMethod(f) } +{ + safeAssumptions(e); + + uint256 fundBefore = migrationFeesFund(); + calldataarg args; + f(e, args); + + uint256 fundAfter = migrationFeesFund(); + assert fundAfter > fundBefore => canIncreaseMigrationFeesFund(f); + assert fundAfter < fundBefore => canDecreaseMigrationFeesFund(f); +} + +definition canDecreaseMigrationFeesFund(method f) returns bool = + f.selector == sig:completeMigration(uint32, uint256, uint256).selector; + +definition canIncreaseMigrationFeesFund(method f) returns bool = + f.selector == sig:donateForCompletingMigration(uint256).selector; diff --git a/certora/specs/GenericVaultBridgeToken_helpers.spec b/certora/specs/GenericVaultBridgeToken_helpers.spec new file mode 100644 index 00000000..16f9c72c --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_helpers.spec @@ -0,0 +1,105 @@ +import "MathSummaries.spec"; +import "GenericVaultBridgeToken_methods.spec"; + +using VaultMock as yieldVaultContract; + +function userAssets(address a) returns uint256 +{ + return ERC20a.balanceOf(a); +} + +function requireLinking() +{ + require yieldVault() == yieldVaultContract; + require yieldVaultContract.asset() == ERC20a; + require asset() == ERC20a; + + require lxlyBridge() == ILxLyBridgeMock; +} + +function callContributionMethods(env e, method f, uint256 assets, uint256 shares, address receiver) { + if (f.selector == sig:deposit(uint256,address).selector) { + deposit(e, assets, receiver); + } + if (f.selector == sig:depositAndBridge(uint256,address,uint32,bool).selector) { + uint32 destinationNetworkId; + bool forceUpdateGlobalExitRoot; + depositAndBridge(e, assets, receiver, destinationNetworkId, forceUpdateGlobalExitRoot); + } + if (f.selector == sig:depositWithPermit(uint256,address,bytes).selector) { + bytes permitData; + depositWithPermit(e, assets, receiver, permitData); + } + if (f.selector == sig:depositWithPermitAndBridge(uint256,address,uint32,bool,bytes).selector) { + uint32 destinationNetworkId; + bool forceUpdateGlobalExitRoot; + bytes permitData; + depositWithPermitAndBridge(e, assets, receiver, destinationNetworkId, forceUpdateGlobalExitRoot, permitData); + } + if (f.selector == sig:mint(uint256,address).selector) { + mint(e, shares, receiver); + } +} + +function callReclaimingMethods(env e, method f, uint256 assets, uint256 shares, address receiver, address owner) { + if (f.selector == sig:withdraw(uint256,address,address).selector) { + withdraw(e, assets, receiver, owner); + } + if (f.selector == sig:redeem(uint256,address,address).selector) { + redeem(e, shares, receiver, owner); + } + if (f.selector == sig:claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes).selector) { + bytes32[32] smtProofLocalExitRoot; + bytes32[32] smtProofRollupExitRoot; + uint256 globalIndex; + bytes32 mainnetExitRoot; + bytes32 rollupExitRoot; + bytes metadata; + claimAndRedeem(e, + smtProofLocalExitRoot, + smtProofRollupExitRoot, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + owner, shares, receiver, metadata); + } +} + +function requireNonSceneSender(env e) +{ + require e.msg.sender != GenericVaultBridgeToken; + require !hasRole(e, DEFAULT_ADMIN_ROLE(e), e.msg.sender); + require e.msg.sender != yieldVaultContract; +} + +function isPrivilegedSender(env e) returns bool +{ + return e.msg.sender == GenericVaultBridgeToken || + hasRole(e, DEFAULT_ADMIN_ROLE(e), e.msg.sender) || + hasRole(e, PAUSER_ROLE(e), e.msg.sender) || + hasRole(e, REBALANCER_ROLE(e), e.msg.sender) || + hasRole(e, YIELD_COLLECTOR_ROLE(e), e.msg.sender) || + e.msg.sender == yieldRecipient() + ; +} + +definition canBeCalledWhenPaused(method f) returns bool = + //f.selector == sig:initialize(address,address,string,string,address,uint256,address,address,address,uint256,address,uint256).selector || + f.selector == sig:burn(uint256).selector || + f.selector == sig:grantRole(bytes32,address).selector || + f.selector == sig:revokeRole(bytes32,address).selector || + f.selector == sig:renounceRole(bytes32,address).selector || + f.selector == sig:donateAsYield(uint256).selector +; + +definition excludedMethod(method f) returns bool = + f.isView || f.isFallback || + f.selector == sig:initialize(address, VaultBridgeToken.InitializationParameters).selector || + + // harness methods + f.selector == sig:simulateWithdraw_harness(uint256,bool).selector || + f.selector == sig:rebalanceReserve_harness(bool,bool).selector || + f.selector == sig:depositIntoYieldVault_harness(uint256,bool).selector + +; + diff --git a/certora/specs/GenericVaultBridgeToken_integrity.spec b/certora/specs/GenericVaultBridgeToken_integrity.spec new file mode 100644 index 00000000..f802d1f4 --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_integrity.spec @@ -0,0 +1,112 @@ +import "GenericVaultBridgeToken_ERC4626.spec"; + +// Only allowed methods may be called when paused +// rule noActivityWhenPaused(method f, env e) +// filtered {f -> !excludedMethod(f) } +// { +// requireLinking(); +// bool paused = paused(); +// calldataarg args; +// f@withrevert(e, args); +// bool reverted = lastReverted; +// assert paused => (reverted || isPrivilegedSender(e) || canBeCalledWhenPaused(f)); +// } + +//_simulateWithdraw(x, true) == x or revert +rule integrityOf_simulateWithdraw_force(env e) +{ + uint256 assets; + uint256 res = simulateWithdraw_harness(e, assets, true); + assert res == assets; +} + +// reservedAssets >= minimumReservedAssets +// where minimumReservedAssets = minimumReservePercentage * totalSupply / 10^18 +function isBalanced() returns bool +{ + return reservedAssets() * 10^18 >= minimumReservePercentage() * totalSupply(); +} + +// After calling rebalanceReserve, the reservedAssets >= minimumReservedAssets +rule integrityOfRebalance(env e) +{ + safeAssumptions(e); + uint256 assetsBefore = totalAssets(); + rebalanceReserve_harness(e, false, false); + uint256 assetsAfter = totalAssets(); + assert isBalanced(); + assert assetsBefore == assetsAfter; +} + +// same as isBalanced but we add "+ 1" as a margin for rounding errors +function isBalanced_margin1() returns bool +{ + return (reservedAssets() + 1) * 10^18 >= minimumReservePercentage() * totalSupply(); +} + +// After calling rebalanceReserve, the reservedAssets + 1 >= minimumReservedAssets +rule integrityOfRebalance_margin1(env e) +{ + safeAssumptions(e); + uint256 assetsBefore = totalAssets(); + rebalanceReserve_harness(e, false, false); + uint256 assetsAfter = totalAssets(); + assert isBalanced_margin1(); + assert assetsBefore == assetsAfter; +} + +// nonDeposited = depositIntoYieldVault(assets) then nonDeposited <= assets +rule integrityOf_depositIntoYieldVault(env e) +{ + safeAssumptions(e); + uint assets; bool exact; + uint nonDeposited = depositIntoYieldVault_harness(e, assets, exact); + assert nonDeposited <= assets; +} + +rule previewRedeemCorrectness_strict(env e, address receiver) +{ + requireNonSceneSender(e); + requireLinking(); + + uint256 shares; + uint256 assetsReported = previewRedeem(shares); + uint256 assetsReceived = redeem(e, shares, receiver, e.msg.sender); + + assert assetsReported == assetsReceived; +} + +rule previewWithdrawCorrectness_strict(env e, address receiver) +{ + requireNonSceneSender(e); + requireLinking(); + + uint256 assets; + uint256 sharesReported = previewWithdraw(assets); + uint256 sharesPaid = withdraw(e, assets, receiver, e.msg.sender); + assert sharesPaid == sharesReported; +} + +rule previewMintCorrectness_strict(env e, address receiver) +{ + requireNonSceneSender(e); + requireLinking(); + + uint256 shares; + uint256 assetsReported = previewMint(shares); + uint256 assetsPaid = mint(e, shares, receiver); + + assert assetsReported == assetsPaid; +} + +rule previewDepositCorrectness_strict(env e, address receiver) +{ + requireNonSceneSender(e); + requireLinking(); + + uint256 assets; + uint256 sharesReported = previewDeposit(assets); + uint256 sharesReceived = deposit(e, assets, receiver); + + assert sharesReported == sharesReceived; +} \ No newline at end of file diff --git a/certora/specs/GenericVaultBridgeToken_invariants.spec b/certora/specs/GenericVaultBridgeToken_invariants.spec new file mode 100644 index 00000000..bfd76966 --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_invariants.spec @@ -0,0 +1,167 @@ +import "bridgeSummary.spec"; +import "GenericVaultBridgeToken_helpers.spec"; +import "./tokenMockBalances.spec"; +import "./GVBTBalances.spec"; + +function requireAllInvariants() +{ + requireInvariant sumOfBalancesGVBTStartsAtZero(); + requireInvariant sumOfBalancesGVBTGrowsCorrectly(); + requireInvariant sumOfBalancesGVBTMonotone(); + requireInvariant sumOfBalancesGVBTEqualsTotalSupply(); + + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + + address user; + requireInvariant zeroAllowanceOnAssets(user); + requireInvariant zeroAllowanceOnShares(user); + requireInvariant reserveBacked(); + requireInvariant minimumReservePercentageLimit(); + requireInvariant assetsMoreThanSupply(); + requireInvariant noSupplyIfNoAssets(); + requireInvariant vaultBridgeTokenSolvency_simple(); + uint256 assets; + requireInvariant vaultBridgeTokenSolvency(assets); + requireInvariant netCollectedYieldAccounted(); + requireInvariant netCollectedYieldLimited(); +} + +invariant netCollectedYieldAccounted() + getNetCollectedYield() <= balanceOf(yieldRecipient()) + filtered { f -> !excludedMethod(f) && + f.selector != sig:setYieldRecipient(address).selector && // the admin method that's allowed to break this + f.selector != sig:burn(uint256).selector // this gives sanity issue because of the require e.msg.sender != yieldRecipient() + } + { + preserved with (env e) { + require e.msg.sender != yieldRecipient(), "yieldRecepient is allowed to break this"; + require allowance(yieldRecipient(), e.msg.sender) == 0, "allowed user manipulating yieldRecepient's balance can also break this"; + safeAssumptions(e); + } +} + +invariant netCollectedYieldLimited() + getNetCollectedYield() <= totalSupply() + filtered { f -> !excludedMethod(f) && + f.selector != sig:burn(uint256).selector // this gives sanity issue because of the require e.msg.sender != yieldRecipient() + } + { + preserved with (env e) { + require e.msg.sender != yieldRecipient(), "yieldRecepient is allowed to break this"; + require allowance(yieldRecipient(), e.msg.sender) == 0, "allowed user manipulating yieldRecepient's balance can also break this"; + safeAssumptions(e); + } +} + +invariant reserveBacked() + ERC20a.balanceOf(GenericVaultBridgeToken) >= require_uint256(reservedAssets() + migrationFeesFund()) + filtered { f -> !excludedMethod(f) && + f.selector != sig:performReversibleYieldVaultDeposit(uint256).selector // not supposed to be called directly + } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant minimumReservePercentageLimit() + minimumReservePercentage() <= 10^18 + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant vaultBridgeTokenSolvency_simple() + totalAssets() >= convertToAssets(totalSupply()) + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +// the formula we're verifying: +// Math.mulDiv(convertToAssets(totalSupply() + yield()) - reservedAssets(), burnedYieldVaultShares, assets) +// <= Math.mulDiv(yieldVault.balanceOf(address(this)), 1e18 + $.yieldVaultMaximumSlippagePercentage, 1e18); +// where burnedYieldVaultShares = $.yieldVault.withdraw(assets, receiver, address(this)); + +// this can be rewritten to get rid of the divisions +// afterwards, we cancel out the terms +// assets and yieldVaultContract.withdraw(assets, GenericVaultBridgeToken, GenericVaultBridgeToken) +// we know that yieldVaultContract.withdraw(assets,..) <= assets so by canceling out we can only make the rule stronger +invariant vaultBridgeTokenSolvency(uint assets) + (convertToAssets(require_uint256(totalSupply() + yield())) - reservedAssets()) + //* yieldVaultContract.withdraw(assets, GenericVaultBridgeToken, GenericVaultBridgeToken) + * 10^18 + <= + yieldVaultContract.balanceOf(GenericVaultBridgeToken) + * (10^18 + yieldVaultMaximumSlippagePercentage()) + //* assets + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant assetsMoreThanSupply() + totalAssets() >= totalSupply() + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant noSupplyIfNoAssets() + totalAssets() == 0 => totalSupply() == 0 + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant zeroAllowanceOnAssets(address user) + ERC20a.allowance(currentContract, user) == 0 || user == yieldVault() + filtered { f -> !excludedMethod(f) } + { + preserved with (env e) { + safeAssumptions(e); + } +} + +invariant zeroAllowanceOnShares(address user) + GenericVaultBridgeToken.allowance(currentContract, user) == 0 + filtered { f -> !excludedMethod(f) && + f.selector != sig:permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector && + f.selector != sig:performReversibleYieldVaultDeposit(uint256).selector // this gives sanity issue because of the require e.msg.sender != GenericVaultBridgeToken; + } + { + preserved with (env e) { + require e.msg.sender != GenericVaultBridgeToken; // the contract itself could provide allowance + safeAssumptions(e); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// # helpers and miscellaneous ////////// +//////////////////////////////////////////////////////////////////////////////// + +function safeAssumptions(env e) { + requireLinking(); + requireAllInvariants(); + require currentContract != asset(); // Although this is not disallowed, we assume the contract's underlying asset is not the contract itself +} + + + + + + + diff --git a/certora/specs/GenericVaultBridgeToken_methods.spec b/certora/specs/GenericVaultBridgeToken_methods.spec new file mode 100644 index 00000000..4ed27f1d --- /dev/null +++ b/certora/specs/GenericVaultBridgeToken_methods.spec @@ -0,0 +1,71 @@ +using TokenMock as ERC20a; +using GenericVaultBridgeToken as GenericVaultBridgeToken; +using ILxLyBridgeMock as ILxLyBridgeMock; +using VaultBridgeTokenPart2 as VBTpart2; + +/* + Declaration of methods that are used in the rules. envfree indicate that + the method is not dependent on the environment (msg.value, msg.sender). + Methods that are not declared here are assumed to be dependent on env. +*/ +methods { + function name() external returns string envfree; + function symbol() external returns string envfree; + function decimals() external returns uint8 envfree; + function asset() external returns address envfree; + function VaultMock.asset() external returns address envfree; + + function totalSupply() external returns uint256 envfree; + function balanceOf(address) external returns uint256 envfree; + function nonces(address) external returns uint256 envfree; + function totalAssets() external returns uint256 envfree; + function convertToShares(uint256) external returns uint256 envfree; + function convertToAssets(uint256) external returns uint256 envfree; + function previewDeposit(uint256) external returns uint256 envfree; + function previewMint(uint256) external returns uint256 envfree; + function previewWithdraw(uint256) external returns uint256 envfree; + function previewRedeem(uint256) external returns uint256 envfree; + function maxDeposit(address) external returns uint256 envfree; + function maxMint(address) external returns uint256 envfree; + function maxWithdraw(address) external returns uint256 envfree; + function maxRedeem(address) external returns uint256 envfree; + function yieldVault() external returns (address) envfree; + function yieldRecipient() external returns (address) envfree; + function lxlyBridge() external returns (address) envfree; + + function stakedAssets() external returns (uint256) envfree; + function yield() external returns (uint256) envfree; + function getNetCollectedYield() external returns (uint256) envfree; + function reservedAssets() external returns (uint256) envfree; + function paused() external returns (bool) envfree; + + function reservePercentage() external returns (uint256) envfree; + function minimumReservePercentage() external returns (uint256) envfree; + function minimumYieldVaultDeposit() external returns (uint256) envfree; + function yieldVaultMaximumSlippagePercentage() external returns (uint256) envfree; + function migrationFeesFund() external returns (uint256) envfree; + function GenericVaultBridgeToken.allowance(address, address) external returns uint256 envfree; + + //// #ERC20 methods + function _.balanceOf(address) external => DISPATCHER(true); + function _.transfer(address,uint256) external => DISPATCHER(true); + function _.transferFrom(address,address,uint256) external => DISPATCHER(true); + + function ERC20a.balanceOf(address) external returns uint256 envfree; + function ERC20a.allowance(address, address) external returns uint256 envfree; + function ERC20a.totalSupply() external returns uint256 envfree; + + function yieldVaultContract.balanceOf(address) external returns uint256 envfree; + function GenericVaultBridgeToken.eip712Domain() external returns (bytes1, string, string, uint256, address, bytes32, uint256[]) => NONDET DELETE; + + // summarising to avoid the "call" in SafeERC20._callOptionalReturnBool + function _.forceApprove(address token, address spender, uint256 value) internal + => cvlForceApprove(executingContract, token, spender, value) expect void ALL; +} + +function cvlForceApprove(address sender, address token, address spender, uint256 value) +{ + env e; + require e.msg.sender == sender; + token.approve(e, spender, value); +} diff --git a/certora/specs/MathSummaries.spec b/certora/specs/MathSummaries.spec new file mode 100644 index 00000000..e6b222e0 --- /dev/null +++ b/certora/specs/MathSummaries.spec @@ -0,0 +1,55 @@ +// safe summaries to enhance performance + +methods { + // lib/openzeppelin-contracts/contracts/utils/math/Math.sol + function _.mulDiv(uint256 x, uint256 y, uint256 denominator) internal => cvlMulDiv(x, y, denominator) expect uint256; + function _.mulDiv(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) internal => cvlMulDivWithRounding(x, y, denominator, rounding) expect uint256; + function _.sqrt(uint256 a) internal => cvlSqrt(a) expect uint256; + function _.average(uint256 a, uint256 b) internal => cvlAverage(a, b) expect uint256; + function _.ternary(bool condition, uint256 a, uint256 b) internal => cvlTernary(condition, a, b) expect uint256; + function _.zeroFloorSub(uint256 x, uint256 y) internal => cvlZeroFloorSub(x, y) expect (uint256); + +} + +function cvlZeroFloorSub(uint256 x, uint256 y) returns uint256 { + if (x > y) return require_uint256(x - y); + else return 0; +} + +function cvlMulDiv(uint256 x, uint256 y, uint256 denominator) returns uint256 { + require denominator > 0; + mathint res = x * y / denominator; + return require_uint256(res); +} + +function cvlMulDivWithRounding(uint256 x, uint256 y, uint256 denominator, Math.Rounding rounding) returns uint256 +{ + require denominator > 0; + if (rounding == Math.Rounding.Ceil) { + return require_uint256((x * y + denominator - 1) / denominator); + } + if (rounding == Math.Rounding.Floor) { + return require_uint256((x * y) / denominator); + } + else { + assert false; //add other branches if different rounding type is used + return 0; + } +} + +function cvlSqrt(uint256 a) returns uint256 { + mathint a_mathint = to_mathint(a); + uint256 sqrt_a; + require + sqrt_a * sqrt_a <= a_mathint && + (sqrt_a + 1) * (sqrt_a + 1) > a_mathint; + return sqrt_a; +} + +function cvlAverage(uint256 a, uint256 b) returns uint256 { + return require_uint256((a + b) / 2); +} + +function cvlTernary(bool condition, uint256 a, uint256 b) returns uint256 { + return condition ? a : b; +} \ No newline at end of file diff --git a/certora/specs/MigrationManager-generic.spec b/certora/specs/MigrationManager-generic.spec new file mode 100644 index 00000000..8fc9e3ae --- /dev/null +++ b/certora/specs/MigrationManager-generic.spec @@ -0,0 +1,43 @@ +import "dispatchingMigrationManager.spec"; + +using GenericVaultBridgeToken as GenericVaultBridgeToken; +using VaultBridgeTokenPart2 as VaultBridgeTokenPart2; + +methods { + function _.completeMigration(uint32 o, uint256 s, uint256 a) external with(env e) => CVL_completeMigration(e, o, s, a) expect void; + function GenericVaultBridgeToken.underlyingToken() external returns (address) envfree; + function _.underlyingToken() external => CVL_underlyingToken() expect (address); +} + +function CVL_completeMigration(env e, uint32 o, uint256 s, uint256 a) { + VaultBridgeTokenPart2.completeMigration(e, o, s, a); +} +function CVL_underlyingToken() returns address { + return GenericVaultBridgeToken.underlyingToken(); +} + +use builtin rule sanity filtered { f -> + f.contract == currentContract && + f.selector != sig:configureNativeConverters(uint32[],address[],address).selector +} + +rule sanity_configureNativeConverters() { + env e; + + uint32[] layerYLxlyIds; + address[] nativeConverters; + address vbToken = GenericVaultBridgeToken; + + require(layerYLxlyIds.length > 0); + + configureNativeConverters(e, layerYLxlyIds, nativeConverters, vbToken); + + satisfy(true); +} + +rule onMsgReceived_doesntAlwaysRevert(env e) +{ + address originAddress; uint32 originNetwork; bytes data; + onMessageReceived(e, originAddress, originNetwork, data); + satisfy true; +} diff --git a/certora/specs/bridgeSummary.spec b/certora/specs/bridgeSummary.spec new file mode 100644 index 00000000..6af56684 --- /dev/null +++ b/certora/specs/bridgeSummary.spec @@ -0,0 +1,30 @@ +methods { + function _.bridgeAsset(uint32,address,uint256 assets ,address,bool,bytes) external => bridge_cvl(assets) expect void; // this is UNSAFE summary. + + function _.claimAsset( + bytes32[32], bytes32[32], uint256, bytes32, bytes32, + uint32, address, uint32, address, uint256, bytes) external => emptyFunc() expect void; // this is UNSAFE summary. + + function _.bridgeMessage(uint32,address,bool,bytes) external => emptyFunc() expect void; // this is UNSAFE summary. + + //other unresolved calls: + function _._permit(address, uint256, bytes calldata) internal => NONDET; // this is UNSAFE summary. + + unresolved external in _.claimAndRedeem(bytes32[32],bytes32[32],uint256,bytes32,bytes32,address,uint256,address,bytes) => DISPATCH [ + + ] default NONDET; +} + +ghost mathint totalBridged +{ + init_state axiom totalBridged == 0; +} + +function bridge_cvl(uint assets) +{ + totalBridged = totalBridged + assets; +} + +function emptyFunc() +{ +} \ No newline at end of file diff --git a/certora/specs/dispatchingBridge.spec b/certora/specs/dispatchingBridge.spec new file mode 100644 index 00000000..ff398e46 --- /dev/null +++ b/certora/specs/dispatchingBridge.spec @@ -0,0 +1,15 @@ +methods { + // to bridge + function _.networkID() external => NONDET; + function _.gasTokenAddress() external => NONDET; + function _.gasTokenNetwork() external => NONDET; + function _.bridgeAsset(uint32,address,uint256,address,bool,bytes) external => NONDET; + function _.claimAsset(bytes32[32],bytes32[32],uint256,bytes32,bytes32,uint32,address,uint32,address,uint256,bytes) external => NONDET; + function _.claimMessage(bytes32[32],bytes32[32],uint256,bytes32,bytes32,uint32,address,uint32,address,uint256,bytes) external => NONDET; + function _.bridgeMessage(uint32,address,bool,bytes) external => NONDET; + function _.wrappedAddressIsNotMintable(address) external => NONDET; + + // from bridge + //function _.onMessageReceived(address,uint32,bytes) external => DISPATCHER(true); + function _.globalExitRootMap(bytes32) external => NONDET; +} \ No newline at end of file diff --git a/certora/specs/dispatchingMigrationManager.spec b/certora/specs/dispatchingMigrationManager.spec new file mode 100644 index 00000000..7c758a55 --- /dev/null +++ b/certora/specs/dispatchingMigrationManager.spec @@ -0,0 +1,20 @@ +import "dispatchingBridge.spec"; + +using TokenMock as Underlying; + +methods { + function _.approve(address a, uint256 b) external with(env e) => CVL_approve(e, a, b) expect (bool); + function Underlying.balanceOf(address) external returns (uint256) envfree; + function _.balanceOf(address a) external => CVL_balanceOf(a) expect (uint256); + function _.deposit() external with(env e) => CVL_deposit(e) expect void; +} + +function CVL_approve(env e, address a, uint256 b) returns bool { + return Underlying.approve(e, a, b); +} +function CVL_balanceOf(address a) returns uint256 { + return Underlying.balanceOf(a); +} +function CVL_deposit(env e) { + Underlying.deposit(e); +} diff --git a/certora/specs/tokenMockBalances.spec b/certora/specs/tokenMockBalances.spec new file mode 100644 index 00000000..0f4a78a7 --- /dev/null +++ b/certora/specs/tokenMockBalances.spec @@ -0,0 +1,107 @@ +import "bridgeSummary.spec"; +import "GenericVaultBridgeToken_helpers.spec"; + +methods { + function ERC20a.balanceOf(address) external returns (uint256) envfree; + function ERC20a.totalSupply() external returns (uint256) envfree; + function ERC20a.transfer(address,uint256) external returns (bool); +} + +// Partial sum of balances. +// sumOfBalances[x] = \sum_{i=0}^{x-1} balances[i]; +ghost mapping(mathint => mathint) sumOfBalances { + init_state axiom forall mathint addr. sumOfBalances[addr] == 0; +} + +// ghost copy of balances +ghost mapping(address => uint256) ghost_balances { + init_state axiom forall address addr. ghost_balances[addr] == 0; +} + +hook Sload uint256 _balance ERC20a._balanceOf[KEY address account] { + require ghost_balances[account] == _balance; +} + +hook Sstore ERC20a._balanceOf[KEY address account] uint256 _balance (uint256 _balance_old) { + // update partial sums for x > to_mathint(account) + havoc sumOfBalances assuming + forall mathint x. sumOfBalances@new[x] == + sumOfBalances@old[x] + (to_mathint(account) < x ? _balance - _balance_old : 0); + ghost_balances[account] = _balance; +} + +// rules and invariant all hold + +invariant sumOfBalancesStartsAtZero() + sumOfBalances[0] == 0 + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + + } +} + +invariant sumOfBalancesGrowsCorrectly() + forall address addr. sumOfBalances[to_mathint(addr) + 1] == + sumOfBalances[to_mathint(addr)] + ghost_balances[addr] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + + } +} + +invariant sumOfBalancesMonotone() + forall mathint i. forall mathint j. i <= j => sumOfBalances[i] <= sumOfBalances[j] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + } + } + +invariant sumOfBalancesEqualsTotalSupply() + sumOfBalances[2^160] == to_mathint(ERC20a.totalSupply()) + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + } + } + +rule twoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + uint256 balanceA = ERC20a.balanceOf(accountA); + uint256 balanceB = ERC20a.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(ERC20a.totalSupply()); + satisfy(accountA != accountB && balanceA > 0 && balanceB > 0); +} + + +rule threeBalancesCannotExceedTotalSupply(address accountA, address accountB, address accountC) { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + uint256 balanceA = ERC20a.balanceOf(accountA); + uint256 balanceB = ERC20a.balanceOf(accountB); + uint256 balanceC = ERC20a.balanceOf(accountC); + + assert accountA != accountB && accountA != accountC && accountB != accountC => + balanceA + balanceB + balanceC <= to_mathint(ERC20a.totalSupply()); + satisfy(accountA != accountB && balanceA + balanceB + balanceC > to_mathint(ERC20a.totalSupply())); +} diff --git a/certora/specs/tokenMockBalances2.spec b/certora/specs/tokenMockBalances2.spec new file mode 100644 index 00000000..f32ac7ad --- /dev/null +++ b/certora/specs/tokenMockBalances2.spec @@ -0,0 +1,107 @@ +import "bridgeSummary.spec"; +import "GenericNativeConverter_helpers.spec"; + +methods { + function underlyingTokenContract.balanceOf(address) external returns (uint256) envfree; + function underlyingTokenContract.totalSupply() external returns (uint256) envfree; + function underlyingTokenContract.transfer(address,uint256) external returns (bool); +} + +// Partial sum of balances. +// sumOfBalances[x] = \sum_{i=0}^{x-1} balances[i]; +ghost mapping(mathint => mathint) sumOfBalances { + init_state axiom forall mathint addr. sumOfBalances[addr] == 0; +} + +// ghost copy of balances +ghost mapping(address => uint256) ghost_balances { + init_state axiom forall address addr. ghost_balances[addr] == 0; +} + +hook Sload uint256 _balance underlyingTokenContract._balanceOf[KEY address account] { + require ghost_balances[account] == _balance; +} + +hook Sstore underlyingTokenContract._balanceOf[KEY address account] uint256 _balance (uint256 _balance_old) { + // update partial sums for x > to_mathint(account) + havoc sumOfBalances assuming + forall mathint x. sumOfBalances@new[x] == + sumOfBalances@old[x] + (to_mathint(account) < x ? _balance - _balance_old : 0); + ghost_balances[account] = _balance; +} + +// rules and invariant all hold + +invariant sumOfBalancesStartsAtZero() + sumOfBalances[0] == 0 + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + + } +} + +invariant sumOfBalancesGrowsCorrectly() + forall address addr. sumOfBalances[to_mathint(addr) + 1] == + sumOfBalances[to_mathint(addr)] + ghost_balances[addr] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + + } +} + +invariant sumOfBalancesMonotone() + forall mathint i. forall mathint j. i <= j => sumOfBalances[i] <= sumOfBalances[j] + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + } + } + +invariant sumOfBalancesEqualsTotalSupply() + sumOfBalances[2^160] == to_mathint(underlyingTokenContract.totalSupply()) + filtered { f -> !excludedMethod(f) } + { + preserved { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + } + } + +rule twoBalancesCannotExceedTotalSupply(address accountA, address accountB) { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + uint256 balanceA = underlyingTokenContract.balanceOf(accountA); + uint256 balanceB = underlyingTokenContract.balanceOf(accountB); + + assert accountA != accountB => + balanceA + balanceB <= to_mathint(underlyingTokenContract.totalSupply()); + satisfy(accountA != accountB && balanceA > 0 && balanceB > 0); +} + + +rule threeBalancesCannotExceedTotalSupply(address accountA, address accountB, address accountC) { + requireLinking(); + requireInvariant sumOfBalancesStartsAtZero(); + requireInvariant sumOfBalancesGrowsCorrectly(); + requireInvariant sumOfBalancesMonotone(); + requireInvariant sumOfBalancesEqualsTotalSupply(); + uint256 balanceA = underlyingTokenContract.balanceOf(accountA); + uint256 balanceB = underlyingTokenContract.balanceOf(accountB); + uint256 balanceC = underlyingTokenContract.balanceOf(accountC); + + assert accountA != accountB && accountA != accountC && accountB != accountC => + balanceA + balanceB + balanceC <= to_mathint(underlyingTokenContract.totalSupply()); + satisfy(accountA != accountB && balanceA + balanceB + balanceC > to_mathint(underlyingTokenContract.totalSupply())); +} diff --git a/foundry.toml b/foundry.toml index 4eb67227..e1185a77 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,44 +2,42 @@ src = "src" out = "out" libs = ["dependencies"] -via-ir = true -verbosity = 2 -ffi = true -optimizer = true -optimizer_runs = 833 - -fs_permissions = [ - { access = "read", path = "script/input.json" }, -] - remappings = [ "@openzeppelin-contracts-upgradeable/=dependencies/@openzeppelin-contracts-upgradeable-5.1.0/", "@openzeppelin-contracts/=dependencies/@openzeppelin-contracts-5.1.0/", "@openzeppelin/contracts/=dependencies/@openzeppelin-contracts-5.1.0/", "forge-std/=dependencies/forge-std-1.9.4/src", ] +via-ir = true +optimizer = true +optimizer_runs = 833 +verbosity = 2 +ffi = true +dynamic_test_linking = true +fs_permissions = [ + { access = "read", path = "script/input.json" }, + { access = "read", path = "out" } +] -[rpc_endpoints] -mainnet = "wss://mainnet.gateway.tenderly.co" -sepolia = "https://sepolia.gateway.tenderly.co" -polygon_pos = "https://polygon-mainnet.infura.io/v3/${INFURA_TOKEN}" -polygon_zkevm = "https://zkevm-rpc.com" -polygon_zkevm_testnet = "https://rpc.public.zkevm-test.net" -tatara = "https://rpc.tatara.katanarpc.com/${TATARA_TOKEN}" -katana = "https://rpc.katanarpc.com/${KATANA_TOKEN}" - -[etherscan] -mainnet = { key = "${API_KEY}" } -sepolia = { key = "${API_KEY}" } +[soldeer] +remappings_location = "config" +remappings_generate = true +remappings_version = false [dependencies] forge-std = { version = "1.9.4" } "@openzeppelin-contracts" = { version = "5.1.0" } "@openzeppelin-contracts-upgradeable" = { version = "5.1.0" } -[soldeer] -remappings_generate = true -remappings_version = false -remappings_location = "config" +[rpc_endpoints] +mainnet = "wss://mainnet.gateway.tenderly.co" +sepolia = "https://sepolia.gateway.tenderly.co" +katana = "https://rpc.katanarpc.com" +bokuto = "https://rpc-bokuto.katanarpc.com" +polygon = "https://polygon-bor-rpc.publicnode.com" +amoy = "https://polygon-amoy.drpc.org" + +[doc] +title = "Technical Reference" # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options \ No newline at end of file diff --git a/script/DeployLayerX.s.sol b/script/DeployLayerX.s.sol index 92d32cbf..51c40b27 100644 --- a/script/DeployLayerX.s.sol +++ b/script/DeployLayerX.s.sol @@ -1,13 +1,13 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Script.sol"; -import "../src/MigrationManager.sol"; -import "../src/VaultBridgeTokenInitializer.sol"; -import "../src/VaultBridgeTokenPart2.sol"; -import "../src/VaultBridgeToken.sol"; -import "../src/vault-bridge-tokens/GenericVaultBridgeToken.sol"; -import "../src/vault-bridge-tokens/vbETH/VbETH.sol"; +import "../src/primary-chain/MigrationManager.sol"; +import "../src/primary-chain/VaultBridgeTokenInitializer.sol"; +import "../src/primary-chain/VaultBridgeTokenPart2.sol"; +import "../src/primary-chain/VaultBridgeToken.sol"; +import "../src/primary-chain/ethereum/GenericVaultBridgeToken.sol"; +import "../src/primary-chain/ethereum/vbETH/VbETH.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; @@ -33,8 +33,9 @@ contract DeployLayerX is Script { // Read from input.json based on current chain ID address ownerMigrationManager = input.readAddress(string.concat(migrationManagerSlug, ".ownerMigrationManager")); - address lxlyBridge = input.readAddress(string.concat(migrationManagerSlug, ".lxlyBridge")); + address agglayerBridge = input.readAddress(string.concat(migrationManagerSlug, ".agglayerBridge")); address proxyAdmin = input.readAddress(string.concat(migrationManagerSlug, ".proxyAdmin")); + address wrappedGasToken = input.readAddress(string.concat(migrationManagerSlug, ".wrappedGasToken")); vm.startBroadcast(deployerPrivateKey); @@ -47,10 +48,12 @@ contract DeployLayerX is Script { MigrationManager migrationManagerImpl = new MigrationManager(); bytes memory migrationManagerInitData = - abi.encodeCall(MigrationManager.initialize, (ownerMigrationManager, lxlyBridge)); + abi.encodeCall(MigrationManager.reinitialize1, (ownerMigrationManager, agglayerBridge)); migrationManager = MigrationManager(payable(_proxify(address(migrationManagerImpl), proxyAdmin, migrationManagerInitData))); + migrationManager.reinitialize2(wrappedGasToken); + console.log("MigrationManager deployed at: ", address(migrationManager)); // 2. VAULT BRIDGE TOKENS @@ -66,7 +69,7 @@ contract DeployLayerX is Script { GenericVaultBridgeToken vbTokenImpl = new GenericVaultBridgeToken(); address initializer = address(new VaultBridgeTokenInitializer()); address vb2 = address(new VaultBridgeTokenPart2()); - VbETH vbETHImpl = new VbETH(); + VbEth vbETHImpl = new VbEth(); for (uint256 i = 0; i < vbTokens.length; i++) { string memory vbTokenSlug = @@ -80,7 +83,7 @@ contract DeployLayerX is Script { minimumReservePercentage: input.readUint(string.concat(vbTokenSlug, ".minimumReservePercentage")), yieldVault: input.readAddress(string.concat(vbTokenSlug, ".yieldVault")), yieldRecipient: input.readAddress(string.concat(vbTokenSlug, ".yieldRecipient")), - lxlyBridge: lxlyBridge, + agglayerBridge: agglayerBridge, minimumYieldVaultDeposit: input.readUint(string.concat(vbTokenSlug, ".minimumYieldVaultDeposit")), migrationManager: address(migrationManager), yieldVaultMaximumSlippagePercentage: input.readUint( @@ -91,13 +94,14 @@ contract DeployLayerX is Script { proxyAdmin = input.readAddress(string.concat(vbTokenSlug, ".proxyAdmin")); - bytes memory initData = abi.encodeCall(vbTokenImpl.initialize, (initializer, initParams)); + bytes memory initData = abi.encodeCall(vbTokenImpl.reinitialize1, (initializer, initParams)); if (i == 0) { vbTokenContracts[i] = GenericVaultBridgeToken(_proxify(address(vbETHImpl), proxyAdmin, initData)); } else { vbTokenContracts[i] = GenericVaultBridgeToken(_proxify(address(vbTokenImpl), proxyAdmin, initData)); } + vbTokenContracts[i].reinitialize2(); console.log(vbTokens[i], "deployed at: ", address(vbTokenContracts[i])); } diff --git a/script/DeployLayerY.s.sol b/script/DeployLayerY.s.sol index d041003e..a38bc46f 100644 --- a/script/DeployLayerY.s.sol +++ b/script/DeployLayerY.s.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Script.sol"; -import "../src/custom-tokens/GenericCustomToken.sol"; -import "../src/custom-tokens/GenericNativeConverter.sol"; +import "../src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol"; +import "../src/secondary-chain/agglayer/GenericNativeConverterAgglayer.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ERC1967Proxy, ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -29,7 +29,7 @@ contract DeployLayerY is Script { address migrationManagerAddress = input.readAddress(string.concat(slug, ".migrationManager")); address lxlyBridge = input.readAddress(string.concat(slug, ".lxlyBridge")); - GenericNativeConverter[] memory nativeConverters = new GenericNativeConverter[](5); + GenericNativeConverterAgglayer[] memory nativeConverters = new GenericNativeConverterAgglayer[](5); string[] memory vbTokens = new string[](4); vbTokens[0] = "vbUSDC"; @@ -38,8 +38,8 @@ contract DeployLayerY is Script { vbTokens[3] = "vbUSDS"; // deploy token impl - GenericCustomToken customTokenImpl = new GenericCustomToken(); - GenericNativeConverter nativeConverterImpl = new GenericNativeConverter(); + GenericCustomTokenAgglayer customTokenImpl = new GenericCustomTokenAgglayer(); + GenericNativeConverterAgglayer nativeConverterImpl = new GenericNativeConverterAgglayer(); for (uint256 i = 0; i < vbTokens.length; i++) { string memory vbSlug = @@ -47,17 +47,14 @@ contract DeployLayerY is Script { address customToken = input.readAddress(string.concat(vbSlug, ".customToken")); address underlyingToken = input.readAddress(string.concat(vbSlug, ".underlyingToken")); - string memory name = input.readString(string.concat(vbSlug, ".name")); - string memory symbol = input.readString(string.concat(vbSlug, ".symbol")); uint8 decimals = uint8(input.readUint(string.concat(vbSlug, ".decimals"))); uint256 nonMigratableBackingPercentage = input.readUint(string.concat(vbSlug, ".nonMigratableBackingPercentage")); bytes memory initNativeConverter = abi.encodeCall( - GenericNativeConverter.initialize, + GenericNativeConverterAgglayer.reinitialize1, ( polygonEngineeringMultisig, - decimals, customToken, underlyingToken, lxlyBridge, @@ -69,19 +66,23 @@ contract DeployLayerY is Script { address nativeConverter = _proxify(address(nativeConverterImpl), polygonEngineeringMultisig, initNativeConverter); - nativeConverters[i] = GenericNativeConverter(nativeConverter); + GenericNativeConverterAgglayer(payable(nativeConverter)).reinitialize2(); + + nativeConverters[i] = GenericNativeConverterAgglayer(nativeConverter); console.log("Native converter ", vbTokens[i], " deployed at: ", nativeConverter); // update custom token bytes memory data = abi.encodeCall( - GenericCustomToken.reinitialize, - (polygonEngineeringMultisig, name, symbol, decimals, lxlyBridge, nativeConverter) + GenericCustomTokenAgglayer.reinitialize2, + (polygonEngineeringMultisig, decimals, lxlyBridge, nativeConverter) ); IERC1967Proxy customTokenProxy = IERC1967Proxy(payable(customToken)); bytes memory payload = abi.encodeCall(customTokenProxy.upgradeToAndCall, (address(customTokenImpl), data)); + // TODO call other reinitialization functions? + console.log("Payload for upgrading custom token", vbTokens[i]); console.logBytes(payload); } diff --git a/script/DeployLayerY_WETH.s.sol b/script/DeployLayerY_WETH.s.sol index 6e0d0c01..4bb7367b 100644 --- a/script/DeployLayerY_WETH.s.sol +++ b/script/DeployLayerY_WETH.s.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Script.sol"; -import "../src/custom-tokens/WETH/WETH.sol"; -import "../src/custom-tokens/WETH/WETHNativeConverter.sol"; +import "../src/secondary-chain/agglayer/vbETH/WethAgglayer.sol"; +import "../src/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ERC1967Proxy, ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; @@ -33,19 +33,16 @@ contract DeployLayerY_WETH is Script { address vbWETH = input.readAddress(string.concat(vbETHSlug, ".customToken")); address wETH = input.readAddress(string.concat(vbETHSlug, ".underlyingToken")); - string memory name = input.readString(string.concat(vbETHSlug, ".name")); - string memory symbol = input.readString(string.concat(vbETHSlug, ".symbol")); uint8 decimals = uint8(input.readUint(string.concat(vbETHSlug, ".decimals"))); uint256 nonMigratableGasBackingPercentage = input.readUint(string.concat(vbETHSlug, ".nonMigratableGasBackingPercentage")); - WETHNativeConverter nativeConverterImpl = new WETHNativeConverter(); + WethNativeConverterAgglayer nativeConverterImpl = new WethNativeConverterAgglayer(); bytes memory initNativeConverter = abi.encodeCall( - WETHNativeConverter.initialize, + WethNativeConverterAgglayer.reinitialize1, ( polygonEngineeringMultisig, - decimals, vbWETH, wETH, lxlyBridge, @@ -58,17 +55,21 @@ contract DeployLayerY_WETH is Script { address wethNativeConverter = _proxify(address(nativeConverterImpl), polygonEngineeringMultisig, initNativeConverter); + WethNativeConverterAgglayer(payable(wethNativeConverter)).reinitialize2(); + // deploy vbWETH impl - WETH wethImpl = new WETH(); + WethAgglayer wethImpl = new WethAgglayer(); // update vbWETH bytes memory data = abi.encodeCall( - WETH.reinitialize, (polygonEngineeringMultisig, name, symbol, decimals, lxlyBridge, wethNativeConverter) + WethAgglayer.reinitialize2, (polygonEngineeringMultisig, decimals, lxlyBridge, wethNativeConverter) ); IERC1967Proxy vbWethProxy = IERC1967Proxy(payable(vbWETH)); bytes memory payload = abi.encodeCall(vbWethProxy.upgradeToAndCall, (address(wethImpl), data)); + // TODO: call other reinitialization functions? + console.log("Payload for upgrading vbWETH", "use this multisig: ", polygonEngineeringMultisig); console.logBytes(payload); diff --git a/script/DepositAndBridge.s.sol b/script/DepositAndBridge.s.sol index 55549ec6..b1fc9795 100644 --- a/script/DepositAndBridge.s.sol +++ b/script/DepositAndBridge.s.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Script.sol"; -import "../src/vault-bridge-tokens/vbETH/VbETH.sol"; +import "../src/primary-chain/ethereum/vbETH/VbETH.sol"; /// @dev this can be used to send some initial ETH to LayerY. Needs to be replicated for other tokens as well, /// @dev but can also be done manually. Ly token addresses are necessary for the rest of the deployment process. @@ -22,7 +22,7 @@ contract DepositAndBridge is Script { console.log(receiver); - VbETH vbETH = VbETH(payable(0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF)); + VbEth vbETH = VbEth(payable(0x2DC70fb75b88d2eB4715bc06E1595E6D97c34DFF)); uint256 shares = vbETH.depositGasTokenAndBridge{value: depositAmount}(receiver, NETWORK_ID_L2, true); diff --git a/script/RegisterNativeConverters.s.sol b/script/RegisterNativeConverters.s.sol index ab490a7e..5120fcc7 100644 --- a/script/RegisterNativeConverters.s.sol +++ b/script/RegisterNativeConverters.s.sol @@ -1,8 +1,8 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Script.sol"; -import "../src/MigrationManager.sol"; +import "../src/primary-chain/MigrationManager.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/script/etc/DeployRevertingContract.s.sol b/script/etc/DeployRevertingContract.s.sol new file mode 100644 index 00000000..37b06a15 --- /dev/null +++ b/script/etc/DeployRevertingContract.s.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (script/polygon/DeployRevertingContract.s.sol) + +pragma solidity ^0.8.29; + +// @remind Document (the entire file). + +import "forge-std/Script.sol"; + +contract DeployRevertingContract is Script { + string public chainName; + + address public deployerAddress; + + bytes public constant REVERTING_CONTRACT_CREATION_CODE = hex"6005600c60003960056000f360006000fd"; + bytes public constant REVERTING_CONTRACT_RUNTIME_CODE = hex"60006000fd"; + + /// @notice Setup. + /// @dev You can customize the setup here. + function setUp() public { + chainName = ""; + deployerAddress = 0x0000000000000000000000000000000000000000; + + require(bytes(chainName).length != 0, "Aborted: `chainName` not set"); + require(deployerAddress != address(0), "Aborted: `deployerAddress` not set"); + } + + function run() public { + console.log("Running `DeployRevertingContract` script..."); + + _createSelectFork(chainName); + + _createRevertingContract(); + + console.log("Finished running `DeployRevertingContract` script"); + } + + function _createRevertingContract() internal returns (address) { + console.log("Creating Reverting Contract..."); + + address revertingContract; + + bytes memory initCode = REVERTING_CONTRACT_CREATION_CODE; + + _startBroadcast(); + + assembly { + revertingContract := create(0, add(initCode, 0x20), mload(initCode)) + } + + _stopBroadcast(); + + require(revertingContract != address(0), "Aborted: Failed to create Reverting Contract"); + + assert(revertingContract.codehash == keccak256(REVERTING_CONTRACT_RUNTIME_CODE)); + + console.log("Reverting contract created:", revertingContract); + + return revertingContract; + } + + function _createSelectFork(string memory chainName_) internal { + vm.createSelectFork(vm.rpcUrl(chainName_)); + console.log("Switched to", chainName_, "chain"); + } + + function _startBroadcast() internal { + vm.startBroadcast(deployerAddress); + } + + function _stopBroadcast() internal { + vm.stopBroadcast(); + } +} diff --git a/script/input.json b/script/input.json index ea9d995c..3f8798d6 100644 --- a/script/input.json +++ b/script/input.json @@ -3,8 +3,9 @@ "migrationManager": { "proxyAdmin": "0x9d851f8b8751c5FbC09b9E74E6e68E9950949052", "ownerMigrationManager": "0x9d851f8b8751c5FbC09b9E74E6e68E9950949052", - "lxlyBridge": "0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe", + "agglayerBridge": "0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe", "address": "0x417d01B64Ea30C4E163873f3a1f77b727c689e02", + "wrappedGasToken": "TBD", "layerYNetworkId": 20, "vbUSDSNativeConverter": "0x639f13D5f30B47c792b6851238c05D0b623C77DE", "vbUSDTNativeConverter": "0x053FA9b934b83E1E0ffc7e98a41aAdc3640bB462", @@ -26,9 +27,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x4cE9c93513DfF543Bc392870d57dF8C04e89Ba0a", "yieldRecipient": "0xA8C31B2edd84c654d06d626383f4154D1E40C5Ff", - "lxlyBridge": "using above", + "agglayerBridge": "using above", "minimumYieldVaultDeposit": 0, "migrationManager": "using from deployment", + "wrappedGasToken": "using above", "yieldVaultMaximumSlippagePercentage": 0.01e18 }, "vbUSDT": { @@ -40,9 +42,10 @@ "minimumReservePercentage": 0, "yieldVault": "0xc54b4E08C1Dcc199fdd35c6b5Ab589ffD3428a8d", "yieldRecipient": "0x2De242e27386e224E5fbF110EA8406d5B70740ec", - "lxlyBridge": "using above", + "agglayerBridge": "using above", "minimumYieldVaultDeposit": 0, "migrationManager": "using from deployment", + "wrappedGasToken": "using above", "yieldVaultMaximumSlippagePercentage": 0.01e18 }, "vbUSDC": { @@ -54,9 +57,10 @@ "minimumReservePercentage": 0, "yieldVault": "0xBEefb9f61CC44895d8AEc381373555a64191A9c4", "yieldRecipient": "0xf4F2f5F6bAdBE05433C4604320ecC56BbECBC04E", - "lxlyBridge": "using above", + "agglayerBridge": "using above", "minimumYieldVaultDeposit": 0, "migrationManager": "using from deployment", + "wrappedGasToken": "using above", "yieldVaultMaximumSlippagePercentage": 0.01e18 }, "vbWBTC": { @@ -68,9 +72,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x812B2C6Ab3f4471c0E43D4BB61098a9211017427", "yieldRecipient": "0x2De242e27386e224E5fbF110EA8406d5B70740ec", - "lxlyBridge": "using above", + "agglayerBridge": "using above", "minimumYieldVaultDeposit": 0, "migrationManager": "using from deployment", + "wrappedGasToken": "using above", "yieldVaultMaximumSlippagePercentage": 0.01e18 }, "vbETH": { @@ -82,9 +87,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x31A5684983EeE865d943A696AAC155363bA024f9", "yieldRecipient": "0x2De242e27386e224E5fbF110EA8406d5B70740ec", - "lxlyBridge": "using above", + "agglayerBridge": "using above", "minimumYieldVaultDeposit": 0, "migrationManager": "using from deployment", + "wrappedGasToken": "using above", "yieldVaultMaximumSlippagePercentage": 0.01e18 } }, @@ -136,13 +142,15 @@ }, "migrationManager": "0x417d01B64Ea30C4E163873f3a1f77b727c689e02", "polygonEngineeringMultisig": "0x4e981bAe8E3cd06Ca911ffFE5504B2653ac1C38a", - "lxlyBridge": "0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe" + "agglayerBridge": "0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe" }, "31337": { "proxyAdmin": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "migrationManager": { + "proxyAdmin": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "ownerMigrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6" + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6" }, "vbETH": { "owner": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", @@ -152,9 +160,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldRecipient": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "minimumYieldVaultDeposit": 0, "migrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldVaultMaximumSlippagePercentage": 0 }, "vbUSDC": { @@ -165,9 +174,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldRecipient": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "minimumYieldVaultDeposit": 0, "migrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldVaultMaximumSlippagePercentage": 0 }, "vbUSDS": { @@ -178,9 +188,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldRecipient": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "minimumYieldVaultDeposit": 0, "migrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldVaultMaximumSlippagePercentage": 0 }, "vbUSDT": { @@ -191,9 +202,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldRecipient": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "minimumYieldVaultDeposit": 0, "migrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldVaultMaximumSlippagePercentage": 0 }, "vbWBTC": { @@ -204,9 +216,10 @@ "minimumReservePercentage": 0, "yieldVault": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldRecipient": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", - "lxlyBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "agglayerBridge": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "minimumYieldVaultDeposit": 0, "migrationManager": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", + "wrappedGasToken": "0x0000c6A4e8C654dF65503CBb0eDc82B4Ce9158e6", "yieldVaultMaximumSlippagePercentage": 0 } } diff --git a/script/polygon/DeployCustomTokensPolygon.s.sol b/script/polygon/DeployCustomTokensPolygon.s.sol new file mode 100644 index 00000000..45d703fe --- /dev/null +++ b/script/polygon/DeployCustomTokensPolygon.s.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (script/polygon/DeployCustomTokensPolygon.s.sol) + +pragma solidity ^0.8.29; + +// Forge Standard Library. +import "forge-std/Script.sol"; + +// Main functionality. +import {GenericCustomTokenPolygon} from "src/secondary-chain/polygon/GenericCustomTokenPolygon.sol"; + +// Other functionality. +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Deploy Custom Tokens Polygon +/// @author See https://github.com/agglayer/vault-bridge +/// @notice Creates a singleton `GenericCustomTokenPolygon` implementation and a `TransparentUpgradeableProxy` for each Custom Token, points the proxies to the implementation, and initializes them. +/// @dev The Custom Tokens need to be custom mapped on PoS Portal subsequently. Please refer to `src/secondary-chain/polygon/README.md` for more information. +contract DeployCustomTokensPolygon is Script { + // Secondary Chain name. + string public secondaryChainName; + + // Deployer address. + address public deployerAddress; + + // `GenericCustomTokenPolygon` implementation. + address public genericCustomTokenPolygonImplementation; + + // `GenericCustomTokenPolygon` proxies. + GenericCustomTokenPolygon public vbUsdc; + GenericCustomTokenPolygon public vbUsdt; + GenericCustomTokenPolygon public vbUsds; + + // Bridge address. + address public childChainManagerAddress; + + // Owner address. + address public ownerAddress; + + /// @notice Setup. + /// @dev You can customize the setup here. + function setUp() public { + // Set the inputs. + secondaryChainName = ""; + deployerAddress = 0x0000000000000000000000000000000000000000; + childChainManagerAddress = 0x0000000000000000000000000000000000000000; + ownerAddress = 0x0000000000000000000000000000000000000000; + + // Check the inputs. + require(bytes(secondaryChainName).length != 0, "Aborted: `secondaryChainName` not set"); + require(deployerAddress != address(0), "Aborted: `deployerAddress` not set"); + require(childChainManagerAddress != address(0), "Aborted: `childChainManagerAddress` not set"); + require(ownerAddress != address(0), "Aborted: `ownerAddress` not set"); + } + + /// @notice Run. + /// @dev You can customize the run here. + function run() public { + console.log("Running `DeployCustomTokensPolygon` script..."); + + // Switch to Polygon. + _createSelectFork(secondaryChainName); + + // Create a singleton `GenericCustomTokenPolygon` implementation. + genericCustomTokenPolygonImplementation = _createGenericCustomTokenPolygonImplementation(); + + // Proxify and initialize vbUSDC. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, + (ownerAddress, "Vault Bridge USDC", "vbUSDC", 6, childChainManagerAddress) + ); + + vbUsdc = _proxifyAndInitializeGenericCustomTokenPolygon( + genericCustomTokenPolygonImplementation, + abi.encodeCall(GenericCustomTokenPolygon.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbUSDT. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, + (ownerAddress, "Vault Bridge USDT", "vbUSDT", 6, childChainManagerAddress) + ); + + vbUsdt = _proxifyAndInitializeGenericCustomTokenPolygon( + genericCustomTokenPolygonImplementation, + abi.encodeCall(GenericCustomTokenPolygon.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbUSDS. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, + (ownerAddress, "Vault Bridge USDS", "vbUSDS", 18, childChainManagerAddress) + ); + + vbUsds = _proxifyAndInitializeGenericCustomTokenPolygon( + genericCustomTokenPolygonImplementation, + abi.encodeCall(GenericCustomTokenPolygon.reinitialize, (reinitializeData)) + ); + } + + console.log("Finished running `DeployCustomTokensPolygon` script"); + } + + /// @notice Creates a singleton `GenericCustomTokenPolygon` implementation. + function _createGenericCustomTokenPolygonImplementation() internal returns (address) { + console.log("Creating `GenericCustomTokenPolygon` implementation..."); + + _startBroadcast(); + + // Create `GenericCustomTokenPolygon` implementation. + GenericCustomTokenPolygon implementation = new GenericCustomTokenPolygon(); + + _stopBroadcast(); + + console.log("`GenericCustomTokenPolygon` implementation created:", address(implementation)); + + // Return the address of the implementation. + return address(implementation); + } + + /// @notice Creates a `TransparentUpgradeableProxy` for a Custom Token, points it to the `GenericCustomTokenPolygon` implementation, and initializes it. + function _proxifyAndInitializeGenericCustomTokenPolygon( + address genericCustomTokenPolygonImplementation_, + bytes memory initializationData_ + ) internal returns (GenericCustomTokenPolygon) { + console.log("Proxifying and initializing `GenericCustomTokenPolygon`..."); + + // Check the input. + require( + genericCustomTokenPolygonImplementation_ != address(0), + "Aborted: `genericCustomTokenPolygonImplementation_` not set" + ); + + _startBroadcast(); + + // Create a `TransparentUpgradeableProxy` and point it to the `GenericCustomTokenPolygon` implementation. + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(genericCustomTokenPolygonImplementation_, ownerAddress, initializationData_); + + _stopBroadcast(); + + console.log(GenericCustomTokenPolygon(address(proxy)).name(), "proxified and initialized:", address(proxy)); + + // Return a `GenericCustomTokenPolygon`. + return GenericCustomTokenPolygon(address(proxy)); + } + + function _createSelectFork(string memory chainName_) internal { + vm.createSelectFork(vm.rpcUrl(chainName_)); + console.log("Switched to", chainName_, "chain"); + } + + function _startBroadcast() internal { + vm.startBroadcast(deployerAddress); + } + + function _stopBroadcast() internal { + vm.stopBroadcast(); + } +} diff --git a/script/wormhole/DeployCustomTokensWormhole.s.sol b/script/wormhole/DeployCustomTokensWormhole.s.sol new file mode 100644 index 00000000..ca646bcc --- /dev/null +++ b/script/wormhole/DeployCustomTokensWormhole.s.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (script/wormhole/DeployCustomTokensWormhole.s.sol) + +pragma solidity ^0.8.29; + +// Forge Standard Library. +import "forge-std/Script.sol"; + +// Main functionality. +import {WethWormhole} from "src/secondary-chain/wormhole/vbETH/WethWormhole.sol"; +import {GenericCustomTokenWormhole} from "src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol"; + +// Other functionality. +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Deploy Custom Tokens Wormhole +/// @author See https://github.com/agglayer/vault-bridge +/// @notice Creates singleton `WethWormhole` and `GenericCustomTokenWormhole` implementations and a `TransparentUpgradeableProxy` for each Custom Token, points the proxies to the implementation, and initializes them. +/// @dev A NTT Manager/Wormhole Transceiver pair needs to be deployed and configured subsequently, and the bridge needs to be set. Please refer to `src/secondary-chain/wormhole/README.md` for more information. +contract DeployCustomTokensWormhole is Script { + // Secondary Chain name. + string public secondaryChainName; + + // Deployer address. + address public deployerAddress; + + // `WethWormhole` and `GenericCustomTokenWormhole` implementations. + address public wethWormholeImplementation; + address public genericCustomTokenWormholeImplementation; + + // `WethWormhole` and `GenericCustomTokenWormhole` proxies. + WethWormhole public vbEth; + GenericCustomTokenWormhole public vbUsdc; + GenericCustomTokenWormhole public vbUsdt; + GenericCustomTokenWormhole public vbUsds; + GenericCustomTokenWormhole public vbWbtc; + + // Bridge address. + address public nttManagerAddress; + + // Owner address. + address public ownerAddress; + + // Reinitialization parameters. + bool public gasTokenIsEth; + bool public wethFunctionalityEnabled; + + /// @notice Setup. + /// @dev You can customize the setup here. + function setUp() public { + // Set the inputs. + secondaryChainName = ""; + deployerAddress = 0x0000000000000000000000000000000000000000; + nttManagerAddress = 0x0000000000000000000000000000000000000000; + ownerAddress = 0x0000000000000000000000000000000000000000; + gasTokenIsEth = false; + wethFunctionalityEnabled = false; + + // Check the inputs. + require(bytes(secondaryChainName).length != 0, "Aborted: `secondaryChainName` not set"); + require(deployerAddress != address(0), "Aborted: `deployerAddress` not set"); + require(nttManagerAddress != address(0), "Aborted: `nttManagerAddress` not set"); + require(ownerAddress != address(0), "Aborted: `ownerAddress` not set"); + } + + /// @notice Run. + /// @dev You can customize the run here. + function run() public { + console.log("Running `DeployCustomTokensWormhole` script..."); + + // Switch to the Secondary Chain. + _createSelectFork(secondaryChainName); + + // Create singleton `WethWormhole` and `GenericCustomTokenWormhole` implementations. + wethWormholeImplementation = _createWethWormholeImplementation(); + genericCustomTokenWormholeImplementation = _createGenericCustomTokenWormholeImplementation(); + + // Proxify and initialize vbETH. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + WethWormhole.reinitialize1, + ( + ownerAddress, + "Vault Bridge ETH", + "vbETH", + 18, + nttManagerAddress, + gasTokenIsEth, + wethFunctionalityEnabled + ) + ); + + vbEth = _proxifyAndInitializeWethWormhole( + wethWormholeImplementation, abi.encodeCall(WethWormhole.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbUSDC. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (ownerAddress, "Vault Bridge USDC", "vbUSDC", 6, nttManagerAddress) + ); + + vbUsdc = _proxifyAndInitializeGenericCustomTokenWormhole( + genericCustomTokenWormholeImplementation, + abi.encodeCall(GenericCustomTokenWormhole.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbUSDT. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (ownerAddress, "Vault Bridge USDT", "vbUSDT", 6, nttManagerAddress) + ); + + vbUsdt = _proxifyAndInitializeGenericCustomTokenWormhole( + genericCustomTokenWormholeImplementation, + abi.encodeCall(GenericCustomTokenWormhole.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbUSDS. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (ownerAddress, "Vault Bridge USDS", "vbUSDS", 18, nttManagerAddress) + ); + + vbUsds = _proxifyAndInitializeGenericCustomTokenWormhole( + genericCustomTokenWormholeImplementation, + abi.encodeCall(GenericCustomTokenWormhole.reinitialize, (reinitializeData)) + ); + } + + // Proxify and initialize vbWBTC. + { + bytes[] memory reinitializeData = new bytes[](1); + + reinitializeData[0] = abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (ownerAddress, "Vault Bridge WBTC", "vbWBTC", 8, nttManagerAddress) + ); + + vbWbtc = _proxifyAndInitializeGenericCustomTokenWormhole( + genericCustomTokenWormholeImplementation, + abi.encodeCall(GenericCustomTokenWormhole.reinitialize, (reinitializeData)) + ); + } + + console.log("Finished running `DeployCustomTokensWormhole` script"); + } + + /// @notice Creates a singleton `WethWormhole` implementation. + function _createWethWormholeImplementation() internal returns (address) { + console.log("Creating `WethWormhole` implementation..."); + + _startBroadcast(); + + // Create `WethWormhole` implementation. + WethWormhole implementation = new WethWormhole(); + + _stopBroadcast(); + + console.log("`WethWormhole` implementation created:", address(implementation)); + + // Return the address of the implementation. + return address(implementation); + } + + /// @notice Creates a singleton `GenericCustomTokenWormhole` implementation. + function _createGenericCustomTokenWormholeImplementation() internal returns (address) { + console.log("Creating `GenericCustomTokenWormhole` implementation..."); + + _startBroadcast(); + + // Create `GenericCustomTokenWormhole` implementation. + GenericCustomTokenWormhole implementation = new GenericCustomTokenWormhole(); + + _stopBroadcast(); + + console.log("`GenericCustomTokenWormhole` implementation created:", address(implementation)); + + // Return the address of the implementation. + return address(implementation); + } + + /// @notice Creates a `TransparentUpgradeableProxy` for WETH, points it to the `WethWormhole` implementation, and initializes it. + function _proxifyAndInitializeWethWormhole(address wethWormholeImplementation_, bytes memory initializationData_) + internal + returns (WethWormhole) + { + console.log("Proxifying and initializing `WethWormhole`..."); + + // Check the input. + require(wethWormholeImplementation_ != address(0), "Aborted: `wethWormholeImplementation_` not set"); + + _startBroadcast(); + + // Create a `TransparentUpgradeableProxy` and point it to the `WethWormhole` implementation. + TransparentUpgradeableProxy proxy = + new TransparentUpgradeableProxy(wethWormholeImplementation_, ownerAddress, initializationData_); + + _stopBroadcast(); + + console.log(WethWormhole(address(proxy)).name(), "proxified and initialized:", address(proxy)); + + // Return a `WethWormhole`. + return WethWormhole(address(proxy)); + } + + /// @notice Creates a `TransparentUpgradeableProxy` for a generic Custom Token, points it to the `GenericCustomTokenWormhole` implementation, and initializes it. + function _proxifyAndInitializeGenericCustomTokenWormhole( + address genericCustomTokenWormholeImplementation_, + bytes memory initializationData_ + ) internal returns (GenericCustomTokenWormhole) { + console.log("Proxifying and initializing `GenericCustomTokenWormhole`..."); + + // Check the input. + require( + genericCustomTokenWormholeImplementation_ != address(0), + "Aborted: `genericCustomTokenWormholeImplementation_` not set" + ); + + _startBroadcast(); + + // Create a `TransparentUpgradeableProxy` and point it to the `GenericCustomTokenWormhole` implementation. + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy( + genericCustomTokenWormholeImplementation_, ownerAddress, initializationData_ + ); + + _stopBroadcast(); + + console.log(GenericCustomTokenWormhole(address(proxy)).name(), "proxified and initialized:", address(proxy)); + + // Return a `GenericCustomTokenWormhole`. + return GenericCustomTokenWormhole(address(proxy)); + } + + function _createSelectFork(string memory chainName_) internal { + vm.createSelectFork(vm.rpcUrl(chainName_)); + console.log("Switched to", chainName_, "chain"); + } + + function _startBroadcast() internal { + vm.startBroadcast(deployerAddress); + } + + function _stopBroadcast() internal { + vm.stopBroadcast(); + } +} diff --git a/src/custom-tokens/GenericCustomToken.sol b/src/custom-tokens/GenericCustomToken.sol deleted file mode 100644 index a7b35a4a..00000000 --- a/src/custom-tokens/GenericCustomToken.sol +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {CustomToken} from "../CustomToken.sol"; - -// Other functionality. -import {IVersioned} from "../etc/IVersioned.sol"; - -/// @title Generic Custom Token -/// @author See https://github.com/agglayer/vault-bridge -/// @dev This contract can be used to deploy Custom Tokens that do not require any customization. -contract GenericCustomToken is CustomToken { - // -----================= ::: SETUP ::: =================----- - - constructor() { - _disableInitializers(); - } - - function reinitialize( - address owner_, - string calldata name_, - string calldata symbol_, - uint8 originalUnderlyingTokenDecimals_, - address lxlyBridge_, - address nativeConverter_ - ) external virtual reinitializer(2) { - // Initialize the base implementation. - __CustomToken_init(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, lxlyBridge_, nativeConverter_); - } - - // -----================= ::: INFO ::: =================----- - - /// @inheritdoc IVersioned - function version() external pure virtual returns (string memory) { - return "0.5.0"; - } -} diff --git a/src/custom-tokens/GenericNativeConverter.sol b/src/custom-tokens/GenericNativeConverter.sol deleted file mode 100644 index ec3368ef..00000000 --- a/src/custom-tokens/GenericNativeConverter.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {NativeConverter} from "../NativeConverter.sol"; - -// Other functionality. -import {IVersioned} from "../etc/IVersioned.sol"; - -/// @title Generic Native Converter -/// @author See https://github.com/agglayer/vault-bridge -/// @dev This contract can be used to deploy Native Converters that do not require any customization. -contract GenericNativeConverter is NativeConverter { - // -----================= ::: SETUP ::: =================----- - - constructor() { - _disableInitializers(); - } - - function initialize( - address owner_, - uint8 originalUnderlyingTokenDecimals_, - address customToken_, - address underlyingToken_, - address lxlyBridge_, - uint32 layerXLxlyId_, - uint256 nonMigratableBackingPercentage_, - address migrationManager_ - ) external initializer { - // Initialize the base implementation. - __NativeConverter_init( - owner_, - originalUnderlyingTokenDecimals_, - customToken_, - underlyingToken_, - lxlyBridge_, - layerXLxlyId_, - nonMigratableBackingPercentage_, - migrationManager_ - ); - } - - // -----================= ::: INFO ::: =================----- - - /// @inheritdoc IVersioned - function version() external pure virtual returns (string memory) { - return "0.5.0"; - } -} diff --git a/src/custom-tokens/README.md b/src/custom-tokens/README.md deleted file mode 100644 index 5ef77aed..00000000 --- a/src/custom-tokens/README.md +++ /dev/null @@ -1 +0,0 @@ -

LAYER Y

\ No newline at end of file diff --git a/src/custom-tokens/WETH/WETH.sol b/src/custom-tokens/WETH/WETH.sol deleted file mode 100644 index 1cbf8daf..00000000 --- a/src/custom-tokens/WETH/WETH.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -import {CustomToken} from "../../CustomToken.sol"; -import {IWETH9} from "../../etc/IWETH9.sol"; -import {IVersioned} from "../../etc/IVersioned.sol"; -import {ILxLyBridge} from "../../etc/ILxLyBridge.sol"; - -/// @title WETH -/// @author See https://github.com/agglayer/vault-bridge -/// @dev based on https://github.com/gnosis/canonical-weth/blob/master/contracts/WETH9.sol -contract WETH is CustomToken { - /// @dev Storage of WETH contract. - /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. - /// @custom:storage-location erc7201:agglayer.vault-bridge.WETH.storage - struct WETHStorage { - bool _gasTokenIsEth; - } - - /// @dev The storage slot at which WETH storage starts, following the EIP-7201 standard. - /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.WETH.storage")) - 1)) & ~bytes32(uint256(0xff))`. - bytes32 private constant _WETH_STORAGE = hex"df8caff5d0161908572492829df972cd19b1aabe3c3078d95299408cd561dc00"; - - error AssetsTooLarge(uint256 availableAssets, uint256 requestedAssets); - error FunctionNotSupportedOnThisNetwork(); - - event Deposit(address indexed from, uint256 value); - event Withdrawal(address indexed to, uint256 value); - - constructor() { - _disableInitializers(); - } - - modifier onlyNativeConverter() { - require(msg.sender == nativeConverter(), Unauthorized()); - _; - } - - modifier onlyIfGasTokenIsEth() { - require(_getWETHStorage()._gasTokenIsEth, FunctionNotSupportedOnThisNetwork()); - _; - } - - function reinitialize( - address owner_, - string calldata name_, - string calldata symbol_, - uint8 originalUnderlyingTokenDecimals_, - address lxlyBridge_, - address nativeConverter_ - ) external virtual reinitializer(2) { - WETHStorage storage $ = _getWETHStorage(); - - // Initialize the inherited contracts. - __CustomToken_init(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, lxlyBridge_, nativeConverter_); - - $._gasTokenIsEth = - ILxLyBridge(lxlyBridge_).gasTokenAddress() == address(0) && ILxLyBridge(lxlyBridge_).gasTokenNetwork() == 0; - } - - function _getWETHStorage() private pure returns (WETHStorage storage $) { - assembly { - $.slot := _WETH_STORAGE - } - } - - function bridgeBackingToLayerX(uint256 amount) - external - whenNotPaused - onlyIfGasTokenIsEth - onlyNativeConverter - nonReentrant - { - (bool success,) = nativeConverter().call{value: amount}(""); - require(success); - } - - receive() external payable whenNotPaused onlyIfGasTokenIsEth nonReentrant { - _deposit(); - } - - function deposit() external payable whenNotPaused onlyIfGasTokenIsEth nonReentrant { - _deposit(); - } - - function _deposit() internal { - _mint(msg.sender, msg.value); - emit Deposit(msg.sender, msg.value); - } - - function withdraw(uint256 value) external whenNotPaused onlyIfGasTokenIsEth nonReentrant { - _burn(msg.sender, value); - uint256 availableAssets = address(this).balance; - require(availableAssets >= value, AssetsTooLarge(availableAssets, value)); - payable(msg.sender).transfer(value); - emit Withdrawal(msg.sender, value); - } - - /// @inheritdoc IVersioned - function version() public pure returns (string memory) { - return "0.5.0"; - } -} diff --git a/src/custom-tokens/vbUSDC/VbUSDC.sol.generic b/src/custom-tokens/vbUSDC/VbUSDC.sol.generic deleted file mode 100644 index c90e7cf0..00000000 --- a/src/custom-tokens/vbUSDC/VbUSDC.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericCustomToken} from "../GenericCustomToken.sol"; - -/// @title VbUSDC Custom Token -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDC` proxy to `GenericCustomToken` instead. -contract VbUSDC is GenericCustomToken {} diff --git a/src/custom-tokens/vbUSDC/VbUSDCNativeConverter.sol.generic b/src/custom-tokens/vbUSDC/VbUSDCNativeConverter.sol.generic deleted file mode 100644 index 21185030..00000000 --- a/src/custom-tokens/vbUSDC/VbUSDCNativeConverter.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericNativeConverter} from "../GenericNativeConverter.sol"; - -/// @title VbUSDC Native Converter -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDCNativeConverter` proxy to `GenericNativeConverter` instead. -contract VbUSDCNativeConverter is GenericNativeConverter {} diff --git a/src/custom-tokens/vbUSDS/VbUSDS.sol.generic b/src/custom-tokens/vbUSDS/VbUSDS.sol.generic deleted file mode 100644 index 6d9cd0cb..00000000 --- a/src/custom-tokens/vbUSDS/VbUSDS.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericCustomToken} from "../GenericCustomToken.sol"; - -/// @title VbUSDS Custom Token -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDS` proxy to `GenericCustomToken` instead. -contract VbUSDS is GenericCustomToken {} diff --git a/src/custom-tokens/vbUSDS/VbUSDSNativeConverter.sol.generic b/src/custom-tokens/vbUSDS/VbUSDSNativeConverter.sol.generic deleted file mode 100644 index 6120dc7b..00000000 --- a/src/custom-tokens/vbUSDS/VbUSDSNativeConverter.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericNativeConverter} from "../GenericNativeConverter.sol"; - -/// @title VbUSDS Native Converter -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDSNativeConverter` proxy to `GenericNativeConverter` instead. -contract VbUSDSNativeConverter is GenericNativeConverter {} diff --git a/src/custom-tokens/vbUSDT/VbUSDT.sol.generic b/src/custom-tokens/vbUSDT/VbUSDT.sol.generic deleted file mode 100644 index d280e54e..00000000 --- a/src/custom-tokens/vbUSDT/VbUSDT.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericCustomToken} from "../GenericCustomToken.sol"; - -/// @title VbUSDT Custom Token -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDT` proxy to `GenericCustomToken` instead. -contract VbUSDT is GenericCustomToken {} diff --git a/src/custom-tokens/vbUSDT/VbUSDTNativeConverter.sol.generic b/src/custom-tokens/vbUSDT/VbUSDTNativeConverter.sol.generic deleted file mode 100644 index bdbf3fce..00000000 --- a/src/custom-tokens/vbUSDT/VbUSDTNativeConverter.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericNativeConverter} from "../GenericNativeConverter.sol"; - -/// @title VbUSDT Native Converter -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDTNativeConverter` proxy to `GenericNativeConverter` instead. -contract VbUSDTNativeConverter is GenericNativeConverter {} diff --git a/src/custom-tokens/vbWBTC/VbWBTC.sol.generic b/src/custom-tokens/vbWBTC/VbWBTC.sol.generic deleted file mode 100644 index 7176eb31..00000000 --- a/src/custom-tokens/vbWBTC/VbWBTC.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericCustomToken} from "../GenericCustomToken.sol"; - -/// @title VbWBTC Custom Token -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbWBTC` proxy to `GenericCustomToken` instead. -contract VbWBTC is GenericCustomToken {} diff --git a/src/custom-tokens/vbWBTC/VbWBTCNativeConverter.sol.generic b/src/custom-tokens/vbWBTC/VbWBTCNativeConverter.sol.generic deleted file mode 100644 index 22ba0a48..00000000 --- a/src/custom-tokens/vbWBTC/VbWBTCNativeConverter.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericNativeConverter} from "../GenericNativeConverter.sol"; - -/// @title VbWBTC Native Converter -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbWBTCNativeConverter` proxy to `GenericNativeConverter` instead. -contract VbWBTCNativeConverter is GenericNativeConverter {} diff --git a/src/etc/ERC20PermitUser.sol b/src/etc/ERC20PermitUser.sol index 2e04b27d..f4a10980 100644 --- a/src/etc/ERC20PermitUser.sol +++ b/src/etc/ERC20PermitUser.sol @@ -1,9 +1,11 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v0.5.0) (etc/ERC20PermitUser.sol) + pragma solidity 0.8.29; /// @title ERC-20 Permit User /// @author See https://github.com/agglayer/vault-bridge -/// @dev Mimics the behavior of LxLy Bridge for validating and using ERC-20 permits. +/// @dev Mimics the behavior of Agglayer Bridge for validating and using ERC-20 permits. abstract contract ERC20PermitUser { /// @dev Calculated as `bytes4(keccak256(bytes("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")))`. bytes4 private constant _PERMIT_SELECTOR_ERC_2612 = hex"d505accf"; diff --git a/src/etc/ILxLyBridge.sol b/src/etc/IAgglayerBridge.sol similarity index 87% rename from src/etc/ILxLyBridge.sol rename to src/etc/IAgglayerBridge.sol index 795df93c..7f77d31a 100644 --- a/src/etc/ILxLyBridge.sol +++ b/src/etc/IAgglayerBridge.sol @@ -1,8 +1,10 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (etc/IAgglayerBridge.sol) + pragma solidity 0.8.29; /// @author See https://github.com/agglayer/vault-bridge -interface ILxLyBridge { +interface IAgglayerBridge { function networkID() external view returns (uint32); function gasTokenAddress() external view returns (address); function gasTokenNetwork() external view returns (uint32); @@ -47,4 +49,5 @@ interface ILxLyBridge { bytes calldata metadata ) external payable; function wrappedAddressIsNotMintable(address wrappedAddress) external view returns (bool isNotMintable); + function localBalanceTree(bytes32 tokenInfoHash) external view returns (uint256); } diff --git a/src/etc/IBridgeMessageReceiver.sol b/src/etc/IBridgeMessageReceiver.sol index 313e2d5c..6fc88d31 100644 --- a/src/etc/IBridgeMessageReceiver.sol +++ b/src/etc/IBridgeMessageReceiver.sol @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v0.5.0) (etc/IBridgeMessageReceiver.sol) + pragma solidity 0.8.29; /// @author See https://github.com/agglayer/vault-bridge diff --git a/src/etc/IFiatTokenV2_2.sol b/src/etc/IFiatTokenV2_2.sol new file mode 100644 index 00000000..427e2b10 --- /dev/null +++ b/src/etc/IFiatTokenV2_2.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (etc/IFiatTokenV2_2.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Bridged USDC Standard Interface +/// @author See https://github.com/agglayer/vault-bridge +interface IFiatTokenV2_2 is IERC20 { + function burn(uint256 _amount) external; +} diff --git a/src/etc/IVaultBridgeTokenInitializer.sol b/src/etc/IVaultBridgeTokenInitializer.sol index 2d1d5418..9719cd42 100644 --- a/src/etc/IVaultBridgeTokenInitializer.sol +++ b/src/etc/IVaultBridgeTokenInitializer.sol @@ -1,12 +1,16 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v0.5.0) (etc/IVaultBridgeTokenInitializer.sol) + pragma solidity 0.8.29; // Main functionality. -import {VaultBridgeToken} from "../VaultBridgeToken.sol"; +import {VaultBridgeToken} from "../primary-chain/VaultBridgeToken.sol"; -// @remind Document. +/// @title Vault Bridge Token Initializer Interface /// @author See https://github.com/agglayer/vault-bridge +/// @dev This interface exists because of a limitiation in the Solidity compiler. interface IVaultBridgeTokenInitializer { - // @remind Document. + /// @dev Vault Bridge Token delegates the initialization to this contract. + /// @dev Please refer to `__VaultBridgeToken_init` in `VaultBridgeToken.sol` for more information. function initialize(VaultBridgeToken.InitializationParameters calldata initParams) external; } diff --git a/src/etc/IVersioned.sol b/src/etc/IVersioned.sol deleted file mode 100644 index 32bf443d..00000000 --- a/src/etc/IVersioned.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -/// @author See https://github.com/agglayer/vault-bridge -interface IVersioned { - /// @notice The version of the contract. - function version() external pure returns (string memory); -} diff --git a/src/etc/IWETH9.sol b/src/etc/IWETH9.sol index 6d24e69f..9b1bfc70 100644 --- a/src/etc/IWETH9.sol +++ b/src/etc/IWETH9.sol @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: GPL-2.0-or-later +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v0.5.0) (etc/IWETH9.sol) + pragma solidity 0.8.29; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; diff --git a/src/etc/InitializationCounterUpgradeable.sol b/src/etc/InitializationCounterUpgradeable.sol new file mode 100644 index 00000000..72d85135 --- /dev/null +++ b/src/etc/InitializationCounterUpgradeable.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (etc/InitializationCounterUpgradeable.sol) + +pragma solidity 0.8.29; + +// @remind Document (the entire contract). +/// @author See https://github.com/agglayer/vault-bridge +abstract contract InitializationCounterUpgradeable { + enum Extension { + WETH + } + + /// @dev Storage of Initialization Counter contract. + /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. + /// @custom:storage-location erc7201:agglayer.vault-bridge.InitializationCounterUpgradeable.storage + struct InitializationCounterUpgradeableStorage { + uint64 _localInitializationCounter; + mapping(Extension => uint64) _extensionInitializationCounter; + uint64 globalInitializationCounter; + } + + /// @dev The storage slot at which Initialization Counter storage starts, following the EIP-7201 standard. + /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.InitializationCounterUpgradeable.storage")) - 1)) & ~bytes32(uint256(0xff))`. + bytes32 private constant _INITIALIZATION_COUNTER_UPGRADEABLE_STORAGE = + hex"8d679e361eeeac0b879fa197c8b3bda76a3db4f57c9f89335c04a065390bbb00"; + + // Errors. + error IncorrectInitializationOrder( + uint64 expectedGlobalInitializationCounterValue, uint64 actualGlobalInitializationCounterValue + ); + error AlreadyReinitialized(); + error InvalidReinitializeDataLength(uint256 expectedLength, uint256 actualLength); + error Eip1967NotDetected(); + error UnknownReinitializeSelector(bytes4 selector); + + // -----================= ::: STORAGE ::: =================----- + + // @remind Document. + function globalInitializationCounter() public view returns (uint64) { + InitializationCounterUpgradeableStorage storage $ = _getInitializationCounterUpgradeableStorage(); + return $.globalInitializationCounter; + } + + /// @dev Returns a pointer to the ERC-7201 storage namespace. + function _getInitializationCounterUpgradeableStorage() + private + pure + returns (InitializationCounterUpgradeableStorage storage $) + { + assembly { + $.slot := _INITIALIZATION_COUNTER_UPGRADEABLE_STORAGE + } + } + + // -----================= ::: INITIALIZATION COUNTER ::: =================----- + + // @remind Document (the entire modifier). + modifier incrementsLocalInitializationCounter(uint64 expectedNewLocalInitializationCounterValue) { + _incrementLocalInitializationCounter(expectedNewLocalInitializationCounterValue); + _; + } + + function _incrementLocalInitializationCounter(uint64 expectedNewLocalInitializationCounterValue) private { + InitializationCounterUpgradeableStorage storage $ = _getInitializationCounterUpgradeableStorage(); + + uint64 actualNewLocalInitializationCounterValue = $._localInitializationCounter + 1; + + assert(expectedNewLocalInitializationCounterValue == actualNewLocalInitializationCounterValue); + + $._localInitializationCounter++; + } + + // @remind Document (the entire function). + function _incrementGlobalInitializationCounter(uint64 expectedNewGlobalInitializationCounterValue) + internal + returns (uint64) + { + InitializationCounterUpgradeableStorage storage $ = _getInitializationCounterUpgradeableStorage(); + + uint64 actualNewGlobalInitializationCounterValue = $.globalInitializationCounter + 1; + + require( + expectedNewGlobalInitializationCounterValue == actualNewGlobalInitializationCounterValue, + IncorrectInitializationOrder( + expectedNewGlobalInitializationCounterValue, actualNewGlobalInitializationCounterValue + ) + ); + + $.globalInitializationCounter++; + + return expectedNewGlobalInitializationCounterValue; + } + + modifier incrementsExtensionInitializationCounter( + uint64 requiredLocalInitializationCounterValue, + Extension extension, + uint64 expectedNewExtensionInitializationCounterValue + ) { + _incrementExtensionInitializationCounter( + requiredLocalInitializationCounterValue, extension, expectedNewExtensionInitializationCounterValue + ); + _; + } + + function _incrementExtensionInitializationCounter( + uint64 requiredLocalInitializationCounterValue, + Extension extension, + uint64 expectedNewExtensionInitializationCounterValue + ) private { + InitializationCounterUpgradeableStorage storage $ = _getInitializationCounterUpgradeableStorage(); + + assert($._localInitializationCounter == requiredLocalInitializationCounterValue); + + uint64 actualNewExtensionInitializationCounterValue = $._extensionInitializationCounter[extension] + 1; + + assert(expectedNewExtensionInitializationCounterValue == actualNewExtensionInitializationCounterValue); + + $._extensionInitializationCounter[extension]++; + } + + // @remind Uncomment later (requires modifications of contracts and tests). + // function reinitialize(bytes[] calldata reinitializeData) external virtual; + + // @remind Document (the entire function). + function _reinitialize(bytes4[] memory reinitializeSelectors, bytes[] calldata reinitializeData) internal { + InitializationCounterUpgradeableStorage storage $ = _getInitializationCounterUpgradeableStorage(); + + assert(reinitializeSelectors.length > 0); + + uint64 globalInitializationCounter_ = $.globalInitializationCounter; + + uint256 expectedReinitializeDataLength = reinitializeSelectors.length - globalInitializationCounter_; + uint256 actualReinitializeDataLength = reinitializeData.length; + + require(expectedReinitializeDataLength > 0, AlreadyReinitialized()); + + require( + actualReinitializeDataLength == expectedReinitializeDataLength, + InvalidReinitializeDataLength(expectedReinitializeDataLength, actualReinitializeDataLength) + ); + + address implementation; + + assembly { + implementation := sload(0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) + } + + require(implementation != address(0), Eip1967NotDetected()); + + for (uint256 i; i < reinitializeData.length; ++i) { + bytes4 selector = bytes4(reinitializeData[i]); + + require( + selector == reinitializeSelectors[i + globalInitializationCounter_], + UnknownReinitializeSelector(selector) + ); + + assert(selector != bytes4(0)); + + (bool ok, bytes memory data) = implementation.delegatecall(reinitializeData[i]); + + if (!ok) { + assembly { + revert(add(32, data), mload(data)) + } + } + } + } +} diff --git a/src/etc/Versioned.sol b/src/etc/Versioned.sol new file mode 100644 index 00000000..59e5c609 --- /dev/null +++ b/src/etc/Versioned.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (etc/Versioned.sol) + +pragma solidity 0.8.29; + +/// @author See https://github.com/agglayer/vault-bridge +abstract contract Versioned { + /// @notice The version of the contract. + function VAULT_BRIDGE_PROTOCOL() public pure returns (string memory) { + return "1.0.0"; + } +} diff --git a/src/MigrationManager.sol b/src/primary-chain/MigrationManager.sol similarity index 53% rename from src/MigrationManager.sol rename to src/primary-chain/MigrationManager.sol index 3abd8914..0967c760 100644 --- a/src/MigrationManager.sol +++ b/src/primary-chain/MigrationManager.sol @@ -1,8 +1,10 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/MigrationManager.sol) + pragma solidity 0.8.29; /// @dev Main functionality. -import {IBridgeMessageReceiver} from "./etc/IBridgeMessageReceiver.sol"; +import {IBridgeMessageReceiver} from "../etc/IBridgeMessageReceiver.sol"; /// @dev Other functionality. import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; @@ -10,37 +12,39 @@ import {AccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable/acce import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ReentrancyGuardTransientUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; -import {IVersioned} from "./etc/IVersioned.sol"; +import {InitializationCounterUpgradeable} from "../etc/InitializationCounterUpgradeable.sol"; +import {Versioned} from "../etc/Versioned.sol"; /// @dev Libraries. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @dev External contracts. import {VaultBridgeToken} from "./VaultBridgeToken.sol"; -import {VaultBridgeTokenPart2} from "./VaultBridgeTokenPart2.sol"; -import {ILxLyBridge} from "./etc/ILxLyBridge.sol"; +import {IAgglayerBridge} from "../etc/IAgglayerBridge.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IWETH9} from "../etc/IWETH9.sol"; -// @remind Redocument. /// @title Migration Manager (singleton) /// @author See https://github.com/agglayer/vault-bridge -/// @notice Migration Manager is a singleton contract that lives on Layer X. -/// @notice Backing for custom tokens minted by Native Converters on Layer Ys can be migrated to Layer X using Migration Manager. Migration Manager completes migrations by calling `completeMigration` on the corresponidng vbToken, which mints vbToken and bridge them to address zero on the Layer Ys, effectively locking the backing in LxLy Bridge. Please refer to `onMessageReceived` for more information. +/// @notice Migration Manager is a singleton contract on Primary Chain. +/// @notice Backing for Custom Tokens minted by Native Converters on Secondary Chains can be migrated to Migration Manager on Primary Chain. Migration Manager completes migrations by calling `completeMigration` on the corresponidng vbToken, which mints vbToken and bridges it to address zero on the Secondary Chains, effectively locking the backing in Agglayer Bridge. Please refer to `onMessageReceived` for more information. +/// @dev This contract exists to prevent manipulation of vbTokens' internal accounting through reentrancy (specifically, claiming assets on Agglayer Bridge to vbToken mid-execution). contract MigrationManager is IBridgeMessageReceiver, Initializable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardTransientUpgradeable, - IVersioned + InitializationCounterUpgradeable, + Versioned { // Libraries. using SafeERC20 for IERC20; - /// @dev Used in cross-network communication. - enum CrossNetworkInstruction { - COMPLETE_MIGRATION, - WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION + /// @dev Used in cross-chain communication. + enum CrossChainInstruction { + _0_COMPLETE_MIGRATION, + _1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION } /// @dev Used for mapping Native Converters to vbTokens. @@ -53,10 +57,11 @@ contract MigrationManager is /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. /// @custom:storage-location erc7201:agglayer.vault-bridge.MigrationManager.storage struct MigrationManagerStorage { - ILxLyBridge lxlyBridge; - uint32 _lxlyId; - mapping(uint32 layerYLxLyId => mapping(address nativeConverter => TokenPair tokenPair)) + IAgglayerBridge agglayerBridge; + uint32 _agglayerId; + mapping(uint32 secondaryChainAgglayerId => mapping(address nativeConverter => TokenPair tokenPair)) nativeConvertersConfiguration; + IWETH9 _wrappedGasToken; } /// @dev The storage slot at which Migration Manager storage starts, following the EIP-7201 standard. @@ -64,53 +69,53 @@ contract MigrationManager is bytes32 private constant _MIGRATION_MANAGER_STORAGE = hex"30cf29e424d82bdf294fbec113ef39ac73137edfdb802b37ef3fc9ad433c5000"; - // @remind Redocument. - /// @dev The function selector for wrapping Layer X's gas token, following the WETH9 standard. - /// @dev (ATTENTION) If the method of wrapping the gas token for your Layer X differs, you cannot use this contract. - /// @dev Calculated as `bytes4(keccak256("deposit()"))`. - bytes4 private constant _UNDERLYING_TOKEN_WRAP_SELECTOR = hex"d0e30db0"; - // Basic roles. + // @remind Document. bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // Errors. error InvalidOwner(); - error InvalidLxLyBridge(); - error InvalidVbToken(); + error InvalidAgglayerBridge(); + error InvalidWrappedGasToken(); error NonMatchingInputLengths(); - error InvalidLayerYLxLyId(); + error InvalidSecondaryChainAgglayerId(); error InvalidNativeConverter(); error InvalidUnderlyingToken(); error Unauthorized(); - error CannotWrapGasToken(); error InsufficientUnderlyingTokenBalanceAfterWrapping(uint256 newBalance, uint256 expectedBalance); // Events. event NativeConverterConfigured( - uint32 indexed layerYLxlyId, address indexed nativeConverter, address indexed vbToken + uint32 indexed secondaryChainAgglayerId, address indexed nativeConverter, address indexed vbToken ); // -----================= ::: MODIFIERS ::: =================----- - /// @dev Checks if the sender is LxLy Bridge. - modifier onlyLxLyBridge() { + /// @dev Checks if the sender is Agglayer Bridge. + modifier onlyAgglayerBridge() { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); - require(msg.sender == address($.lxlyBridge), Unauthorized()); + require(msg.sender == address($.agglayerBridge), Unauthorized()); _; } + // -----================= ::: SOLIDITY ::: =================----- + + receive() external payable {} + // -----================= ::: SETUP ::: =================----- constructor() { _disableInitializers(); } - function initialize(address owner_, address lxlyBridge_) external initializer { + /// @notice Initializes the Migration Manager contract. + /// @param owner_ (ATTENTION) This address will be granted the `DEFAULT_ADMIN_ROLE`, as well as all basic roles. Roles can be modified at any time. + function reinitialize1(address owner_, address agglayerBridge_) external reinitializer(1) nonReentrant { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); // Check the inputs. require(owner_ != address(0), InvalidOwner()); - require(lxlyBridge_ != address(0), InvalidLxLyBridge()); + require(agglayerBridge_ != address(0), InvalidAgglayerBridge()); // Initialize the inherited contracts. __AccessControl_init(); @@ -124,32 +129,60 @@ contract MigrationManager is _grantRole(PAUSER_ROLE, owner_); // Initialize the storage. - $.lxlyBridge = ILxLyBridge(lxlyBridge_); - $._lxlyId = $.lxlyBridge.networkID(); + $.agglayerBridge = IAgglayerBridge(agglayerBridge_); + $._agglayerId = $.agglayerBridge.networkID(); } - // -----================= ::: SOLIDITY ::: =================----- + // @remind Document (the entire function). + /// @param wrappedGasToken_ The address of the wrapped gas token (e.g., WETH, if the gas token is ETH). Must be the same as the underlying token of the corresponding vbToken (e.g., of vbETH, if the gas token is ETH). + function reinitialize2(address wrappedGasToken_) external reinitializer(2) nonReentrant { + MigrationManagerStorage storage $ = _getMigrationManagerStorage(); - receive() external payable {} + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + require(wrappedGasToken_ != address(0), InvalidWrappedGasToken()); + + $._wrappedGasToken = IWETH9(wrappedGasToken_); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize3() + external + reinitializer(_incrementGlobalInitializationCounter(3)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](2); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } // -----================= ::: STORAGE ::: =================----- - /// @notice LxLy Bridge, which connects AggLayer networks. - function lxlyBridge() public view returns (ILxLyBridge) { + /// @notice Agglayer Bridge, which connects AggLayer networks. + function agglayerBridge() public view returns (IAgglayerBridge) { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); - return $.lxlyBridge; + return $.agglayerBridge; } - // @remind Redocument. - /// @notice Tells which vbToken and the underlying token Native Converter on Layer Ys belongs to. - /// @param nativeConverter The address of Native Converter on Layer Ys. - function nativeConvertersConfiguration(uint32 layerYLxlyId, address nativeConverter) + /// @notice Tells which vbToken Native Converter on Secondary Chain belongs to. + /// @param secondaryChainAgglayerId Secondary Chain's Agglayer ID. + /// @param nativeConverter The address of Native Converter on Secondary Chain. + function nativeConvertersConfiguration(uint32 secondaryChainAgglayerId, address nativeConverter) public view returns (TokenPair memory tokenPair) { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); - return $.nativeConvertersConfiguration[layerYLxlyId][nativeConverter]; + return $.nativeConvertersConfiguration[secondaryChainAgglayerId][nativeConverter]; } /// @dev Returns a pointer to the ERC-7201 storage namespace. @@ -161,37 +194,38 @@ contract MigrationManager is // -----================= ::: MIGRATION MANAGER ::: =================----- - // @remind Redocument (the entire function). - /// @notice Maps Native Converter on Layer Ys to vbToken and underlying token on Layer X. + /// @notice Maps Native Converters on Secondary Chains to vbToken and underlying token on Primary Chain. /// @dev CAUTION! Misconfiguration could allow an attacker to gain unauthorized access to vbToken and other contracts. /// @notice This function can be called by the owner only. - /// @param nativeConverters The address of Native Converter on Layer Ys. - /// @param vbToken The address of vbToken on Layer X Native Converter belongs to. To unmap the tokens, set to address zero. You can override tokens without unmapping them first. + /// @param secondaryChainAgglayerIds The Secondary Chains' Agglayer IDs. + /// @param nativeConverters The addresses of Native Converters on Secondary Chains. + /// @param vbToken The address of vbToken on Primary Chain Native Converter belongs to. Set to address zero to unset the tokens. You can override tokens without unsetting them first. function configureNativeConverters( - uint32[] calldata layerYLxlyIds, + uint32[] calldata secondaryChainAgglayerIds, address[] calldata nativeConverters, address payable vbToken ) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); // Check the inputs. - require(layerYLxlyIds.length == nativeConverters.length, NonMatchingInputLengths()); + require(secondaryChainAgglayerIds.length == nativeConverters.length, NonMatchingInputLengths()); - // @remind Document. - uint32 lxlyId = $._lxlyId; + // Cache Primary Chain Agglayer ID. + uint32 agglayerId = $._agglayerId; - for (uint256 i; i < layerYLxlyIds.length; ++i) { + for (uint256 i; i < secondaryChainAgglayerIds.length; ++i) { // Cache the inputs. - uint32 layerYLxlyId = layerYLxlyIds[i]; + uint32 secondaryChainAgglayerId = secondaryChainAgglayerIds[i]; address nativeConverter = nativeConverters[i]; // Check the inputs. - require(layerYLxlyId != lxlyId, InvalidLayerYLxLyId()); + require(secondaryChainAgglayerId != agglayerId, InvalidSecondaryChainAgglayerId()); require(nativeConverter != address(0), InvalidNativeConverter()); - TokenPair memory oldTokens = $.nativeConvertersConfiguration[layerYLxlyId][nativeConverter]; + // Cache the current tokens. + TokenPair memory oldTokens = $.nativeConvertersConfiguration[secondaryChainAgglayerId][nativeConverter]; - // Map or override tokens. + // Set or override tokens. /* Set tokens. */ if (vbToken != address(0)) { // Cache the tokens. @@ -206,7 +240,7 @@ contract MigrationManager is } // Set the tokens. - $.nativeConvertersConfiguration[layerYLxlyId][nativeConverter] = + $.nativeConvertersConfiguration[secondaryChainAgglayerId][nativeConverter] = TokenPair(VaultBridgeToken(vbToken), underlyingToken); // Approve vbToken. @@ -218,36 +252,36 @@ contract MigrationManager is oldTokens.underlyingToken.forceApprove(address(oldTokens.vbToken), 0); // Unset the tokens. - delete $.nativeConvertersConfiguration[layerYLxlyId][nativeConverter]; + delete $.nativeConvertersConfiguration[secondaryChainAgglayerId][nativeConverter]; } // Emit the event. - emit NativeConverterConfigured(layerYLxlyId, nativeConverter, vbToken); + emit NativeConverterConfigured(secondaryChainAgglayerId, nativeConverter, vbToken); } } - // @remind Redocument (the entire function). - /// @dev Native Converters on a Layer Ys call both `bridgeAsset` and `bridgeMessage` on LxLy Bridge to `migrateBackingToLayerX`. - /// @dev The asset must be claimed before the message on LxLy Bridge. - /// @dev The message tells Migration Manager on Layer X how much custom token must be backed by vbToken, which is minted and bridged to address zero on the respective Layer Y. This action provides liquidity when bridging the custom token to from Layer Ys to Layer X and increments the pessimistic proof. + /// @dev When Native Converter migrates backing, it calls both `bridgeAsset` and `bridgeMessage` on Agglayer Bridge to `migrateBackingToPrimaryChain`. + /// @dev The asset must be claimed before the message on Agglayer Bridge. + /// @dev The message tells vbToken how much Custom Token must be backed by vbToken, which is minted and bridged to address zero on the respective Secondary Chain. This action provides liquidity when bridging Custom Token to from Secondary Chains to Primary Chain and increments the pessimistic proof. + /// @dev This function can be called by Agglayer Bridge only. function onMessageReceived(address originAddress, uint32 originNetwork, bytes memory data) external payable whenNotPaused - onlyLxLyBridge + onlyAgglayerBridge nonReentrant { MigrationManagerStorage storage $ = _getMigrationManagerStorage(); - // Decode the cross-network instruction. - (CrossNetworkInstruction instruction, bytes memory instructionData) = - abi.decode(data, (CrossNetworkInstruction, bytes)); + // Decode the cross-chain instruction. + (CrossChainInstruction instruction, bytes memory instructionData) = + abi.decode(data, (CrossChainInstruction, bytes)); // Dispatch. /* Complete migration. */ if ( - instruction == CrossNetworkInstruction.COMPLETE_MIGRATION - || instruction == CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION + instruction == CrossChainInstruction._0_COMPLETE_MIGRATION + || instruction == CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION ) { // Cache vbToken. VaultBridgeToken vbToken = $.nativeConvertersConfiguration[originNetwork][originAddress].vbToken; @@ -259,23 +293,24 @@ contract MigrationManager is (uint256 shares, uint256 assets) = abi.decode(instructionData, (uint256, uint256)); // Wrap the gas token if instructed. - if (instruction == CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION) { + if (instruction == CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION) { // Cache the underlying token. IERC20 underlyingToken = $.nativeConvertersConfiguration[originNetwork][originAddress].underlyingToken; + // Check the input. + require(address(underlyingToken) == address($._wrappedGasToken), Unauthorized()); + // Cache the previous balance. uint256 previousBalance = underlyingToken.balanceOf(address(this)); // Wrap the gas token. - (bool ok,) = - address(underlyingToken).call{value: assets}(abi.encodePacked(_UNDERLYING_TOKEN_WRAP_SELECTOR)); + $._wrappedGasToken.deposit{value: assets}(); // Cache the result. uint256 expectedBalance = previousBalance + assets; uint256 newBalance = underlyingToken.balanceOf(address(this)); // Check the result. - require(ok, CannotWrapGasToken()); require( newBalance == expectedBalance, InsufficientUnderlyingTokenBalanceAfterWrapping(newBalance, expectedBalance) @@ -283,7 +318,7 @@ contract MigrationManager is } // Complete the migration. - VaultBridgeTokenPart2(payable(address(vbToken))).completeMigration(originNetwork, shares, assets); + vbToken.completeMigration(originNetwork, shares, assets); } } @@ -300,11 +335,4 @@ contract MigrationManager is function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { _unpause(); } - - // -----================= ::: INFO ::: =================----- - - /// @inheritdoc IVersioned - function version() external pure returns (string memory) { - return "0.5.0"; - } } diff --git a/src/primary-chain/VaultBridgeToken.sol b/src/primary-chain/VaultBridgeToken.sol new file mode 100644 index 00000000..4007b84a --- /dev/null +++ b/src/primary-chain/VaultBridgeToken.sol @@ -0,0 +1,1352 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/VaultBridgeToken.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {ERC20PermitUpgradeable} from + "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {IVaultBridgeTokenInitializer} from "../etc/IVaultBridgeTokenInitializer.sol"; + +// Other functionality. +import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import {AccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {ReentrancyGuardTransientUpgradeable} from + "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardTransientUpgradeable.sol"; +import {ERC20PermitUser} from "../etc/ERC20PermitUser.sol"; +import {InitializationCounterUpgradeable} from "../etc/InitializationCounterUpgradeable.sol"; +import {Versioned} from "../etc/Versioned.sol"; + +// Libraries. +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +// External contracts. +import {IAgglayerBridge} from "../etc/IAgglayerBridge.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +// Other. +import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/// @title Vault Bridge Token +/// @author See https://github.com/agglayer/vault-bridge +/// @notice A vbToken is an ERC-20 token, ERC-4626 vault, and Agglayer Bridge extension, enabling deposits and bridging of select assets, such as WBTC, WETH, USDT, USDC, and USDS, while putting the assets to work to produce yield. +/// @dev A base contract used to create vbTokens. +/// @dev @note IMPORTANT: In order to not drive the complexity of the Vault Bridge protocol up, vbToken MUST NOT have transfer, deposit, or withdrawal fees. The underlying token on Primary Chain MUST NOT have a transfer fee; this contract will revert if it detects a transfer fee. The underlying token and Custom Token on Secondary Chains MAY have transfer fees. The yield vault SHOULD NOT have deposit and/or withdrawal fees; however, it is expected that produced yield will offset any costs incurred when depositing to and withdrawing from the yield vault for the purpose of producing yield or rebalancing the internal reserve. The price of the yield vault's shares MUST NOT decrease (e.g., there is no bad debt realization); still, this contract implements solvency checks for protection with a configurable slippage parameter. Additionally, the underlying token MUST NOT be a rebasing token, and MUST NOT have transfer hooks (i.e., does not enable reentrancy/crossentrancy). +abstract contract VaultBridgeToken is + Initializable, + AccessControlUpgradeable, + PausableUpgradeable, + ReentrancyGuardTransientUpgradeable, + IERC4626, + ERC20PermitUpgradeable, + ERC20PermitUser, + InitializationCounterUpgradeable, + Versioned +{ + // Libraries. + using SafeERC20 for IERC20; + + /// @dev Storage of Vault Bridge Token contract. + /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. + /// @custom:storage-location erc7201:agglayer.vault-bridge.VaultBridgeToken.storage + struct VaultBridgeTokenStorage { + IERC20 underlyingToken; + uint8 decimals; + uint256 minimumReservePercentage; + uint256 reservedAssets; + IERC4626 yieldVault; + address yieldRecipient; + uint256 _netCollectedYield; + uint32 agglayerId; + IAgglayerBridge agglayerBridge; + uint256 migrationFeesFund; + uint256 minimumYieldVaultDeposit; + address migrationManager; + uint256 yieldVaultMaximumSlippagePercentage; + address _vaultBridgeTokenPart2; + } + + /// @dev Parameters for initializing Vault Bridge Token contract. + /// @dev @note (ATTENTION) `decimals` will match the underlying token. Defaults to 18 decimals if the underlying token reverts on `decimals`. + /// @param owner (ATTENTION) This address will be granted the `DEFAULT_ADMIN_ROLE`, as well as all basic roles. Roles can be modified at any time. + /// @param minimumReservePercentage vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). `1e18` is 100%. @note (ATTENTION) Automatic reserve rebalancing will be disabled for values greater than `1e18` (100%). + /// @param yieldVault An external, ERC-4246 compliant vault into which the underlying token is deposited to produce yield. + /// @param yieldRecipient The address that receives yield produced by the yield vault. The yield collector collects yield, while the yield recipient receives it. + /// @param minimumYieldVaultDeposit The minimum amount of the underlying token that triggers a yield vault deposit. Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. The limit does not apply when rebalancing the reserve. Set to `0` to disable. + /// @param yieldVaultMaximumSlippagePercentage The maximum slippage percentage when depositing into or withdrawing from the yield vault. @note IMPORTANT: Any losses incurred due to slippage (and not fully covered by produced yield) will need to be covered by whomever is responsible for this contract. `1e18` is 100%. The recommended value is `0.01e18` (1%). + struct InitializationParameters { + address owner; + string name; + string symbol; + address underlyingToken; + uint256 minimumReservePercentage; + address yieldVault; + address yieldRecipient; + address agglayerBridge; + uint256 minimumYieldVaultDeposit; + address migrationManager; + uint256 yieldVaultMaximumSlippagePercentage; + address vaultBridgeTokenPart2; + } + + // Basic roles. + // @remind Document. + bytes32 public constant REBALANCER_ROLE = keccak256("REBALANCER_ROLE"); + bytes32 public constant YIELD_COLLECTOR_ROLE = keccak256("YIELD_COLLECTOR_ROLE"); + bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + + /// @dev The storage slot at which Vault Bridge Token storage starts, following the EIP-7201 standard. + /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.VaultBridgeToken.storage")) - 1)) & ~bytes32(uint256(0xff))`. + bytes32 private constant _VAULT_BRIDGE_TOKEN_STORAGE = + hex"f082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44700"; + + // Errors. + error Unauthorized(); + error InvalidInitializer(); + error InvalidOwner(); + error InvalidName(); + error InvalidSymbol(); + error InvalidUnderlyingToken(); + error InvalidMinimumReservePercentage(); + error InvalidYieldVault(); + error InvalidYieldRecipient(); + error InvalidAgglayerBridge(); + error InvalidMigrationManager(); + error InvalidYieldVaultMaximumSlippagePercentage(); + error InvalidVaultBridgeTokenPart2(); + error InvalidAssets(); + error InvalidDestinationNetworkId(); + error InvalidReceiver(); + error InvalidPermitData(); + error InvalidShares(); + error IncorrectAmountOfSharesMinted(uint256 mintedShares, uint256 requiredShares); + error AssetsTooLarge(uint256 availableAssets, uint256 requestedAssets); + error IncorrectAmountOfSharesRedeemed(uint256 redeemedShares, uint256 requiredShares); + error CannotRebalanceReserve(); + error NoNeedToRebalanceReserve(); + error NoYield(); + error InvalidOriginNetwork(); + error CannotCompleteMigration(uint256 requiredAssets, uint256 receivedAssets, uint256 assetsInMigrationFund); + error YieldVaultRedemptionFailed(uint256 sharesToRedeem, uint256 redemptionLimit); + error MinimumYieldVaultDepositNotMet(uint256 assetsToDeposit, uint256 minimumYieldVaultDeposit); + error YieldVaultDepositFailed(uint256 assetsToDeposit, uint256 depositLimit); + error InsufficientYieldVaultSharesMinted(uint256 depositedAssets, uint256 mintedShares); + error UnknownError(bytes data); + error YieldVaultWithdrawalFailed(uint256 assetsToWithdraw, uint256 withdrawalLimit); + error ExcessiveYieldVaultSharesBurned(uint256 burnedShares, uint256 withdrawnAssets); + error InsufficientUnderlyingTokenReceived(uint256 receivedAssets, uint256 requestedAssets); + error UnknownFunction(bytes4 functionSelector); + + // Events. + event ReserveRebalanced(uint256 oldReservedAssets, uint256 newReservedAssets, uint256 reservePercentage); + event YieldCollected(address indexed yieldRecipient, uint256 vbTokenAmount); + event Burned(uint256 vbTokenAmount); + event DonatedAsYield(address indexed who, uint256 assets); + event DonatedForCompletingMigration(address indexed who, uint256 assets); + event MigrationCompleted( + uint32 indexed originNetwork, + uint256 indexed shares, + uint256 indexed assets, + uint256 migrationFeesFundUtilization + ); + event YieldRecipientSet(address indexed yieldRecipient); + event MinimumReservePercentageSet(uint256 minimumReservePercentage); + event YieldVaultDrained(uint256 redeemedShares, uint256 receivedAssets); + event YieldVaultSet(address yieldVault); + event YieldVaultMaximumSlippagePercentageSet(uint256 slippagePercentage); + + // -----================= ::: MODIFIERS ::: =================----- + + /// @dev Checks if the sender is the yield recipient. + modifier onlyYieldRecipient() { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + require(msg.sender == $.yieldRecipient, Unauthorized()); + _; + } + + /// @dev Checks if the sender is Migration Manager. + modifier onlyMigrationManager() { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + require(msg.sender == $.migrationManager, Unauthorized()); + _; + } + + /// @dev Checks if the sender is the vbToken itself. + modifier onlySelf() { + require(msg.sender == address(this), Unauthorized()); + _; + } + + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + modifier delegatedToPart2() { + _; + _delegateToPart2(); + } + + // -----================= ::: SETUP ::: =================----- + + /// @param initializer_ The address of `VaultBridgeTokenInitializer`. + /// @param initParams Please refer to `InitializationParameters` for more information. + function __VaultBridgeToken_init1(address initializer_, InitializationParameters calldata initParams) + internal + onlyInitializing + { + // Check the input. + require(initializer_ != address(0), InvalidInitializer()); + + // Verify the version of the initializer. + // The version string must be the same as that of this contract. + require( + keccak256(bytes(VaultBridgeToken(initializer_).VAULT_BRIDGE_PROTOCOL())) + == keccak256(bytes(VAULT_BRIDGE_PROTOCOL())), + InvalidInitializer() + ); + + // Initialize the contract using the external initializer. + (bool ok, bytes memory data) = + initializer_.delegatecall(abi.encodeCall(IVaultBridgeTokenInitializer.initialize, (initParams))); + + // Check the result. + if (!ok) { + // If the call failed, bubble up the revert data. + assembly ("memory-safe") { + revert(add(32, data), mload(data)) + } + } + } + + // @remind Document. + function __VaultBridgeToken_init2() + internal + onlyInitializing + incrementsLocalInitializationCounter(1) + incrementsLocalInitializationCounter(2) + { + // Empty function body. + } + + /* + /// @dev How to add a new init step: + function __VaultBridgeToken_init3() internal onlyInitializing incrementsLocalInitializationCounter(3) {} + */ + + // @remind Document. + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure virtual; + + // -----================= ::: STORAGE ::: =================----- + + /// @notice The underlying token that backs vbToken. + function underlyingToken() public view returns (IERC20) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.underlyingToken; + } + + /// @notice The number of decimals of vbToken. + /// @notice The number of decimals is the same as that of the underlying token, or `18` if the underlying token reverted on `decimals`. + function decimals() public view override(ERC20Upgradeable, IERC20Metadata) returns (uint8) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.decimals; + } + + /// @notice vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). + /// @notice The owner can rebalance the reserve by calling `rebalanceReserve` when it is below or above the `minimumReservePercentage`. The reserve may also be rebalanced automatically on deposits and withdrawals. + /// @return 1e18 is 100%. + function minimumReservePercentage() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.minimumReservePercentage; + } + + /// @notice vbTokens can maintain an internal reserve of the underlying token for serving withdrawals from first (as opposed to staking all assets). + /// @notice How much of the underlying token is in the internal reserve. + function reservedAssets() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.reservedAssets; + } + + /// @notice An external, ERC-4246 compliant vault into which the underlying token is deposited to produce yield. + function yieldVault() public view returns (IERC4626) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.yieldVault; + } + + /// @notice The address that receives yield produced by the yield vault. + /// @notice The yield collector collects yield, while the yield recipient receives it. + function yieldRecipient() public view returns (address) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.yieldRecipient; + } + + /// @notice The Agglayer ID of this network. + function agglayerId() public view returns (uint32) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.agglayerId; + } + + /// @notice Agglayer Bridge, which connects AggLayer networks. + function agglayerBridge() public view returns (IAgglayerBridge) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.agglayerBridge; + } + + /// @notice A dedicated fund for covering any fees on Secondary Chain during a migration of backing to Primary Chain. Please refer to `completeMigration` for more information. + function migrationFeesFund() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.migrationFeesFund; + } + + /// @notice The minimum amount of the underlying token that triggers a yield vault deposit. + /// @dev Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. + /// @dev The limit does not apply when rebalancing the reserve. + function minimumYieldVaultDeposit() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.minimumYieldVaultDeposit; + } + + /// @notice The address of Migration Manager. Please refer to `MigrationManager.sol` for more information. + function migrationManager() public view returns (address) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.migrationManager; + } + + /// @notice The maximum slippage percentage when depositing into or withdrawing from the yield vault. + /// @return 1e18 is 100%. + function yieldVaultMaximumSlippagePercentage() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.yieldVaultMaximumSlippagePercentage; + } + + /// @dev Returns a pointer to the ERC-7201 storage namespace. + function _getVaultBridgeTokenStorage() private pure returns (VaultBridgeTokenStorage storage $) { + assembly { + $.slot := _VAULT_BRIDGE_TOKEN_STORAGE + } + } + + // -----================= ::: ERC-4626 ::: =================----- + + /// @notice The underlying token that backs vbToken. + function asset() public view returns (address assetTokenAddress) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return address($.underlyingToken); + } + + /// @notice The total backing of vbToken in the underlying token in real-time. + function totalAssets() public view returns (uint256 totalManagedAssets) { + return stakedAssets() + reservedAssets(); + } + + /// @notice Tells how much a specific amount of underlying token is worth in vbToken. + /// @dev The underlying token backs vbToken 1:1. + function convertToShares(uint256 assets) public pure returns (uint256 shares) { + // @note CAUTION! Changing this function will affect the conversion rate for the entire contract, and may introduce bugs. + shares = assets; + } + + /// @notice Tells how much a specific amount of vbToken is worth in the underlying token. + /// @dev vbToken is backed by the underlying token 1:1. + function convertToAssets(uint256 shares) public pure returns (uint256 assets) { + // @note CAUTION! Changing this function will affect the conversion rate for the entire contract, and may introduce bugs. + assets = shares; + } + + /// @notice How much underlying token can deposited for a specific user right now. (Depositing the underlying token mints vbToken). + function maxDeposit(address) external view returns (uint256 maxAssets) { + return paused() ? 0 : type(uint256).max; + } + + /// @notice How much vbToken would be minted if a specific amount of the underlying token were deposited right now. + function previewDeposit(uint256 assets) external view whenNotPaused returns (uint256 shares) { + // Check the input. + require(assets > 0, InvalidAssets()); + + return convertToShares(assets); + } + + /// @notice Deposit a specific amount of the underlying token and mint vbToken. + function deposit(uint256 assets, address receiver) external whenNotPaused nonReentrant returns (uint256 shares) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + (shares,) = _deposit(assets, $.agglayerId, receiver, false, 0); + } + + /// @notice Deposit a specific amount of the underlying token, and bridge minted vbToken to another network. + /// @dev The `receiver` in the ERC-4626 `Deposit` event will be this contract. + function depositAndBridge( + uint256 assets, + address receiver, + uint32 destinationNetworkId, + bool forceUpdateGlobalExitRoot + ) external whenNotPaused nonReentrant returns (uint256 shares) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the input. + require(destinationNetworkId != $.agglayerId, InvalidDestinationNetworkId()); + + (shares,) = _deposit(assets, destinationNetworkId, receiver, forceUpdateGlobalExitRoot, 0); + } + + /// @notice Locks the underlying token, mints vbToken, and optionally bridges it to another network. + /// @param maxShares Caps the amount of vbToken that is minted. Unused underlying token will be refunded to the sender. Set to `0` to disable. + /// @dev If bridging to another network, the `receiver` in the ERC-4626 `Deposit` event will be this contract. + function _deposit( + uint256 assets, + uint32 destinationNetworkId, + address receiver, + bool forceUpdateGlobalExitRoot, + uint256 maxShares + ) internal returns (uint256 shares, uint256 spentAssets) { + return _depositUsingCustomReceivingFunction( + _receiveUnderlyingToken, assets, destinationNetworkId, receiver, forceUpdateGlobalExitRoot, maxShares + ); + } + + /// @notice Locks the underlying token, mints vbToken, and optionally bridges it to another network. + /// @param receiveUnderlyingToken A custom function to use for receiving the underlying token from the sender. @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. @note IMPORTANT: The function MUST detect and revert if there was a transfer fee. + /// @param maxShares Caps the amount of vbToken that is minted. Unused underlying token will be refunded to the sender. Set to `0` to disable. + /// @dev If bridging to another network, the `receiver` in the ERC-4626 `Deposit` event will be this contract. + function _depositUsingCustomReceivingFunction( + function(address, uint256) internal receiveUnderlyingToken, + uint256 assets, + uint32 destinationNetworkId, + address receiver, + bool forceUpdateGlobalExitRoot, + uint256 maxShares + ) internal returns (uint256 shares, uint256 spentAssets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the inputs. + require(assets > 0, InvalidAssets()); + require(receiver != address(0), InvalidReceiver()); + require(receiver != address(this), InvalidReceiver()); + + // Transfer the underlying token from the sender to self. + receiveUnderlyingToken(msg.sender, assets); + + // Check for a refund. + if (maxShares > 0) { + // Calculate the required amount of the underlying token. + uint256 requiredAssets = convertToAssets(maxShares); + + if (assets > requiredAssets) { + // Calculate the difference. + uint256 refund = assets - requiredAssets; + + // Refund the difference. + _sendUnderlyingToken(msg.sender, refund); + + // Update the `assets`. + assets = requiredAssets; + } + } + + // Set the return values. + shares = convertToShares(assets); + spentAssets = assets; + + // Calculate the amount to reserve. + uint256 assetsToReserve = _calculateAmountToReserve(assets, shares); + + // Calculate the amount to try to deposit into the yield vault. + uint256 assetsToDeposit = assets - assetsToReserve; + + // Try to deposit into the yield vault. + if (assetsToDeposit > 0) { + // Deposit, and update the amount to reserve if necessary. + assetsToReserve += _depositIntoYieldVault(assetsToDeposit, false); + } + + // Update the reserve. + $.reservedAssets += assetsToReserve; + + // Mint vbToken. + if (destinationNetworkId != $.agglayerId) { + // Mint to self. + _mint(address(this), shares); + + // Bridge to the receiver. + $.agglayerBridge.bridgeAsset( + destinationNetworkId, receiver, shares, address(this), forceUpdateGlobalExitRoot, "" + ); + + // Update the receiver. + receiver = address(this); + } else { + // Mint to the receiver. + _mint(receiver, shares); + } + + // Emit the ERC-4626 event. + emit IERC4626.Deposit(msg.sender, receiver, assets, shares); + + // Cache the reserve percentage. + uint256 reservePercentage_ = reservePercentage(); + + // Check if the reserve needs to be rebalanced. + if ( + $.minimumReservePercentage < 1e18 && reservePercentage_ > 3 * $.minimumReservePercentage + && reservePercentage_ > 0.1e18 + ) { + // Rebalance the reserve. + _rebalanceReserve(false, true); + } + } + + /// @notice Deposit a specific amount of the underlying token and mint vbToken. + /// @dev Uses EIP-2612 permit to transfer the underlying token from the sender to self. + function depositWithPermit(uint256 assets, address receiver, bytes calldata permitData) + external + whenNotPaused + nonReentrant + returns (uint256 shares) + { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + (shares,) = _depositWithPermit(assets, permitData, $.agglayerId, receiver, false, 0); + } + + /// @notice Deposit a specific amount of the underlying token, and bridge minted vbToken to another network. + /// @dev Uses EIP-2612 permit to transfer the underlying token from the sender to self. + /// @dev The `receiver` in the ERC-4626 `Deposit` event will be this contract. + function depositWithPermitAndBridge( + uint256 assets, + address receiver, + uint32 destinationNetworkId, + bool forceUpdateGlobalExitRoot, + bytes calldata permitData + ) external whenNotPaused nonReentrant returns (uint256 shares) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the input. + require(destinationNetworkId != $.agglayerId, InvalidDestinationNetworkId()); + + (shares,) = _depositWithPermit(assets, permitData, destinationNetworkId, receiver, forceUpdateGlobalExitRoot, 0); + } + + /// @notice Locks the underlying token, mints vbToken, and optionally bridges it to another network. + /// @param maxShares Caps the amount of vbToken that is minted. Unused underlying token will be refunded to the sender. Set to `0` to disable. + /// @dev Uses EIP-2612 permit to transfer the underlying token from the sender to self. + function _depositWithPermit( + uint256 assets, + bytes calldata permitData, + uint32 destinationNetworkId, + address receiver, + bool forceUpdateGlobalExitRoot, + uint256 maxShares + ) internal returns (uint256 shares, uint256 spentAssets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the input. + require(permitData.length > 0, InvalidPermitData()); + + // Use the permit. + _permit(address($.underlyingToken), assets, permitData); + + return _deposit(assets, destinationNetworkId, receiver, forceUpdateGlobalExitRoot, maxShares); + } + + /// @notice How much vbToken can be minted to a specific user right now. (Minting vbToken locks the underlying token). + function maxMint(address) external view returns (uint256 maxShares) { + return paused() ? 0 : type(uint256).max; + } + + /// @notice How much underlying token would be required to mint a specific amount of vbToken right now. + function previewMint(uint256 shares) external view whenNotPaused returns (uint256 assets) { + // Check the input. + require(shares > 0, InvalidShares()); + + return convertToAssets(shares); + } + + /// @notice Mint a specific amount of vbToken by locking the required amount of the underlying token. + function mint(uint256 shares, address receiver) external whenNotPaused nonReentrant returns (uint256 assets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the input. + require(shares > 0, InvalidShares()); + + // Mint vbToken to the receiver. + uint256 mintedShares; + (mintedShares, assets) = _deposit(convertToAssets(shares), $.agglayerId, receiver, false, shares); + + // Check the output. + require(mintedShares == shares, IncorrectAmountOfSharesMinted(mintedShares, shares)); + } + + /// @notice How much underlying token can be withdrawn from a specific user right now. (Withdrawing the underlying token burns vbToken). + function maxWithdraw(address owner) external view returns (uint256 maxAssets) { + // Return zero if the contract is paused. + if (paused()) return 0; + + // Return zero if the balance is zero. + uint256 shares = balanceOf(owner); + if (shares == 0) return 0; + + // Return the maximum amount that can be withdrawn. + return _simulateWithdraw(convertToAssets(shares), false); + } + + /// @notice How much vbToken would be burned if a specific amount of the underlying token were withdrawn right now. + function previewWithdraw(uint256 assets) external view whenNotPaused returns (uint256 shares) { + return convertToShares(_simulateWithdraw(assets, true)); + } + + /// @dev Calculates the amount of the underlying token that could be withdrawn right now. + /// @dev This function is used for estimation purposes only. + /// @dev @note IMPORTANT: `reservedAssets` must be up-to-date before using this function! + /// @param assets The maximum amount of the underlying token to simulate a withdrawal for. + /// @param force Whether to revert if the all of the `assets` would not be withdrawn. + function _simulateWithdraw(uint256 assets, bool force) internal view returns (uint256 withdrawnAssets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the input. + require(assets > 0, InvalidAssets()); + + // The amount that cannot be withdrawn at the moment. + uint256 remainingAssets = assets; + + // Simulate a withdrawal from the reserve. + if ($.reservedAssets >= remainingAssets) return assets; + remainingAssets -= $.reservedAssets; + + // Calculate the amount to preview a withdrawal from the yield vault for. + uint256 maxWithdraw_ = $.yieldVault.maxWithdraw(address(this)); + maxWithdraw_ = remainingAssets > maxWithdraw_ ? maxWithdraw_ : remainingAssets; + + // Simulate a withdrawal from the yield vault. + uint256 burnedYieldVaultShares; + try $.yieldVault.previewWithdraw(maxWithdraw_) returns (uint256 shares) { + // Capture the amount of the yield vault shares that would be burned. + burnedYieldVaultShares = shares; + } catch (bytes memory data) { + // If `previewWithdraw` reverted, and all of the `assets` must be withdrawn, bubble up the revert data. + if (force) { + assembly ("memory-safe") { + revert(add(32, data), mload(data)) + } + } + // Otherwise, return the reserved assets. + else { + return $.reservedAssets; + } + } + + // Perform the same solvency check as `_withdrawFromYieldVault` would. + bool solvencyCheckPassed = Math.mulDiv( + convertToAssets(totalSupply() + yield()) - reservedAssets(), burnedYieldVaultShares, maxWithdraw_ + ) <= Math.mulDiv($.yieldVault.balanceOf(address(this)), 1e18 + $.yieldVaultMaximumSlippagePercentage, 1e18); + + // Revert if the solvency check failed and all of the `assets` must be withdrawn. + if (!solvencyCheckPassed) { + if (force) revert ExcessiveYieldVaultSharesBurned(burnedYieldVaultShares, maxWithdraw_); + return $.reservedAssets; + } + + // Return if all of the `assets` would be withdrawn. + if (remainingAssets == maxWithdraw_) return assets; + remainingAssets -= maxWithdraw_; + + // Set the return value (the amount of the underlying token that can be withdrawn right now). + withdrawnAssets = assets - remainingAssets; + + // Revert if all of the `assets` must have been withdrawn and there is a remaining amount. + if (force) require(remainingAssets == 0, AssetsTooLarge(withdrawnAssets, assets)); + } + + /// @notice Withdraw a specific amount of the underlying token by burning the required amount of vbToken. + function withdraw(uint256 assets, address receiver, address owner) + external + whenNotPaused + nonReentrant + returns (uint256 shares) + { + return _withdraw(assets, receiver, owner); + } + + /// @notice Withdraw a specific amount of the underlying token by burning the required amount of vbToken. + function _withdraw(uint256 assets, address receiver, address owner) internal returns (uint256 shares) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check the inputs. + require(assets > 0, InvalidAssets()); + require(receiver != address(0), InvalidReceiver()); + require(owner != address(0), InvalidOwner()); + + // Cache the total supply, uncollected yield, and reserved assets. + uint256 originalTotalSupply = totalSupply(); + uint256 originalUncollectedYield = yield(); + uint256 originalReservedAssets = $.reservedAssets; + + // Set the return value. + shares = convertToShares(assets); + + // Check the input. + if (msg.sender != owner) _spendAllowance(owner, msg.sender, shares); + + // The amount that cannot be withdrawn at the moment. + uint256 remainingAssets = assets; + + // Calculate the amount to withdraw from the reserve. + uint256 amountToWithdraw = originalReservedAssets > remainingAssets ? remainingAssets : originalReservedAssets; + + // Withdraw the underlying token from the reserve. + if (amountToWithdraw > 0) { + // Update the reserve. + $.reservedAssets -= amountToWithdraw; + + // Update the remaining assets. + remainingAssets -= amountToWithdraw; + } + + uint256 receivedAssets; + + if (remainingAssets != 0) { + // Calculate the amount to withdraw from the yield vault. + uint256 maxWithdraw_ = $.yieldVault.maxWithdraw(address(this)); + + // Withdraw the underlying token from the yield vault. + if (maxWithdraw_ >= remainingAssets) { + // Withdraw to this contract. + (, receivedAssets) = _withdrawFromYieldVault( + remainingAssets, + true, + address(this), + originalTotalSupply, + originalUncollectedYield, + originalReservedAssets + ); + } else { + // Update the remaining assets. + remainingAssets -= maxWithdraw_; + + // Revert because all of the `assets` could not be withdrawn. + revert AssetsTooLarge(assets - remainingAssets, assets); + } + } + + // Burn vbToken. + _burn(owner, shares); + + // Send the underlying token to the receiver. + _sendUnderlyingToken(receiver, amountToWithdraw + receivedAssets); + + // Emit the ERC-4626 event. + emit IERC4626.Withdraw(msg.sender, receiver, owner, assets, shares); + + // Check if the reserve needs to be rebalanced. + if ($.minimumReservePercentage < 1e18 && reservePercentage() <= 0.01e18 && $.minimumReservePercentage >= 0.1e18) + { + // Rebalance the reserve. + _rebalanceReserve(false, false); + } + } + + /// @notice How much vbToken can be redeemed for a specific user. (Redeeming vbToken burns it and unlocks the underlying token). + function maxRedeem(address owner) external view returns (uint256 maxShares) { + // Return zero if the contract is paused. + if (paused()) return 0; + + // Return zero if the balance is zero. + uint256 shares = balanceOf(owner); + if (shares == 0) return 0; + + // Return the maximum amount that can be redeemed. + return convertToShares(_simulateWithdraw(convertToAssets(shares), false)); + } + + /// @notice How much underlying token would be unlocked if a specific amount of vbToken were redeemed and burned right now. + function previewRedeem(uint256 shares) external view whenNotPaused returns (uint256 assets) { + // Check the input. + require(shares > 0, InvalidShares()); + + return _simulateWithdraw(convertToAssets(shares), true); + } + + /// @notice Burn a specific amount of vbToken and unlock the respective amount of the underlying token. + function redeem(uint256 shares, address receiver, address owner) + external + whenNotPaused + nonReentrant + returns (uint256 assets) + { + // Check the input. + require(shares > 0, InvalidShares()); + + // Set the return value. + assets = convertToAssets(shares); + + // Burn vbToken and unlock the underlying token. + uint256 redeemedShares = _withdraw(assets, receiver, owner); + + // Check the output. + require(redeemedShares == shares, IncorrectAmountOfSharesRedeemed(redeemedShares, shares)); + } + + // @todo Deprecation warning? + /// @notice Claim vbToken from Agglayer Bridge and redeem it. + function claimAndRedeem( + bytes32[32] calldata smtProofLocalExitRoot, + bytes32[32] calldata smtProofRollupExitRoot, + uint256 globalIndex, + bytes32 mainnetExitRoot, + bytes32 rollupExitRoot, + address destinationAddress, + uint256 amount, + address receiver, + bytes calldata metadata + ) external whenNotPaused nonReentrant returns (uint256 assets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Claim vbToken from Agglayer Bridge. + $.agglayerBridge.claimAsset( + smtProofLocalExitRoot, + smtProofRollupExitRoot, + globalIndex, + mainnetExitRoot, + rollupExitRoot, + $.agglayerId, + address(this), + $.agglayerId, + destinationAddress, + amount, + metadata + ); + + // Set the return value. + assets = convertToAssets(amount); + + // Burn vbToken and unlock the underlying token. + uint256 redeemedShares = _withdraw(assets, receiver, destinationAddress); + + // Check the output. + require(redeemedShares == amount, IncorrectAmountOfSharesRedeemed(redeemedShares, amount)); + } + + // -----================= ::: ERC-20 ::: =================----- + + /// @dev Pausable ERC-20 `transfer` function. + function transfer(address to, uint256 value) + public + override(ERC20Upgradeable, IERC20) + whenNotPaused + returns (bool) + { + return ERC20Upgradeable.transfer(to, value); + } + + /// @dev Pausable ERC-20 `transferFrom` function. + function transferFrom(address from, address to, uint256 value) + public + override(ERC20Upgradeable, IERC20) + whenNotPaused + returns (bool) + { + return ERC20Upgradeable.transferFrom(from, to, value); + } + + /// @dev Pausable ERC-20 `approve` function. + function approve(address spender, uint256 value) + public + override(ERC20Upgradeable, IERC20) + whenNotPaused + returns (bool) + { + return ERC20Upgradeable.approve(spender, value); + } + + /// @dev Pausable ERC-20 Permit `permit` function. + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + public + override + whenNotPaused + { + super.permit(owner, spender, value, deadline, v, r, s); + } + + // -----================= ::: VAULT BRIDGE TOKEN ::: =================----- + + /// @notice The amount of the underlying token in the yield vault, as reported by the yield vault in real-time. + function stakedAssets() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + return $.yieldVault.convertToAssets($.yieldVault.balanceOf(address(this))); + } + + /// @notice The reserve percentage in real-time. + /// @notice The reserve is based on the total supply of vbToken, and does not account for uncompleted migrations of backing from Secondary Chains to Primary Chain. Please refer to `completeMigration` for more information. + function reservePercentage() public view returns (uint256) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Return zero if the total supply is zero. + if (totalSupply() == 0) return 0; + + // Calculate the reserve percentage. + return Math.mulDiv($.reservedAssets, 1e18, convertToAssets(totalSupply())); + } + + /// @notice The amount of yield available for collection. + function yield() public view returns (uint256) { + // The formula for calculating yield is: + // yield = assets reported by yield vault + reserved assets - vbToken total supply in assets + (bool positive, uint256 difference) = backingDifference(); + + // Returns zero if the backing is negative. + return positive ? convertToShares(difference) : 0; + } + + /// @notice The difference between the total assets and the minimum assets required to back the total supply of vbToken in real-time. + function backingDifference() public view returns (bool positive, uint256 difference) { + // Get the state. + uint256 totalAssets_ = totalAssets(); + uint256 minimumAssets = convertToAssets(totalSupply()); + + // Calculate the difference. + return + totalAssets_ >= minimumAssets ? (true, totalAssets_ - minimumAssets) : (false, minimumAssets - totalAssets_); + } + + /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. + /// @notice This function can be called by a rebalancer only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function rebalanceReserve() external virtual delegatedToPart2 {} + + /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. + /// @param force Whether to revert if the reserve cannot be rebalanced. + /// @param allowRebalanceDown Whether to allow the reserve to be rebalanced down (by depositing into the yield vault). + function _rebalanceReserve(bool force, bool allowRebalanceDown) internal { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Cache the reserved assets, total supply, and uncollected yield. + uint256 originalReservedAssets = $.reservedAssets; + uint256 originalTotalSupply = totalSupply(); + uint256 originalUncollectedYield = yield(); + + // Calculate the minimum reserve amount. + uint256 minimumReserve = + convertToAssets(Math.mulDiv(originalTotalSupply, $.minimumReservePercentage, 1e18, Math.Rounding.Ceil)); + + // Check if the reserve is below, above, or at the minimum threshold. + /* Below. */ + if (originalReservedAssets < minimumReserve) { + // Calculate the amount to try to withdraw from the yield vault. + uint256 shortfall = minimumReserve - originalReservedAssets; + + // Try to withdraw from the yield vault. + (uint256 nonWithdrawnAssets, uint256 receivedAssets) = _withdrawFromYieldVault( + shortfall, false, address(this), originalTotalSupply, originalUncollectedYield, originalReservedAssets + ); + + // Revert if the the reserve could not be rebalanced and `force` is set to `true`. + if (force && nonWithdrawnAssets == shortfall) revert CannotRebalanceReserve(); + + // Update the reserve. + $.reservedAssets += receivedAssets; + + // Emit the event. + emit ReserveRebalanced(originalReservedAssets, $.reservedAssets, reservePercentage()); + } + /* Above */ + else if (originalReservedAssets > minimumReserve && allowRebalanceDown) { + // Calculate the amount to try to deposit into the yield vault. + uint256 excess = originalReservedAssets - minimumReserve; + + // Try to deposit into the yield vault. + uint256 nonDepositedAssets = _depositIntoYieldVault(excess, false); + + // Revert if the the reserve could not be rebalanced and `force` is set to `true`. + if (force && nonDepositedAssets == excess) revert CannotRebalanceReserve(); + + // Update the reserve. + $.reservedAssets -= (excess - nonDepositedAssets); + + // Emit the event. + emit ReserveRebalanced(originalReservedAssets, $.reservedAssets, reservePercentage()); + } + /* At. */ + else if (force) { + revert NoNeedToRebalanceReserve(); + } + } + + /// @notice Transfers yield produced by the yield vault to the yield recipient in the form of vbToken. + /// @notice Does not rebalance the reserve after collecting yield to allow usage while the contract is paused. + /// @notice This function can be called by a yield collector only. + /// @dev Increases the net collected yield. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function collectYield() external virtual delegatedToPart2 {} + + /// @notice Burns a specific amount of vbToken. + /// @notice This function can be used if the yield recipient has collected an unrealistic amount of yield over time. + /// @notice This function can be called by the yield recipient only. + /// @dev Decreases the net collected yield. + /// @dev Does not rebalance the reserve after burning to allow usage while the contract is paused. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function burn(uint256 shares) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + shares; + } + + /// @notice Adds a specific amount of the underlying token to the reserve by transferring it from the sender. + /// @notice This function can be used to restore backing difference by donating the underlying token. + /// @notice This function can be called by anyone. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function donateAsYield(uint256 assets) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + assets; + } + + /// @notice Adds a specific amount of the underlying token to a dedicated fund for covering any fees on Secondary Chain during a migration of backing to Primary Chain by transferring it from the sender. Please refer to `completeMigration` for more information. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function donateForCompletingMigration(uint256 assets) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + assets; + } + + /// @notice Completes a migration of backing from a Secondary Chain to Primary Chain by minting and locking the required amount of vbToken in Agglayer Bridge. + /// @notice Anyone can trigger the execution of this function by claiming the asset and message on Agglayer Bridge. Please refer to `NativeConverter.sol` for more information. + /// @dev Backing for Custom Token minted by Native Converter on Secondary Chains can be migrated to Primary Chain. + /// @dev When Native Converter migrates backing, it calls both `bridgeAsset` and `bridgeMessage` on Agglayer Bridge to `migrateBackingToPrimaryChain`. + /// @dev The asset must be claimed before the message on Agglayer Bridge. + /// @dev The message tells vbToken how much Custom Token must be backed by vbToken, which is minted and bridged to address zero on the respective Secondary Chain. This action provides liquidity when bridging Custom Token to from Secondary Chains to Primary Chain and increments the pessimistic proof. + /// @dev This function can be called by Migraton Manager only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param originNetwork The Agglayer ID of Secondary Chain the backing is being migrated from. + /// @param shares The amount of vbToken required to mint and lock up in Agglayer Bridge. Assets from a dedicated migration fees fund may be used to offset any fees incurred on Secondary Chain during the process. If a migration cannot be completed due to insufficient assets, anyone can donate the underlying token to the migration fees fund. Please refer to `donateForCompletingMigration` for more information. + /// @param assets The amount of the underlying token migrated from Secondary Chain (after any fees on Secondary Chain). + function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) + external + virtual + delegatedToPart2 + { + // Silence the Solidity compiler. + originNetwork; + shares; + assets; + } + + /// @notice Drains the yield vault by redeeming yield vault shares. Assets will be put into the internal reserve. + /// @notice This function may utilize availabe yield to ensure successful draining if there is larger slippage. Consider collecting yield before calling this function to disable this behavior. + /// @dev This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param shares The amount of the yield vault shares to redeem. + /// @param exact Whether to revert if the exact amount of shares could not be redeemed. + function drainYieldVault(uint256 shares, bool exact) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + shares; + exact; + } + + /// @notice Sets the minimum reserve percentage. + /// @notice @note (ATTENTION) Automatic reserve rebalancing will be disabled for values greater than `1e18` (100%). + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumReservePercentage_ `1e18` is 100%. + function setMinimumReservePercentage(uint256 minimumReservePercentage_) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + minimumReservePercentage_; + } + + /// @notice Sets the yield vault. + /// @notice @note CAUTION! Use `drainYieldVault` to drain the current yield vault completely before changing it. Any yield vault shares that are not redeemed will not count toward the underlying token backing after changing the yield vault. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function setYieldVault(address yieldVault_) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + yieldVault_; + } + + /// @notice Sets the yield recipient. + /// @notice Yield will be collected before changing the recipient. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function setYieldRecipient(address yieldRecipient_) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + yieldRecipient_; + } + + /// @notice The minimum amount of the underlying token that triggers a yield vault deposit. + /// @notice Amounts below this value will be reserved regardless of the reserve percentage, in order to save gas for users. + /// @notice The limit does not apply when rebalancing the reserve. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param minimumYieldVaultDeposit_ Set to `0` to disable. + function setMinimumYieldVaultDeposit(uint256 minimumYieldVaultDeposit_) external virtual delegatedToPart2 { + // Silence the Solidity compiler. + minimumYieldVaultDeposit_; + } + + /// @notice The maximum slippage percentage when depositing into or withdrawing from the yield vault. + /// @notice @note IMPORTANT: Any losses incurred due to slippage (and not fully covered by produced yield) will need to be covered by whomever is responsible for this contract. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + /// @param maximumSlippagePercentage 1e18 is 100%. The recommended value is `0.01e18` (1%). + function setYieldVaultMaximumSlippagePercentage(uint256 maximumSlippagePercentage) + external + virtual + delegatedToPart2 + { + // Silence the Solidity compiler. + maximumSlippagePercentage; + } + + /// @notice Calculates the amount of assets to reserve (as opposed to depositing into the yield vault) based on the current and minimum reserve percentages. + /// @dev @note (ATTENTION) `reservedAssets` must be up-to-date before using this function. + /// @param assets The amount of the underlying token being deposited. + /// @param nonMintedShares The amount of vbToken that will be minted after using this function as a result of the deposit. (Set to `0` if you have already minted all the shares). + function _calculateAmountToReserve(uint256 assets, uint256 nonMintedShares) + internal + view + returns (uint256 assetsToReserve) + { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Calculate the minimum reserve. + uint256 minimumReserve = convertToAssets( + Math.mulDiv(totalSupply() + nonMintedShares, $.minimumReservePercentage, 1e18, Math.Rounding.Ceil) + ); + + // Calculate the amount to reserve. + assetsToReserve = $.reservedAssets < minimumReserve ? minimumReserve - $.reservedAssets : 0; + return assetsToReserve <= assets ? assetsToReserve : assets; + } + + /// @notice Deposit a specific amount of the underlying token into the yield vault. + /// @param assets The amount of the underlying token to deposit into the yield vault. + /// @param exact Whether to revert if the exact amount of the underlying token could not be deposited into the yield vault. + /// @return nonDepositedAssets The amount of the underlying token that could not be deposited into the yield vault. The value will be `0` if `exact` is set to `true`. + function _depositIntoYieldVault(uint256 assets, bool exact) internal returns (uint256 nonDepositedAssets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Check whether to skip depositing into the yield vault. + if (assets < $.minimumYieldVaultDeposit) { + if (exact) revert MinimumYieldVaultDepositNotMet(assets, $.minimumYieldVaultDeposit); + return assets; + } + + // Cache the original assets. + uint256 originalAssets = assets; + + // Get the yield vault's deposit limit. + uint256 maxDeposit_ = $.yieldVault.maxDeposit(address(this)); + + // Revert if the assets are greater than the deposit limit and `exact` is set to `true`. + if (exact) require(assets <= maxDeposit_, YieldVaultDepositFailed(assets, maxDeposit_)); + + // Set the return value. + nonDepositedAssets = assets > maxDeposit_ ? assets - maxDeposit_ : 0; + + // Calculate the amount to deposit into the yield vault. + assets = assets > maxDeposit_ ? maxDeposit_ : assets; + + // Return if no assets would be deposited. + if (assets == 0) return nonDepositedAssets; + + // Try to deposit into the yield vault. + try this.performReversibleYieldVaultDeposit(assets) {} + // If the deposit failed, decode the revert data. + catch (bytes memory data) { + (bool depositSucceeded, bytes memory depositData, bool solvencyCheckPassed) = + abi.decode(data, (bool, bytes, bool)); + + // The yield vault deposit failed. + if (!depositSucceeded) { + // Revert if the assets must have been put into the yield vault. + if (exact) { + // Bubble up the revert data. + assembly ("memory-safe") { + revert(add(32, depositData), mload(depositData)) + } + } else { + // Return the amount of non-deposited assets. + return originalAssets; + } + } + + // The yield vault deposit succeeded, but the solvency check did not pass. + if (!solvencyCheckPassed) { + // Revert if the assets must have been put into the yield vault. + if (exact) { + // Revert with the standard solvency check error. + uint256 mintedYieldVaultShares = abi.decode(depositData, (uint256)); + revert InsufficientYieldVaultSharesMinted(assets, mintedYieldVaultShares); + } else { + // Return the amount of non-deposited assets. + return originalAssets; + } + } + + // The yield vault deposit succeeded and the solvency check passed but the call still reverted for some reason. (Sanity check - should not happen). + revert UnknownError(data); + } + } + + /// @notice Enables infinte deposits regardless of the behavior of the yield vault. + /// @dev This function reverts if the yield vault deposit fails, or the solvency check does not pass with revert data ABI-encoded in the following format: `abi.encode(depositSucceeded, depositData, solvencyCheckPassed)`, which can be decoded in another function. + /// @notice This function can be called by the this contract only. + function performReversibleYieldVaultDeposit(uint256 assets) external whenNotPaused onlySelf { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Prepare the variables. + bool depositSucceeded; + bytes memory depositData; + bool solvencyCheckPassed; + + // Cache the staked assets before the deposit. + uint256 oldStakedAssets = stakedAssets(); + + // Try to deposit into the yield vault. + (depositSucceeded, depositData) = + address($.yieldVault).call(abi.encodeCall(IERC4626.deposit, (assets, address(this)))); + + // The deposit succeeded. + if (depositSucceeded) { + // Check the output. + // This code checks if the minted yield vault shares are worth enough in the underlying token. Allows slippage. + solvencyCheckPassed = stakedAssets() - oldStakedAssets + >= Math.mulDiv(assets, 1e18 - $.yieldVaultMaximumSlippagePercentage, 1e18); + } + + // The deposit failed or the solvency check did not pass. + if (!depositSucceeded || !solvencyCheckPassed) { + // Encode the information in the revert data. + bytes memory data = abi.encode(depositSucceeded, depositData, solvencyCheckPassed); + // Revert with the encoded data. + assembly ("memory-safe") { + revert(add(32, data), mload(data)) + } + } + } + + /// @notice Withdraws a specific amount of the underlying token from the yield vault. + /// @param assets The amount of the underlying token to withdraw from the yield vault. + /// @param exact Whether to revert if the exact amount of the underlying token could not be withdrawn from the yield vault. + /// @param receiver The address to withdraw the underlying token to. + /// @param originalTotalSupply The total supply of vbToken before burning the required amount of vbToken or updating the reserve. Used for the solvency check. + /// @param originalUncollectedYield The uncollected yield before burning the required amount of vbToken or updating the reserve. Used for the solvency check. + /// @return nonWithdrawnAssets The amount of the underlying token that could not be withdrawn from the yield vault. The value will be `0` if `exact` is set to `true`. + /// @return receivedAssets The amount of the underlying token actually received (e.g., after any fees). The value will be `0` if `receiver` is not `address(this)`. + function _withdrawFromYieldVault( + uint256 assets, + bool exact, + address receiver, + uint256 originalTotalSupply, + uint256 originalUncollectedYield, + uint256 originalReservedAssets + ) internal returns (uint256 nonWithdrawnAssets, uint256 receivedAssets) { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Get the yield vault's withdraw limit. + uint256 maxWithdraw_ = $.yieldVault.maxWithdraw(address(this)); + + // Revert if the assets are greater than the withdraw limit and `exact` is set to `true`. + if (exact) require(assets <= maxWithdraw_, YieldVaultWithdrawalFailed(assets, maxWithdraw_)); + + // Set a return value. + nonWithdrawnAssets = assets > maxWithdraw_ ? assets - maxWithdraw_ : 0; + + // Calculate the amount to withdraw from the yield vault. + assets = assets > maxWithdraw_ ? maxWithdraw_ : assets; + + // Return if no assets would be withdrawn. + if (assets == 0) return (nonWithdrawnAssets, 0); + + // Cache the underlying token balance and yield vault shares balance. + // The underying token balance is cached only when the receiver is vbToken. + uint256 underlyingTokenBalanceBefore; + if (receiver == address(this)) underlyingTokenBalanceBefore = $.underlyingToken.balanceOf(address(this)); + uint256 yieldVaultSharesBalanceBefore = $.yieldVault.balanceOf(address(this)); + + // Withdraw. + uint256 burnedYieldVaultShares = $.yieldVault.withdraw(assets, receiver, address(this)); + + // Check the output. + // This code checks if the contract would go insolvent if the amount of the underlying token required to back the portion of the total supply (including the uncollected yield) not backed by the reserved assets were withdrawn at this exchange rate. Allows slippage. + require( + Math.mulDiv( + convertToAssets(originalTotalSupply + originalUncollectedYield) - originalReservedAssets, + burnedYieldVaultShares, + assets + ) <= Math.mulDiv(yieldVaultSharesBalanceBefore, 1e18 + $.yieldVaultMaximumSlippagePercentage, 1e18), + ExcessiveYieldVaultSharesBurned(burnedYieldVaultShares, assets) + ); + + // Calculate the withdrawn amount. + // The withdrawn amount is only calculated when the receiver is vbToken. + receivedAssets = + receiver == address(this) ? ($.underlyingToken.balanceOf(address(this)) - underlyingTokenBalanceBefore) : 0; + } + + // -----================= ::: UNDERLYING TOKEN ::: =================----- + + /// @notice Transfers the underlying token from an external account to self. + /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. + function _receiveUnderlyingToken(address from, uint256 value) internal { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Cache the balance. + uint256 balanceBefore = $.underlyingToken.balanceOf(address(this)); + + // Transfer. + // @note IMPORTANT: Make sure the underlying token you are integrating does not enable reentrancy on `transferFrom`. + $.underlyingToken.safeTransferFrom(from, address(this), value); + + // Calculate the received amount. + uint256 receivedValue = $.underlyingToken.balanceOf(address(this)) - balanceBefore; + + // Check the output. + require(receivedValue == value, InsufficientUnderlyingTokenReceived(receivedValue, value)); + } + + /// @notice Transfers the underlying token to an external account. + /// @dev @note CAUTION! This function MUST NOT introduce reentrancy/crossentrancy vulnerabilities. + function _sendUnderlyingToken(address to, uint256 value) internal { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + // Transfer. + // @note IMPORTANT: Make sure the underlying token you are integrating does not enable reentrancy on `transfer`. + $.underlyingToken.safeTransfer(to, value); + } + + // -----================= ::: ADMIN ::: =================----- + + /// @notice Prevents usage of functions with the `whenNotPaused` modifier. + /// @notice This function can be called by a pauser only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function pause() external virtual delegatedToPart2 {} + + /// @notice Allows usage of functions with the `whenNotPaused` modifier. + /// @notice This function can be called by the owner only. + /// @dev Delegates the call to `VaultBridgeTokenPart2`. + /// @dev @note (ATTENTION) The `virtual` modifier allows `VaultBridgeTokenPart2` to override this function. Do not override the function yourself. + function unpause() external virtual delegatedToPart2 {} + + // -----================= ::: PART 2 ::: =================----- + + /// @notice Delegates the call to `VaultBridgeTokenPart2`. + function _delegateToPart2() private { + VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + + address vaultBridgeTokenPart2 = $._vaultBridgeTokenPart2; + + assembly { + calldatacopy(0, 0, calldatasize()) + let ok := delegatecall(gas(), vaultBridgeTokenPart2, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch ok + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } +} diff --git a/src/VaultBridgeTokenInitializer.sol b/src/primary-chain/VaultBridgeTokenInitializer.sol similarity index 59% rename from src/VaultBridgeTokenInitializer.sol rename to src/primary-chain/VaultBridgeTokenInitializer.sol index 3f211d58..c770c76f 100644 --- a/src/VaultBridgeTokenInitializer.sol +++ b/src/primary-chain/VaultBridgeTokenInitializer.sol @@ -1,45 +1,58 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/VaultBridgeTokenInitializer.sol) + pragma solidity 0.8.29; // Main functionality. -import {IVaultBridgeTokenInitializer} from "./etc/IVaultBridgeTokenInitializer.sol"; +import {IVaultBridgeTokenInitializer} from "../etc/IVaultBridgeTokenInitializer.sol"; import {VaultBridgeToken} from "./VaultBridgeToken.sol"; -// Other functionality. -import {IVersioned} from "./etc/IVersioned.sol"; - // Libraries. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; // External contracts. import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {ILxLyBridge} from "./etc/ILxLyBridge.sol"; +import {IAgglayerBridge} from "../etc/IAgglayerBridge.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// @remind Document. -// @title Vault Bridge Token: Initializer (singleton) +/// @title Vault Bridge Token Initializer (singleton) /// @author See https://github.com/agglayer/vault-bridge +/// @notice A singleton contract used by Vault Bridge Token for initialization. +/// @dev This contract exists because of the contract size limit of the EVM. contract VaultBridgeTokenInitializer is IVaultBridgeTokenInitializer, VaultBridgeToken { // Libraries. using SafeERC20 for IERC20; + /// @dev The storage slot at which Vault Bridge Token storage starts, following the EIP-7201 standard. + /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.VaultBridgeToken.storage")) - 1)) & ~bytes32(uint256(0xff))`. + bytes32 private constant _VAULT_BRIDGE_TOKEN_STORAGE = + hex"f082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44700"; + // -----================= ::: SETUP ::: =================----- constructor() { _disableInitializers(); } + // -----================= ::: STORAGE ::: =================----- + + /// @dev Returns a pointer to the ERC-7201 storage namespace. + function __getVaultBridgeTokenStorage() private pure returns (VaultBridgeTokenStorage storage $) { + assembly { + $.slot := _VAULT_BRIDGE_TOKEN_STORAGE + } + } + // -----================= ::: VAULT BRIDGE TOKEN ::: =================----- - // @remind Document. + /// @inheritdoc IVaultBridgeTokenInitializer function initialize(VaultBridgeToken.InitializationParameters calldata initParams) external override onlyInitializing - nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the inputs. require(initParams.owner != address(0), InvalidOwner()); @@ -49,10 +62,15 @@ contract VaultBridgeTokenInitializer is IVaultBridgeTokenInitializer, VaultBridg require(initParams.minimumReservePercentage <= 1e18, InvalidMinimumReservePercentage()); require(initParams.yieldVault != address(0), InvalidYieldVault()); require(initParams.yieldRecipient != address(0), InvalidYieldRecipient()); - require(initParams.lxlyBridge != address(0), InvalidLxLyBridge()); + require(initParams.agglayerBridge != address(0), InvalidAgglayerBridge()); require(initParams.migrationManager != address(0), InvalidMigrationManager()); require(initParams.yieldVaultMaximumSlippagePercentage <= 1e18, InvalidYieldVaultMaximumSlippagePercentage()); require(initParams.vaultBridgeTokenPart2 != address(0), InvalidVaultBridgeTokenPart2()); + require( + keccak256(bytes(VaultBridgeToken(initParams.vaultBridgeTokenPart2).VAULT_BRIDGE_PROTOCOL())) + == keccak256(bytes(VAULT_BRIDGE_PROTOCOL())), + InvalidVaultBridgeTokenPart2() + ); // Initialize the inherited contracts. __ERC20_init(initParams.name, initParams.symbol); @@ -75,28 +93,29 @@ contract VaultBridgeTokenInitializer is IVaultBridgeTokenInitializer, VaultBridg try IERC20Metadata(initParams.underlyingToken).decimals() returns (uint8 decimals_) { $.decimals = decimals_; } catch { - // Default to 18 decimals. + // Default to 18 decimals if the underlying token reverted. $.decimals = 18; } $.minimumReservePercentage = initParams.minimumReservePercentage; $.yieldVault = IERC4626(initParams.yieldVault); $.yieldRecipient = initParams.yieldRecipient; - $.lxlyId = ILxLyBridge(initParams.lxlyBridge).networkID(); - $.lxlyBridge = ILxLyBridge(initParams.lxlyBridge); + $.agglayerId = IAgglayerBridge(initParams.agglayerBridge).networkID(); + $.agglayerBridge = IAgglayerBridge(initParams.agglayerBridge); $.minimumYieldVaultDeposit = initParams.minimumYieldVaultDeposit; $.migrationManager = initParams.migrationManager; $.yieldVaultMaximumSlippagePercentage = initParams.yieldVaultMaximumSlippagePercentage; $._vaultBridgeTokenPart2 = initParams.vaultBridgeTokenPart2; - // Approve the yield vault and LxLy Bridge. + // Approve the yield vault and Agglayer Bridge. IERC20(initParams.underlyingToken).forceApprove(initParams.yieldVault, type(uint256).max); - _approve(address(this), address(initParams.lxlyBridge), type(uint256).max); + _approve(address(this), address(initParams.agglayerBridge), type(uint256).max); } - // -----================= ::: INFO ::: =================----- + /* + /// @dev How to add a new init step: + function reinitialize3() external onlyInitializing {} + */ - /// @inheritdoc IVersioned - function version() external pure returns (string memory) { - return "0.5.0"; - } + /// @inheritdoc VaultBridgeToken + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure override {} } diff --git a/src/VaultBridgeTokenPart2.sol b/src/primary-chain/VaultBridgeTokenPart2.sol similarity index 56% rename from src/VaultBridgeTokenPart2.sol rename to src/primary-chain/VaultBridgeTokenPart2.sol index 5f26f28f..9d709140 100644 --- a/src/VaultBridgeTokenPart2.sol +++ b/src/primary-chain/VaultBridgeTokenPart2.sol @@ -1,13 +1,12 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/VaultBridgeTokenPart2.sol) + pragma solidity 0.8.29; // Main functionality. import {VaultBridgeToken} from "./VaultBridgeToken.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -// Other functionality. -import {IVersioned} from "./etc/IVersioned.sol"; - // Libraries. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; @@ -15,45 +14,59 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; // External contracts. import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -// @remind Document. /// @title Vault Bridge Token: Part 2 (singleton) /// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract exists because of the contract size limit of the EVM. contract VaultBridgeTokenPart2 is VaultBridgeToken { // Libraries. using SafeERC20 for IERC20; + /// @dev The storage slot at which Vault Bridge Token storage starts, following the EIP-7201 standard. + /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.VaultBridgeToken.storage")) - 1)) & ~bytes32(uint256(0xff))`. + bytes32 private constant _VAULT_BRIDGE_TOKEN_STORAGE = + hex"f082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44700"; + + // -----================= ::: SOLIDITY ::: =================----- + + fallback() external { + revert UnknownFunction(bytes4(msg.data)); + } + // -----================= ::: SETUP ::: =================----- constructor() { _disableInitializers(); } - // -----================= ::: SOLIDITY ::: =================----- + /// @inheritdoc VaultBridgeToken + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure override {} - fallback() external payable override { - revert UnknownFunction(bytes4(msg.data)); + // -----================= ::: STORAGE ::: =================----- + + /// @dev Returns a pointer to the ERC-7201 storage namespace. + function __getVaultBridgeTokenStorage() private pure returns (VaultBridgeTokenStorage storage $) { + assembly { + $.slot := _VAULT_BRIDGE_TOKEN_STORAGE + } } // -----================= ::: VAULT BRIDGE TOKEN ::: =================----- - /// @notice Rebalances the internal reserve by withdrawing the underlying token from, or depositing the underlying token into, the yield vault. - /// @notice This function can be called by the rebalancer only. - function rebalanceReserve() external whenNotPaused onlyRole(REBALANCER_ROLE) nonReentrant { + /// @inheritdoc VaultBridgeToken + function rebalanceReserve() external override whenNotPaused onlyRole(REBALANCER_ROLE) nonReentrant { _rebalanceReserve(true, true); } - /// @notice Transfers yield generated by the yield vault to the yield recipient in the form of vbToken. - /// @notice Does not rebalance the reserve after collecting yield to allow usage while the contract is paused. - /// @notice This function can be called by the yield collector only. - function collectYield() external onlyRole(YIELD_COLLECTOR_ROLE) nonReentrant { + /// @inheritdoc VaultBridgeToken + function collectYield() external override onlyRole(YIELD_COLLECTOR_ROLE) nonReentrant { _collectYield(true); } - /// @notice Transfers yield generated by the yield vault to the yield recipient in the form of vbToken. + /// @notice Transfers yield produced by the yield vault to the yield recipient in the form of vbToken. /// @dev Does not rebalance the reserve after collecting yield to allow usage while the contract is paused. /// @param force Whether to revert if no yield can be collected. function _collectYield(bool force) internal { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Calculate the yield. uint256 yield_ = yield(); @@ -68,16 +81,14 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { // Emit the event. emit YieldCollected(yieldRecipient(), yield_); } else if (force) { + // Revert if there is no yield and `force` is `true`. revert NoYield(); } } - /// @notice Burns a specific amount of vbToken. - /// @notice This function can be used if the yield recipient has collected an unrealistic (excessive) amount of yield historically. - /// @notice This function can be called by the yield recipient only. - /// @dev Does not rebalance the reserve after burning to allow usage while the contract is paused. - function burn(uint256 shares) external onlyYieldRecipient nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + /// @inheritdoc VaultBridgeToken + function burn(uint256 shares) external override onlyYieldRecipient nonReentrant { + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the inputs. require(shares > 0, InvalidShares()); @@ -92,11 +103,9 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit Burned(shares); } - /// @notice Adds a specific amount of the underlying token to the reserve by transferring it from the sender. - /// @notice This function can be used to restore backing difference by donating the underlying token. - /// @notice This function can be called by anyone. - function donateAsYield(uint256 assets) external nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + /// @inheritdoc VaultBridgeToken + function donateAsYield(uint256 assets) external override nonReentrant { + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the input. require(assets > 0, InvalidAssets()); @@ -111,34 +120,26 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit DonatedAsYield(msg.sender, assets); } - // @remind Redocument (the entire function). - /// @notice Completes a migration of backing from a Layer Y to Layer X by minting and locking the required amount of vbToken in LxLy Bridge. - /// @notice Anyone can trigger the execution of this function by claiming the asset and message on LxLy Bridge. Please refer to `NativeConverter.sol` for more information. - /// @dev Backing for Custom Token minted by Native Converter on Layer Ys can be migrated to Layer X. - /// @dev When Native Converter migrates backing, it calls both `bridgeAsset` and `bridgeMessage` on LxLy Bridge to `migrateBackingToLayerX`. - /// @dev The asset must be claimed before the message on LxLy Bridge. - /// @dev The message tells vbToken how much Custom Token must be backed by vbToken, which is minted and bridged to address zero on the respective Layer Y. This action provides liquidity when bridging Custom Token to from Layer Ys to Layer X and increments the pessimistic proof. - /// @param originNetwork The LxLy ID of Layer Y the backing is being migrated from. - /// @param shares The required amount of vbToken to mint and lock up in LxLy Bridge. Assets from a dedicated migration fees fund may be used to offset transfer fees of the underlying token. If a migration cannot be completed due to insufficient assets, anyone can donate the underlying token to the migration fees fund. Please refer to `donateForCompletingMigration` for more information. - /// @param assets The amount of the underlying token migrated from Layer Y (before transfer fees on Layer X). + /// @inheritdoc VaultBridgeToken function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) external + override whenNotPaused onlyMigrationManager nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the inputs. - require(originNetwork != $.lxlyId, InvalidOriginNetwork()); + require(originNetwork != $.agglayerId, InvalidOriginNetwork()); require(shares > 0, InvalidShares()); // Transfer the underlying token from the sender to self. _receiveUnderlyingToken(msg.sender, assets); - // Calculate the discrepancy between the required amount of vbToken (`shares`) and the amount of the underlying token received from LxLy Bridge (`assets`). - // A discrepancy is possible due to transfer fees of the underlying token. To offset the discrepancy, we mint more vbToken, backed by assets from the dedicated migration fees fund. - // This ensures that the amount of vbToken locked up in LxLy Bridge on Layer X matches the supply of Custom Token on Layer Ys exactly. + // Calculate the discrepancy between the required amount of vbToken (`shares`) and the amount of the underlying token received from Migration Manager (`assets`). + // A discrepancy is possible if the underlying token implements transfer fees on Secondary Chain. To offset the discrepancy, we mint more vbToken, backed by assets from the dedicated migration fees fund. + // This ensures that the amount of vbToken locked up in Agglayer Bridge on Primary Chain matches the supply of Custom Token on Secondary Chains down to a wei. uint256 requiredAssets = convertToAssets(shares); uint256 discrepancy = requiredAssets - assets; uint256 assetsInMigrationFund = $.migrationFeesFund; @@ -170,9 +171,9 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { $.reservedAssets += assetsToReserve; // Mint vbToken to self and bridge it to address zero on the origin network. - // The vbToken will not be claimable on the origin network, but provides liquidity when bridging from Layer Ys to Layer X and increments the pessimistic proof. + // The vbToken will not be claimable on the origin network, but provides liquidity when bridging from Secondary Chains to Primary Chain and increments the pessimistic proof. _mint(address(this), shares); - $.lxlyBridge.bridgeAsset(originNetwork, address(0), shares, address(this), true, ""); + $.agglayerBridge.bridgeAsset(originNetwork, address(0), shares, address(this), true, ""); // Emit the ERC-4626 event. emit IERC4626.Deposit(msg.sender, address(this), assets, shares); @@ -181,9 +182,9 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit MigrationCompleted(originNetwork, shares, assets, discrepancy); } - /// @notice Adds a specific amount of the underlying token to a dedicated fund for covering the underlying token's transfer fees during a migration by transferring it from the sender. Please refer to `_completeMigration` for more information. - function donateForCompletingMigration(uint256 assets) external whenNotPaused nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + /// @inheritdoc VaultBridgeToken + function donateForCompletingMigration(uint256 assets) external override whenNotPaused nonReentrant { + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the input. require(assets > 0, InvalidAssets()); @@ -198,16 +199,15 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit DonatedForCompletingMigration(msg.sender, assets); } - /// @notice Sets the yield recipient. - /// @notice Yield will be collected before changing the recipient. - /// @notice This function can be called by the owner only. + /// @inheritdoc VaultBridgeToken function setYieldRecipient(address yieldRecipient_) external + override whenNotPaused onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the input. require(yieldRecipient_ != address(0), InvalidYieldRecipient()); @@ -222,14 +222,14 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit YieldRecipientSet(yieldRecipient_); } - /// @notice Sets the minimum reserve percentage. - /// @notice This function can be called by the owner only. + /// @inheritdoc VaultBridgeToken function setMinimumReservePercentage(uint256 minimumReservePercentage_) external + override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the input. require(minimumReservePercentage_ <= 1e18, InvalidMinimumReservePercentage()); @@ -241,43 +241,55 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { emit MinimumReservePercentageSet(minimumReservePercentage_); } - // @remind Document (the entire function). - /// @notice Consider collecting yield before calling this function. - function drainYieldVault(uint256 shares, bool exact) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + /// @inheritdoc VaultBridgeToken + function drainYieldVault(uint256 shares, bool exact) external override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); + // Check the input. require(shares > 0, InvalidShares()); + // Cache the original total supply, reserved assets, and the yield vault shares balance. uint256 originalTotalSupply = totalSupply(); uint256 originalReservedAssets = $.reservedAssets; uint256 originalYieldVaultSharesBalance = $.yieldVault.balanceOf(address(this)); + // Modify the input if set to infinite. if (shares == type(uint256).max) { shares = originalYieldVaultSharesBalance; } + // Check the maximum shares that can be redeemed. uint256 maxShares = $.yieldVault.maxRedeem(address(this)); + // Revert if the requested shares are more than the maximum shares that can be redeemed, and `exact` is set to `true`. if (exact) { require(shares <= maxShares, YieldVaultRedemptionFailed(shares, maxShares)); } + // Modify the input if it is more than the maximum shares that can be redeemed. shares = shares > maxShares ? maxShares : shares; + // Return if no shares would be redeemed. if (shares == 0) return; + // Cache the underlying token balance. uint256 balanceBefore = $.underlyingToken.balanceOf(address(this)); + // Redeem. $.yieldVault.redeem(shares, address(this), address(this)); + // Get the new underlying token balance. uint256 balanceAfter = $.underlyingToken.balanceOf(address(this)); + // Calculate the amount of assets received from the yield vault. uint256 receivedAssets = balanceAfter - balanceBefore; + // Update the reserve. $.reservedAssets += receivedAssets; - // Redeeming all shares at this exchange rate would need to give enough assets to back the total supply of vbToken together with the reserved assets. - // Does not check uncollected yield to relax the condition a bit. Instead, yield can be collected manually before calling this function, if the yield collector wishes to do so. + // Check the output. + // Redeeming all shares at this exchange rate would need to give enough assets to back the total supply of vbToken together with the reserved assets. Allows slippage. + // Does not check uncollected yield to relax the condition. Yield can be collected manually before calling this function. require( Math.mulDiv(originalYieldVaultSharesBalance, receivedAssets, shares) >= Math.mulDiv( @@ -288,45 +300,49 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { ExcessiveYieldVaultSharesBurned(shares, receivedAssets) ); + // Emit the event. emit YieldVaultDrained(shares, receivedAssets); } - // @remind Redocument (the entire function). - /// @notice Sets a new yieldVault. Be careful to only call this once the current vault has been emptied. - /// @notice This function can be called by the owner only. - function setYieldVault(address yieldVault_) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + /// @inheritdoc VaultBridgeToken + function setYieldVault(address yieldVault_) external override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); + // Check the input. require(yieldVault_ != address(0), InvalidYieldVault()); + // Revoke the approval for the old yield vault. $.underlyingToken.forceApprove(address($.yieldVault), 0); + // Set the yield vault. $.yieldVault = IERC4626(yieldVault_); + // Approve the new yield vault. $.underlyingToken.forceApprove(yieldVault_, type(uint256).max); // Emit the event. emit YieldVaultSet(yieldVault_); } - /// @notice Sets the minimum deposit amount that triggers a yield vault deposit. - /// @notice This function can be called by the owner only. + /// @inheritdoc VaultBridgeToken function setMinimumYieldVaultDeposit(uint256 minimumYieldVaultDeposit_) external + override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); $.minimumYieldVaultDeposit = minimumYieldVaultDeposit_; } - // @remind Document. + /// @inheritdoc VaultBridgeToken function setYieldVaultMaximumSlippagePercentage(uint256 maximumSlippagePercentage) external + override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - VaultBridgeTokenStorage storage $ = _getVaultBridgeTokenStorage(); + VaultBridgeTokenStorage storage $ = __getVaultBridgeTokenStorage(); // Check the input. require(maximumSlippagePercentage <= 1e18, InvalidYieldVaultMaximumSlippagePercentage()); @@ -340,22 +356,13 @@ contract VaultBridgeTokenPart2 is VaultBridgeToken { // -----================= ::: ADMIN ::: =================----- - /// @notice Prevents usage of functions with the `whenNotPaused` modifier. - /// @notice This function can be called by the pauser only. - function pause() external onlyRole(PAUSER_ROLE) nonReentrant { + /// @inheritdoc VaultBridgeToken + function pause() external override onlyRole(PAUSER_ROLE) nonReentrant { _pause(); } - /// @notice Allows usage of functions with the `whenNotPaused` modifier. - /// @notice This function can be called by the owner only. - function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + /// @inheritdoc VaultBridgeToken + function unpause() external override onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { _unpause(); } - - // -----================= ::: INFO ::: =================----- - - /// @inheritdoc IVersioned - function version() external pure override returns (string memory) { - return "0.5.0"; - } } diff --git a/src/primary-chain/ethereum/GenericVaultBridgeToken.sol b/src/primary-chain/ethereum/GenericVaultBridgeToken.sol new file mode 100644 index 00000000..166adbc0 --- /dev/null +++ b/src/primary-chain/ethereum/GenericVaultBridgeToken.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/ethereum/GenericVaultBridgeToken.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {VaultBridgeToken} from "../VaultBridgeToken.sol"; + +/// @title Generic Vault Bridge Token +/// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract can be used to deploy vbTokens that do not require any customization. +contract GenericVaultBridgeToken is VaultBridgeToken { + // -----================= ::: SETUP ::: =================----- + + constructor() { + _disableInitializers(); + } + + // @remind Document. + function reinitialize1(address initializer_, VaultBridgeToken.InitializationParameters calldata initParams) + external + reinitializer(1) + nonReentrant + { + // Initialize the base implementation. + __VaultBridgeToken_init1(initializer_, initParams); + } + + // @remind Document (the entire function). + function reinitialize2() external reinitializer(2) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __VaultBridgeToken_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize3() + external + reinitializer(_incrementGlobalInitializationCounter(3)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](2); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc VaultBridgeToken + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure override {} +} diff --git a/src/vault-bridge-tokens/vbETH/VbETH.sol b/src/primary-chain/ethereum/vbETH/VbETH.sol similarity index 57% rename from src/vault-bridge-tokens/vbETH/VbETH.sol rename to src/primary-chain/ethereum/vbETH/VbETH.sol index 221c2b42..54c2a9fa 100644 --- a/src/vault-bridge-tokens/vbETH/VbETH.sol +++ b/src/primary-chain/ethereum/vbETH/VbETH.sol @@ -1,43 +1,74 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (primary-chain/ethereum/vbETH/VbEth.sol) + pragma solidity 0.8.29; -import {VaultBridgeToken, ILxLyBridge} from "../../VaultBridgeToken.sol"; -import {IWETH9} from "../../etc/IWETH9.sol"; +import {VaultBridgeToken, IAgglayerBridge} from "../../VaultBridgeToken.sol"; +import {IWETH9} from "../../../etc/IWETH9.sol"; import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {IVersioned} from "../../etc/IVersioned.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; /// @title Vault Bridge gas token /// @author See https://github.com/agglayer/vault-bridge -/// @dev CAUTION! As-is, this contract MUST NOT be used on a network if the gas token is not ETH. -contract VbETH is VaultBridgeToken { +/// @dev CAUTION! As-is, this contract MUST NOT be used on a chain if the gas token is not ETH. +contract VbEth is VaultBridgeToken { using SafeERC20 for IWETH9; - error ContractNotSupportedOnThisNetwork(); + error ContractNotSupportedOnThisChain(); error IncorrectMsgValue(uint256 msgValue, uint256 requestedAssets); constructor() { _disableInitializers(); } - function initialize(address initializer_, VaultBridgeToken.InitializationParameters calldata initParams) + function reinitialize1(address initializer_, VaultBridgeToken.InitializationParameters calldata initParams) external - initializer + reinitializer(1) + nonReentrant { // Initialize the base implementation. - __VaultBridgeToken_init(initializer_, initParams); + __VaultBridgeToken_init1(initializer_, initParams); require( - ILxLyBridge(initParams.lxlyBridge).gasTokenAddress() == address(0) - && ILxLyBridge(initParams.lxlyBridge).gasTokenNetwork() == 0, - ContractNotSupportedOnThisNetwork() + IAgglayerBridge(initParams.agglayerBridge).gasTokenAddress() == address(0) + && IAgglayerBridge(initParams.agglayerBridge).gasTokenNetwork() == 0, + ContractNotSupportedOnThisChain() ); } + function reinitialize2() external reinitializer(2) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __VaultBridgeToken_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize3() + external + reinitializer(_incrementGlobalInitializationCounter(3)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](2); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc VaultBridgeToken + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure override {} + /// @dev deposit ETH to get vbETH function depositGasToken(address receiver) external payable whenNotPaused nonReentrant returns (uint256 shares) { (shares,) = _depositUsingCustomReceivingFunction( - _receiveUnderlyingTokenViaMsgValue, msg.value, lxlyId(), receiver, false, 0 + _receiveUnderlyingTokenViaMsgValue, msg.value, agglayerId(), receiver, false, 0 ); } @@ -72,7 +103,7 @@ contract VbETH is VaultBridgeToken { (mintedShares, assets) = // msg.value is used as assets value, if it exceeds shares value, WETH will be refunded _depositUsingCustomReceivingFunction( - _receiveUnderlyingTokenViaMsgValue, msg.value, lxlyId(), receiver, false, shares + _receiveUnderlyingTokenViaMsgValue, msg.value, agglayerId(), receiver, false, shares ); // Check the output. @@ -97,9 +128,4 @@ contract VbETH is VaultBridgeToken { require(receivedAssets == assets, InsufficientUnderlyingTokenReceived(receivedAssets, assets)); } - - /// @inheritdoc IVersioned - function version() external pure override returns (string memory) { - return "0.5.0"; - } } diff --git a/src/primary-chain/ethereum/vbUSDC/VbUSDC.sol.generic b/src/primary-chain/ethereum/vbUSDC/VbUSDC.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/primary-chain/ethereum/vbUSDS/VbUSDS.sol.generic b/src/primary-chain/ethereum/vbUSDS/VbUSDS.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/primary-chain/ethereum/vbUSDT/VbUSDT.sol.generic b/src/primary-chain/ethereum/vbUSDT/VbUSDT.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/primary-chain/ethereum/vbWBTC/VbWBTC.sol.generic b/src/primary-chain/ethereum/vbWBTC/VbWBTC.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/CustomToken.sol b/src/secondary-chain/CustomToken.sol similarity index 62% rename from src/CustomToken.sol rename to src/secondary-chain/CustomToken.sol index f3c5f84a..417a4d53 100644 --- a/src/CustomToken.sol +++ b/src/secondary-chain/CustomToken.sol @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/CustomToken.sol) + pragma solidity 0.8.29; // Main functionality. @@ -10,27 +12,29 @@ import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Ini import {AccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {IVersioned} from "./etc/IVersioned.sol"; +import {InitializationCounterUpgradeable} from "../etc/InitializationCounterUpgradeable.sol"; +import {Versioned} from "../etc/Versioned.sol"; /// @title Custom Token /// @author See https://github.com/agglayer/vault-bridge -/// @notice A Custom Token is an ERC-20 token deployed on Layer Ys to represent the native version of the original underlying token from Layer X on Layer Y. +/// @notice A Custom Token is an optional ERC-20 token on Secondary Chains to represent the 'native' version of the original underlying token from Primary Chain on Secondary Chain, ideally (or, simply, the upgraded version of the bridged vbToken). /// @dev A base contract used to create Custom Tokens. -/// @dev @note IMPORTANT: Custom Token MUST be custom mapped to the corresponding vbToken on LxLy Bridge on Layer Y and MUST give the minting and burning permission to LxLy Bridge and Native Converter. It MAY have a transfer fee. +/// @dev @note IMPORTANT: Custom Token MUST be used as the new implementation for the bridged vbToken or be custom mapped to the corresponding vbToken on Agglayer Bridge on Secondary Chain, and MUST give the minting and burning permission to Agglayer Bridge and Native Converter. It MAY have a transfer fee. abstract contract CustomToken is Initializable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, ERC20PermitUpgradeable, - IVersioned + InitializationCounterUpgradeable, + Versioned { /// @dev Storage of Custom Token contract. /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. /// @custom:storage-location erc7201:agglayer.vault-bridge.CustomToken.storage struct CustomTokenStorage { uint8 decimals; - address lxlyBridge; + address bridge; address nativeConverter; } @@ -40,6 +44,7 @@ abstract contract CustomToken is hex"0300d81ec8b5c42d6bd2cedd81ce26f1003c52753656b7512a8eef168b702500"; // Basic roles. + // @remind Document. bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // Errors. @@ -48,32 +53,21 @@ abstract contract CustomToken is error InvalidName(); error InvalidSymbol(); error InvalidOriginalUnderlyingTokenDecimals(); - error InvalidLxLyBridge(); - error InvalidNativeConverter(); - - // -----================= ::: MODIFIERS ::: =================----- - - /// @dev Checks if the sender is LxLy Bridge or Native Converter. - /// @dev This modifier is used to restrict the minting and burning of Custom Token. - modifier onlyLxlyBridgeAndNativeConverter() { - CustomTokenStorage storage $ = _getCustomTokenStorage(); - - // Only LxLy Bridge and Native Converter can mint and burn Custom Token. - require(msg.sender == $.lxlyBridge || msg.sender == $.nativeConverter, Unauthorized()); - - _; - } + error InvalidBridge(); + error BridgeAlreadySet(); + error NativeConverterAlreadySet(); // -----================= ::: SETUP ::: =================----- - /// @param originalUnderlyingTokenDecimals_ The number of decimals of the original underlying token on Layer X. Custom Token will have the same number of decimals as the original underlying token. + /// @dev Preserves the `name` and `symbol` of the bridged vbToken. + /// @param originalUnderlyingTokenDecimals_ The number of decimals of the original underlying token on Primary Chain. Custom Token will have the same number of decimals as the original underlying token. /// @param nativeConverter_ The address of Native Converter for this Custom Token. - function __CustomToken_init( + function __CustomToken_init1( address owner_, - string calldata name_, - string calldata symbol_, + string memory name_, + string memory symbol_, uint8 originalUnderlyingTokenDecimals_, - address lxlyBridge_, + address bridge_, address nativeConverter_ ) internal onlyInitializing { CustomTokenStorage storage $ = _getCustomTokenStorage(); @@ -83,8 +77,7 @@ abstract contract CustomToken is require(bytes(name_).length > 0, InvalidName()); require(bytes(symbol_).length > 0, InvalidSymbol()); require(originalUnderlyingTokenDecimals_ > 0, InvalidOriginalUnderlyingTokenDecimals()); - require(lxlyBridge_ != address(0), InvalidLxLyBridge()); - require(nativeConverter_ != address(0), InvalidNativeConverter()); + require(bridge_ != address(0), InvalidBridge()); // Initialize the inherited contracts. __ERC20_init(name_, symbol_); @@ -102,26 +95,45 @@ abstract contract CustomToken is // Initialize the storage. $.decimals = originalUnderlyingTokenDecimals_; - $.lxlyBridge = lxlyBridge_; + $.bridge = bridge_; $.nativeConverter = nativeConverter_; } + // @remind Document (the entire function). + function __CustomToken_init2() + internal + onlyInitializing + incrementsLocalInitializationCounter(1) + incrementsLocalInitializationCounter(2) + { + // Empty function body. + } + + /* + /// @dev How to add a new init step: + function __CustomToken_init3() internal onlyInitializing incrementsLocalInitializationCounter(3) {} + */ + + // @remind Document. + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure virtual; + // -----================= ::: STORAGE ::: =================----- /// @notice The number of decimals of Custom Token. - /// @notice The number of decimals is the same as that of the original underlying token on Layer X. + /// @notice The number of decimals is the same as that of the original underlying token on Primary Chain. function decimals() public view override returns (uint8) { CustomTokenStorage storage $ = _getCustomTokenStorage(); return $.decimals; } - /// @notice LxLy Bridge, which connects AggLayer networks. - function lxlyBridge() public view returns (address) { + /// @notice The contract that connects Custom Token to Primary Chain. + function bridge() public view returns (address) { CustomTokenStorage storage $ = _getCustomTokenStorage(); - return $.lxlyBridge; + return $.bridge; } /// @notice The address of Native Converter for this Custom Token. + /// @return Returns `address(0)` if Native Converter is not connected. function nativeConverter() public view returns (address) { CustomTokenStorage storage $ = _getCustomTokenStorage(); return $.nativeConverter; @@ -137,30 +149,23 @@ abstract contract CustomToken is // -----================= ::: ERC-20 ::: =================----- /// @dev Pausable ERC-20 `transfer` function. - function transfer(address to, uint256 value) public virtual override whenNotPaused returns (bool) { + function transfer(address to, uint256 value) public override whenNotPaused returns (bool) { return super.transfer(to, value); } /// @dev Pausable ERC-20 `transferFrom` function. - function transferFrom(address from, address to, uint256 value) - public - virtual - override - whenNotPaused - returns (bool) - { + function transferFrom(address from, address to, uint256 value) public override whenNotPaused returns (bool) { return super.transferFrom(from, to, value); } /// @dev Pausable ERC-20 `approve` function. - function approve(address spender, uint256 value) public virtual override whenNotPaused returns (bool) { + function approve(address spender, uint256 value) public override whenNotPaused returns (bool) { return super.approve(spender, value); } /// @dev Pausable ERC-20 Permit `permit` function. function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public - virtual override whenNotPaused { @@ -169,36 +174,32 @@ abstract contract CustomToken is // -----================= ::: CUSTOM TOKEN ::: =================----- - /// @notice Mints Custom Tokens to the recipient. - /// @notice This function can be called by LxLy Bridge and Native Converter only. - function mint(address account, uint256 value) - external - whenNotPaused - onlyLxlyBridgeAndNativeConverter - nonReentrant - { - // When we migrate backing to Lx, we end up sending tokens to address(0) here. - // These need to be claimable so the bridge accounting is correct and we allow it here by not reverting. - if (account == address(0)) return; + // @remind Document. + function _CUSTOM_TOKEN_IS_MINTABLE_BURNABLE() internal virtual; + + // @remind Document (the entire function). + function setBridge(address bridge_) external onlyRole(DEFAULT_ADMIN_ROLE) { + CustomTokenStorage storage $ = _getCustomTokenStorage(); + + require($.bridge.codehash == keccak256(hex"60006000fd"), BridgeAlreadySet()); + require(bridge_ != address(0), InvalidBridge()); - _mint(account, value); + $.bridge = bridge_; } - /// @notice Burns Custom Tokens from a holder. - /// @notice This function can be called by LxLy Bridge and Native Converter only. - function burn(address account, uint256 value) - external - whenNotPaused - onlyLxlyBridgeAndNativeConverter - nonReentrant - { - _burn(account, value); + // @remind Document (the entire function). + function setNativeConverter(address nativeConverter_) external onlyRole(DEFAULT_ADMIN_ROLE) { + CustomTokenStorage storage $ = _getCustomTokenStorage(); + + require($.nativeConverter == address(0), NativeConverterAlreadySet()); + + $.nativeConverter = nativeConverter_; } // -----================= ::: ADMIN ::: =================----- /// @notice Prevents usage of functions with the `whenNotPaused` modifier. - /// @notice This function can be called by the pauser only. + /// @notice This function can be called by a pauser only. function pause() external onlyRole(PAUSER_ROLE) nonReentrant { _pause(); } diff --git a/src/secondary-chain/CustomTokenWethExtension.sol b/src/secondary-chain/CustomTokenWethExtension.sol new file mode 100644 index 00000000..99ce68aa --- /dev/null +++ b/src/secondary-chain/CustomTokenWethExtension.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/CustomTokenWethExtension.sol) + +pragma solidity 0.8.29; + +// @remind Document the entire file. + +// Main functionality. +import {CustomToken} from "./CustomToken.sol"; + +// External contracts. +import {IAgglayerBridge} from "../etc/IAgglayerBridge.sol"; +import {NativeConverter} from "./NativeConverter.sol"; + +/// @title Custom Token WETH Extension +/// @author See https://github.com/agglayer/vault-bridge +abstract contract CustomTokenWethExtension is CustomToken { + /// @dev Storage of Custom Token WETH Extension. + /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. + /// @custom:storage-location erc7201:agglayer.vault-bridge.CustomTokenWethExtension.storage + struct CustomTokenWethExtensionStorage { + bool _gasTokenIsEth; + uint256 gasBackingOnSecondaryChain; + bool wethFunctionalityEnabled; + } + + /// @dev The storage slot at which Custom Token WETH Extension storage starts, following the EIP-7201 standard. + /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.CustomTokenWethExtension.storage")) - 1)) & ~bytes32(uint256(0xff))`. + bytes32 private constant _CUSTOM_TOKEN_WETH_EXTENSION_STORAGE = + hex"79530e5f68ac2fe03ca888330cb59cd18fe7ab48bdc97271c9f69b4c84c28700"; + + error FunctionNotSupportedOnThisChain(); + error FunctionNotEnabledOnThisChain(); + error AssetsTooLarge(uint256 availableAssets, uint256 requestedAssets); + error WithdrawalFailed(); + error WethFunctionalityCannotBeEnabledIfGasTokenIsNotEth(); + + event Deposit(address indexed from, uint256 value); + event Withdrawal(address indexed to, uint256 value); + + modifier onlyNativeConverter() { + require(msg.sender == nativeConverter(), Unauthorized()); + _; + } + + modifier onlyIfGasTokenIsEth() { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + require($._gasTokenIsEth, FunctionNotSupportedOnThisChain()); + _; + } + + modifier onlyIfWethFunctionalityEnabled() { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + require($.wethFunctionalityEnabled, FunctionNotEnabledOnThisChain()); + _; + } + + function __CustomTokenWethExtension_init2_ext1(bool gasTokenIsEth_, bool wethFunctionalityEnabled_) + internal + onlyInitializing + incrementsExtensionInitializationCounter(2, Extension.WETH, 1) + { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + + $._gasTokenIsEth = gasTokenIsEth_; + + // @note CAUTION! ALL WETH NATIVE CONVERTER MIGRATIONS THAT ARE IN PROGRESS MUST BE COMPLETED FIRST! + // @todo THIS LOGIC WILL BE REMOVED ONCE VBETH ON KATANA/BOKUTO HAS BEEN UPGRADED TO VAULT BRIDGE V1.0.0 AND VAULT BRIDGE V0.5.0 HAS BEEN DEPRECATED. + if (block.chainid == 747474 || block.chainid == 737373) { + uint256 wethBridgedSupply = IAgglayerBridge(bridge()).localBalanceTree( + block.chainid == 747474 + ? bytes32(0x56c62e67b0be3f302f4835a408fa9ba657546fd11907c2c30306d84790975467) + : bytes32(0x0b13348aaf539fc7929ee5a1b19220fdcf7c38ae12b877539fe54abaf7a6d0dd) + ); + uint256 wethTotalSupply = totalSupply(); + uint256 wethBackingOnSecondaryChain = NativeConverter(payable(nativeConverter())).backingOnSecondaryChain(); + + $.gasBackingOnSecondaryChain = wethTotalSupply - wethBridgedSupply - wethBackingOnSecondaryChain; + + assert($.gasBackingOnSecondaryChain <= address(this).balance); + } + + $.wethFunctionalityEnabled = wethFunctionalityEnabled_; + } + + /* + /// @dev How to add a new ext step: + function __CustomTokenWethExtension_initX_extY() + internal + onlyInitializing + incrementsExtensionInitializationCounter(X, Extension.WETH, Y) + {} + */ + + function _CUSTOM_TOKEN_WETH_EXTENSION_INIT_2_EXT_1_COMPATIBLE() internal pure virtual; + + function gasBackingOnSecondaryChain() public view returns (uint256) { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + return $.gasBackingOnSecondaryChain; + } + + function wethFunctionalityEnabled() public view returns (bool) { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + return $.wethFunctionalityEnabled; + } + + /// @notice Same as WETH9 deposit function. + function deposit() external payable whenNotPaused onlyIfWethFunctionalityEnabled onlyIfGasTokenIsEth nonReentrant { + _deposit(); + } + + function _deposit() internal { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + $.gasBackingOnSecondaryChain += msg.value; + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + /// @notice Same as WETH9 withdraw function, but liqudity is guaranteed only up to a certain percentage. + function withdraw(uint256 value) external whenNotPaused onlyIfGasTokenIsEth nonReentrant { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + require(value <= $.gasBackingOnSecondaryChain, AssetsTooLarge($.gasBackingOnSecondaryChain, value)); + $.gasBackingOnSecondaryChain -= value; + _burn(msg.sender, value); + (bool ok,) = msg.sender.call{value: value}(""); + require(ok, WithdrawalFailed()); + emit Withdrawal(msg.sender, value); + } + + function bridgeBackingToPrimaryChain(uint256 amount) + external + whenNotPaused + onlyIfGasTokenIsEth + onlyNativeConverter + nonReentrant + { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + require(amount <= $.gasBackingOnSecondaryChain, AssetsTooLarge($.gasBackingOnSecondaryChain, amount)); + $.gasBackingOnSecondaryChain -= amount; + (bool ok,) = nativeConverter().call{value: amount}(""); + require(ok); + } + + function setWethFunctionalityEnabled(bool wethFunctionalityEnabled_) external onlyRole(DEFAULT_ADMIN_ROLE) { + CustomTokenWethExtensionStorage storage $ = _getCustomTokenWethExtensionStorage(); + if (wethFunctionalityEnabled_) require($._gasTokenIsEth, WethFunctionalityCannotBeEnabledIfGasTokenIsNotEth()); + $.wethFunctionalityEnabled = wethFunctionalityEnabled_; + } + + function _getCustomTokenWethExtensionStorage() private pure returns (CustomTokenWethExtensionStorage storage $) { + assembly { + $.slot := _CUSTOM_TOKEN_WETH_EXTENSION_STORAGE + } + } +} diff --git a/src/NativeConverter.sol b/src/secondary-chain/NativeConverter.sol similarity index 59% rename from src/NativeConverter.sol rename to src/secondary-chain/NativeConverter.sol index 2a9b6e31..e77d3620 100644 --- a/src/NativeConverter.sol +++ b/src/secondary-chain/NativeConverter.sol @@ -1,4 +1,6 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/NativeConverter.sol) + pragma solidity 0.8.29; // Other functionality. @@ -6,8 +8,9 @@ import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Ini import {AccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable/access/AccessControlUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {ReentrancyGuardUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; -import {ERC20PermitUser} from "./etc/ERC20PermitUser.sol"; -import {IVersioned} from "./etc/IVersioned.sol"; +import {ERC20PermitUser} from "../etc/ERC20PermitUser.sol"; +import {InitializationCounterUpgradeable} from "../etc/InitializationCounterUpgradeable.sol"; +import {Versioned} from "../etc/Versioned.sol"; // Libraries. import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -15,23 +18,26 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; // External contracts. import {CustomToken} from "./CustomToken.sol"; -import {ILxLyBridge} from "./etc/ILxLyBridge.sol"; -import {MigrationManager} from "./MigrationManager.sol"; +import {CustomTokenWethExtension} from "./CustomTokenWethExtension.sol"; +import {IAgglayerBridge} from "../etc/IAgglayerBridge.sol"; +import {MigrationManager} from "../primary-chain/MigrationManager.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; /// @title Native Converter /// @author See https://github.com/agglayer/vault-bridge -/// @notice Native Converter lives on Layer Ys and converts the underlying token (usually the bridge-wrapped version of the original underlying token from Layer X) to Custom Token, and vice versa, on demand. It can also migrate backing for Custom Token it has minted to Layer X, where vbToken will be minted and locked in LxLy Bridge. Please refer to `migrateBackingToLayerX` for more information. +/// @notice Native Converter is an optional contract on Secondary Chains that converts the underlying token (usually the bridge-wrapped version of the original underlying token from Primary Chain) to Custom Token, and vice versa, on demand. It can also migrate backing for Custom Token it has minted to Primary Chain, where vbToken will be minted and locked in Agglayer Bridge. Please refer to `migrateBackingToPrimaryChain` for more information. +/// @dev A base contract used to create Native Converters. /// @dev @note (ATTENTION) This contract MUST have mint and burn permission on Custom Token. Please refer to `CustomToken.sol` for more information. -/// @dev @note IMPORTANT: The underlying token MUST NOT be a rebasing token, and MUST NOT have transfer hooks (i.e., enable reentrancy). +/// @dev @note IMPORTANT: The underlying token MUST NOT be a rebasing token, and MUST NOT have transfer hooks (i.e., enable reentrancy); it MAY have a transfer fee. abstract contract NativeConverter is Initializable, AccessControlUpgradeable, PausableUpgradeable, ReentrancyGuardUpgradeable, ERC20PermitUser, - IVersioned + InitializationCounterUpgradeable, + Versioned { // Libraries. using SafeERC20 for IERC20; @@ -43,12 +49,16 @@ abstract contract NativeConverter is struct NativeConverterStorage { CustomToken customToken; IERC20 underlyingToken; - uint256 backingOnLayerY; - uint32 lxlyId; - ILxLyBridge lxlyBridge; - uint32 layerXLxlyId; + uint256 backingOnSecondaryChain; + uint32 agglayerId; + IAgglayerBridge bridge; + uint32 primaryChainAgglayerId; uint256 nonMigratableBackingPercentage; address migrationManager; + bool _underlyingTokenIsNotMintable; + mapping(uint256 migratedBacking => uint256 times) _migrationsInProgress; + uint256 _migrationsInProgressCount; + uint256 _totalMigratedBackingInProgress; } /// @dev The storage slot at which Native Converter storage starts, following the EIP-7201 standard. @@ -57,18 +67,19 @@ abstract contract NativeConverter is hex"a14770e0debfe4b8406a01c33ee3a7bbe0acc66b3bde7c71854bf7d080a9c600"; // Basic roles. + // @remind Document. bytes32 public constant MIGRATOR_ROLE = keccak256("MIGRATOR_ROLE"); bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); // Errors. + error Unauthorized(); error InvalidOwner(); error InvalidCustomToken(); error InvalidUnderlyingToken(); - error InvalidLxLyBridge(); - error InvalidLayerXLxlyId(); + error InvalidAgglayerBridge(); + error InvalidPrimaryChainAgglayerId(); error InvalidMigrationManager(); - error NonMatchingCustomTokenDecimals(uint8 customTokenDecimals, uint8 originalUnderlyingTokenDecimals); - error NonMatchingUnderlyingTokenDecimals(uint8 underlyingTokenDecimals, uint8 originalUnderlyingTokenDecimals); + error NonMatchingTokenDecimals(uint8 customTokenDecimals, uint8 underlyingTokenDecimals); error InvalidAssets(); error InvalidReceiver(); error InvalidPermitData(); @@ -76,25 +87,38 @@ abstract contract NativeConverter is error InvalidNonMigratableBackingPercentage(); error AssetsTooLarge(uint256 availableAssets, uint256 requestedAssets); error InvalidDestinationNetworkId(); - error OnlyMigrator(); + error CannotSetCustomTokenIfBackingOnSecondaryChainIsNotZero(); + error CannotSetCustomTokenIfGasBackingOnSecondaryChainIsNotZero(); // Events. event MigrationStarted(uint256 indexed mintedCustomToken, uint256 indexed migratedBacking); event NonMigratableBackingPercentageSet(uint256 nonMigratableBackingPercentage); + event MigrationInProgressAdded(uint256 indexed migratedBacking); + event MigrationInProgressRemoved(uint256 indexed mintedCustomToken); + + // -----================= ::: MODIFIERS ::: =================----- + + /// @dev Checks if the sender is the yield recipient. + modifier onlyCustomToken() { + NativeConverterStorage storage $ = _getNativeConverterStorage(); + require(msg.sender == address($.customToken), Unauthorized()); + _; + } // -----================= ::: SETUP ::: =================----- - /// @param originalUnderlyingTokenDecimals_ The number of decimals of the original underlying token on Layer X. The `customToken` and `underlyingToken` MUST have the same number of decimals as the original underlying token. @note (ATTENTION) The decimals of the `customToken` and `underlyingToken` will default to 18 if they revert. - /// @param customToken_ The token custom mapped to vbToken on LxLy Bridge on Layer Y. Native Converter must be able to mint and burn this token. Please refer to `CustomToken.sol` for more information. - /// @param underlyingToken_ The token that represents the original underlying token on Layer Y. @note IMPORTANT: This token MUST be either the bridge-wrapped version of the original underlying token, or the original underlying token must be custom mapped to this token on LxLy Bridge on Layer Y. - /// @param nonMigratableBackingPercentage_ The percentage of backing that should remain in Native Converter after a migration, based on the total supply of Custom Token. 1e18 is 100%. It is possible to game the system by manipulating the total supply of Custom Token, so this is a soft limit. - function __NativeConverter_init( + /// @dev The `customToken` and `underlyingToken` MUST have the same number of decimals. @note (ATTENTION) The decimals of the `customToken` and `underlyingToken` will default to `18` if they revert on `decimals`. + /// @param owner_ (ATTENTION) This address will be granted the `DEFAULT_ADMIN_ROLE`, as well as all basic roles. Roles can be modified at any time. + /// @param customToken_ The upgraded version of the bridged vbToken. Native Converter must be able to mint and burn this token. Please refer to `CustomToken.sol` for more information. + /// @param underlyingToken_ The token that represents the original underlying token on Secondary Chain. @note IMPORTANT: This token MUST be either the bridge-wrapped version of the original underlying token, or the original underlying token must be custom mapped to this token on Agglayer Bridge on Secondary Chain. + /// @param nonMigratableBackingPercentage_ The percentage of backing that should remain in Native Converter when migrating backing to Primary Chain, based on the total supply of Custom Token. `1e18` is 100%. It is possible to game the system by manipulating the total supply of Custom Token, so this is more of a soft limit. + /// @param migrationManager_ The address of the Migration Manager on Primary Chain. + function __NativeConverter_init1( address owner_, - uint8 originalUnderlyingTokenDecimals_, address customToken_, address underlyingToken_, - address lxlyBridge_, - uint32 layerXLxlyId_, + address agglayerBridge_, + uint32 primaryChainAgglayerId_, uint256 nonMigratableBackingPercentage_, address migrationManager_ ) internal onlyInitializing { @@ -104,35 +128,33 @@ abstract contract NativeConverter is require(owner_ != address(0), InvalidOwner()); require(customToken_ != address(0), InvalidCustomToken()); require(underlyingToken_ != address(0), InvalidUnderlyingToken()); - require(lxlyBridge_ != address(0), InvalidLxLyBridge()); - require(layerXLxlyId_ != ILxLyBridge(lxlyBridge_).networkID(), InvalidLxLyBridge()); + require(agglayerBridge_ != address(0), InvalidAgglayerBridge()); + require(primaryChainAgglayerId_ != IAgglayerBridge(agglayerBridge_).networkID(), InvalidAgglayerBridge()); require(migrationManager_ != address(0), InvalidMigrationManager()); require(nonMigratableBackingPercentage_ <= 1e18, InvalidNonMigratableBackingPercentage()); - // Check Custom Token's decimals. + // Get Custom Token's decimals. uint8 customTokenDecimals; try IERC20Metadata(customToken_).decimals() returns (uint8 decimals) { customTokenDecimals = decimals; } catch { - // Default to 18 decimals. + // Default to 18 decimals if Custom Token reverted. customTokenDecimals = 18; } - require( - customTokenDecimals == originalUnderlyingTokenDecimals_, - NonMatchingCustomTokenDecimals(customTokenDecimals, originalUnderlyingTokenDecimals_) - ); - // Check the underlying token's decimals. + // Get the underlying token's decimals. uint8 underlyingTokenDecimals; - try IERC20Metadata(underlyingToken_).decimals() returns (uint8 decimals_) { - underlyingTokenDecimals = decimals_; + try IERC20Metadata(underlyingToken_).decimals() returns (uint8 decimals) { + underlyingTokenDecimals = decimals; } catch { - // Default to 18 decimals. + // Default to 18 decimals if the underlying token reverted. underlyingTokenDecimals = 18; } + + // Check the tokens' decimals. require( - underlyingTokenDecimals == originalUnderlyingTokenDecimals_, - NonMatchingUnderlyingTokenDecimals(underlyingTokenDecimals, originalUnderlyingTokenDecimals_) + customTokenDecimals == underlyingTokenDecimals, + NonMatchingTokenDecimals(customTokenDecimals, underlyingTokenDecimals) ); // Initialize the inherited contracts. @@ -150,61 +172,81 @@ abstract contract NativeConverter is // Initialize the storage. $.customToken = CustomToken(customToken_); $.underlyingToken = IERC20(underlyingToken_); - $.lxlyId = ILxLyBridge(lxlyBridge_).networkID(); - $.lxlyBridge = ILxLyBridge(lxlyBridge_); - $.layerXLxlyId = layerXLxlyId_; + $.agglayerId = IAgglayerBridge(agglayerBridge_).networkID(); + $.bridge = IAgglayerBridge(agglayerBridge_); + $.primaryChainAgglayerId = primaryChainAgglayerId_; $.migrationManager = migrationManager_; $.nonMigratableBackingPercentage = nonMigratableBackingPercentage_; } + // @remind Document (the entire function). + function __NativeConverter_init2() + internal + onlyInitializing + incrementsLocalInitializationCounter(1) + incrementsLocalInitializationCounter(2) + { + NativeConverterStorage storage $ = _getNativeConverterStorage(); + + $._underlyingTokenIsNotMintable = $.bridge.wrappedAddressIsNotMintable(address($.underlyingToken)); + } + + /* + /// @dev How to add a new init step: + function __NativeConverter_init3() internal onlyInitializing incrementsLocalInitializationCounter(3) {} + */ + + // @remind Document. + function _NATIVE_CONVERTER_INIT_2_COMPATIBLE() internal pure virtual; + // -----================= ::: STORAGE ::: =================----- - /// @notice The token custom mapped to vbToken on LxLy Bridge on Layer Y. + /// @notice The upgraded version of the bridged vbToken. function customToken() public view returns (IERC20) { NativeConverterStorage storage $ = _getNativeConverterStorage(); return $.customToken; } - /// @notice The token that represent the original underlying token on Layer Y. + /// @notice The token that represent the original underlying token on Secondary Chain. function underlyingToken() public view returns (IERC20) { NativeConverterStorage storage $ = _getNativeConverterStorage(); return $.underlyingToken; } - /// @notice The amount of the underlying token that backs Custom Token minted by Native Converter on Layer Y that has not been migrated to Layer X. + /// @notice The amount of the underlying token that backs Custom Token minted by Native Converter on Secondary Chain that has not been migrated to Primary Chain. /// @dev The amount is used in accounting and may be different from Native Converter's underlying token balance. @note IMPORTANT: You may do as you wish with surplus underlying token balance, but you MUST NOT designate it as backing. - function backingOnLayerY() public view returns (uint256) { + function backingOnSecondaryChain() public view returns (uint256) { NativeConverterStorage storage $ = _getNativeConverterStorage(); - return $.backingOnLayerY; + return $.backingOnSecondaryChain; } - /// @notice The LxLy ID of this network. - function lxlyId() public view returns (uint32) { + /// @notice The Agglayer ID of this network. + function agglayerId() public view returns (uint32) { NativeConverterStorage storage $ = _getNativeConverterStorage(); - return $.lxlyId; + return $.agglayerId; } - /// @notice LxLy Bridge, which connects AggLayer networks. - function lxlyBridge() public view returns (ILxLyBridge) { + /// @notice Agglayer Bridge, which connects AggLayer networks. + function bridge() public view returns (IAgglayerBridge) { NativeConverterStorage storage $ = _getNativeConverterStorage(); - return $.lxlyBridge; + return $.bridge; } - /// @notice The LxLy ID of Layer X. - function layerXLxlyId() public view returns (uint32) { + /// @notice The Agglayer ID of Primary Chain. + function primaryChainAgglayerId() public view returns (uint32) { NativeConverterStorage storage $ = _getNativeConverterStorage(); - return $.layerXLxlyId; + return $.primaryChainAgglayerId; } - /// @notice The percentage of backing that should remain in Native Converter after a migration, based on the total supply of Custom Token. - /// @dev It is possible to game the system by manipulating the total supply of Custom Token, so this is a soft limit. + /// @notice The percentage of backing that should remain in Native Converter when migrating backing to Primary Chain, based on the total supply of Custom Token. + /// @dev It is possible to game the system by manipulating the total supply of Custom Token, so this is more of a soft limit. /// @return 1e18 is 100%. function nonMigratableBackingPercentage() public view returns (uint256) { NativeConverterStorage storage $ = _getNativeConverterStorage(); return $.nonMigratableBackingPercentage; } - // @remind Document. + /// @notice The address of the Migration Manager on Primary Chain. function migrationManager() public view returns (MigrationManager) { NativeConverterStorage storage $ = _getNativeConverterStorage(); return MigrationManager(payable($.migrationManager)); @@ -217,7 +259,7 @@ abstract contract NativeConverter is } } - // -----================= ::: PSEUDO-ERC-4626 ::: =================----- + // -----================= ::: NATIVE CONVERTER ::: =================----- /// @notice Deposit a specific amount of the underlying token and get Custom Token. /// @param assets The amount of the underlying token to convert to Custom Token. @@ -240,13 +282,13 @@ abstract contract NativeConverter is assets = _receiveUnderlyingToken(msg.sender, assets); // Update the backing data. - $.backingOnLayerY += assets; + $.backingOnSecondaryChain += assets; // Set the return value. shares = _convertToShares(assets); // Mint Custom Token to the receiver. - $.customToken.mint(receiver, shares); + _mintCustomToken(receiver, shares); } /// @notice Deposit a specific amount of the underlying token and get Custom Token. @@ -300,9 +342,9 @@ abstract contract NativeConverter is uint256 remainingAssets = assets; // Simulate deconversion. - uint256 backingOnLayerY_ = $.backingOnLayerY; - if (backingOnLayerY_ >= remainingAssets) return shares; - remainingAssets -= backingOnLayerY_; + uint256 backingOnSecondaryChain_ = $.backingOnSecondaryChain; + if (backingOnSecondaryChain_ >= remainingAssets) return shares; + remainingAssets -= backingOnSecondaryChain_; // Calculate the converted amount. uint256 convertedAssets = assets - remainingAssets; @@ -319,7 +361,7 @@ abstract contract NativeConverter is /// @return assets The amount of the underlying token unlocked to the receiver. function deconvert(uint256 shares, address receiver) external whenNotPaused nonReentrant returns (uint256 assets) { NativeConverterStorage storage $ = _getNativeConverterStorage(); - return _deconvert(shares, $.lxlyId, receiver, false); + return _deconvert(shares, $.agglayerId, receiver, false); } /// @notice Burn a specific amount of Custom Token to unlock a respective amount of the underlying token, and bridge it to another network. @@ -334,7 +376,7 @@ abstract contract NativeConverter is NativeConverterStorage storage $ = _getNativeConverterStorage(); // Check the input. - require(destinationNetworkId != $.lxlyId, InvalidDestinationNetworkId()); + require(destinationNetworkId != $.agglayerId, InvalidDestinationNetworkId()); return _deconvert(shares, destinationNetworkId, receiver, forceUpdateGlobalExitRoot); } @@ -357,24 +399,24 @@ abstract contract NativeConverter is assets = _convertToAssets(shares); // Get the available backing. - uint256 backingOnLayerY_ = backingOnLayerY(); + uint256 backingOnSecondaryChain_ = backingOnSecondaryChain(); // Revert if there is not enough backing. - require(backingOnLayerY_ >= assets, AssetsTooLarge(backingOnLayerY_, assets)); + require(backingOnSecondaryChain_ >= assets, AssetsTooLarge(backingOnSecondaryChain_, assets)); // Update the backing data. - $.backingOnLayerY -= assets; + $.backingOnSecondaryChain -= assets; // Burn Custom Token. - $.customToken.burn(msg.sender, shares); + _burnCustomToken(msg.sender, shares); // Withdraw the underlying token. - if (destinationNetworkId == $.lxlyId) { + if (destinationNetworkId == $.agglayerId) { // Withdraw to the receiver. _sendUnderlyingToken(receiver, assets); } else { // Bridge to the receiver. - $.lxlyBridge.bridgeAsset( + $.bridge.bridgeAsset( destinationNetworkId, receiver, assets, address($.underlyingToken), forceUpdateGlobalExitRoot, "" ); } @@ -400,25 +442,26 @@ abstract contract NativeConverter is // -----================= ::: NATIVE CONVERTER ::: =================----- - /// @notice The maximum amount of backing that can be migrated to Layer X. + /// @notice The maximum amount of backing that can be migrated to Primary Chain. function migratableBacking() public view returns (uint256) { NativeConverterStorage storage $ = _getNativeConverterStorage(); // Calculate the non-migratable backing. - uint256 nonMigratableBacking = - _convertToAssets(Math.mulDiv(customToken().totalSupply(), $.nonMigratableBackingPercentage, 1e18)); + uint256 nonMigratableBacking = _convertToAssets( + Math.mulDiv(customToken().totalSupply(), $.nonMigratableBackingPercentage, 1e18, Math.Rounding.Ceil) + ); // Return the amount of backing that can be migrated. - return $.backingOnLayerY > nonMigratableBacking ? $.backingOnLayerY - nonMigratableBacking : 0; + return $.backingOnSecondaryChain > nonMigratableBacking ? $.backingOnSecondaryChain - nonMigratableBacking : 0; } - /// @notice Migrates a specific amount of backing to Layer X. - /// @notice This action provides vbToken liquidity on LxLy Bridge on Layer X. - /// @notice The bridged asset and message must be claimed manually on LxLy Bridge on Layer X to complete the migration. - /// @notice This function can be called by the migrator only. - /// @notice The migration can be completed by anyone on Layer X. - /// @dev Consider calling this function periodically; anyone can complete a migration on Layer X. - function migrateBackingToLayerX(uint256 assets) external whenNotPaused onlyRole(MIGRATOR_ROLE) nonReentrant { + /// @notice Migrates a specific amount of backing to Primary Chain. + /// @notice This action provides vbToken liquidity on Agglayer Bridge on Primary Chain. + /// @notice The bridged asset and message must be claimed manually on Agglayer Bridge on Primary Chain to complete the migration. + /// @notice This function can be called by a migrator only. + /// @notice The migration can be completed by anyone on Primary Chain. + /// @dev Consider calling this function periodically; anyone can complete a migration on Primary Chain. + function migrateBackingToPrimaryChain(uint256 assets) external whenNotPaused onlyRole(MIGRATOR_ROLE) nonReentrant { NativeConverterStorage storage $ = _getNativeConverterStorage(); // Cache the migratable backing. @@ -429,20 +472,47 @@ abstract contract NativeConverter is require(assets <= migratableBacking_, AssetsTooLarge(migratableBacking_, assets)); // Update the backing data. - $.backingOnLayerY -= assets; + $.backingOnSecondaryChain -= assets; + + // @remind Document. + _addMigrationInProgress(assets); // Calculate the amount of Custom Token for which backing is being migrated. uint256 shares = _convertToShares(assets); - // Bridge the backing to Migration Manager on Layer X. - $.lxlyBridge.bridgeAsset($.layerXLxlyId, $.migrationManager, assets, address($.underlyingToken), true, ""); + // Bridge the backing to Migration Manager on Primary Chain. + /* If the underlying token is not mintable by Agglayer Bridge, we need to check for a transfer fee. */ + if ($._underlyingTokenIsNotMintable) { + // Cache the balance. + uint256 balanceBefore = $.underlyingToken.balanceOf(address($.bridge)); - // Bridge a message to Migration Manager on Layer X to complete the migration. - $.lxlyBridge.bridgeMessage( - $.layerXLxlyId, + // Bridge. + // @note IMPORTANT: Make sure the underlying token you are integrating does not enable reentrancy on `transferFrom`. + $.bridge.bridgeAsset( + $.primaryChainAgglayerId, $.migrationManager, assets, address($.underlyingToken), true, "" + ); + + uint256 originalAssets = assets; + + // Calculate the bridged amount. + assets = $.underlyingToken.balanceOf(address($.bridge)) - balanceBefore; + + // Try to prevent a mistake in case Agglayer Bridge code changes. + assert(assets > 0 && originalAssets >= assets); + } + /* If the underlying token is mintable by Agglayer Bridge, it will be burned (not transferred). */ + else { + $.bridge.bridgeAsset( + $.primaryChainAgglayerId, $.migrationManager, assets, address($.underlyingToken), true, "" + ); + } + + // Bridge a message to Migration Manager on Primary Chain to complete the migration. + $.bridge.bridgeMessage( + $.primaryChainAgglayerId, $.migrationManager, true, - abi.encode(MigrationManager.CrossNetworkInstruction.COMPLETE_MIGRATION, abi.encode(shares, assets)) + abi.encode(MigrationManager.CrossChainInstruction._0_COMPLETE_MIGRATION, abi.encode(shares, assets)) ); // Emit the event. @@ -470,6 +540,76 @@ abstract contract NativeConverter is emit NonMigratableBackingPercentageSet(nonMigratableBackingPercentage_); } + // @remind Document (the entire function). + function removeMigrationInProgress(uint256 mintedCustomToken) external whenNotPaused onlyCustomToken nonReentrant { + _removeMigrationInProgress(mintedCustomToken); + + emit MigrationInProgressRemoved(mintedCustomToken); + } + + // @remind Document (the entire function). + function _addMigrationInProgress(uint256 migratedBacking) internal { + NativeConverterStorage storage $ = _getNativeConverterStorage(); + + $._migrationsInProgress[migratedBacking]++; + $._migrationsInProgressCount++; + $._totalMigratedBackingInProgress += migratedBacking; + + emit MigrationInProgressAdded(migratedBacking); + } + + // @remind Document (the entire function). + function _removeMigrationInProgress(uint256 mintedCustomToken) private { + NativeConverterStorage storage $ = _getNativeConverterStorage(); + + $._migrationsInProgress[mintedCustomToken]--; + $._migrationsInProgressCount--; + $._totalMigratedBackingInProgress -= mintedCustomToken; + + emit MigrationInProgressRemoved(mintedCustomToken); + } + + // @remind Document (the entire function). + function setCustomToken(address customToken_) external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { + NativeConverterStorage storage $ = _getNativeConverterStorage(); + + require(customToken_ != address(0), InvalidCustomToken()); + + require($.backingOnSecondaryChain == 0, CannotSetCustomTokenIfBackingOnSecondaryChainIsNotZero()); + + try CustomTokenWethExtension(address($.customToken)).gasBackingOnSecondaryChain() returns ( + uint256 gasBackingOnSecondaryChain + ) { + require(gasBackingOnSecondaryChain == 0, CannotSetCustomTokenIfGasBackingOnSecondaryChainIsNotZero()); + } catch {} + + // Get Custom Token's decimals. + uint8 customTokenDecimals; + try IERC20Metadata(customToken_).decimals() returns (uint8 decimals) { + customTokenDecimals = decimals; + } catch { + // Default to 18 decimals if Custom Token reverted. + customTokenDecimals = 18; + } + + // Get the underlying token's decimals. + uint8 underlyingTokenDecimals; + try IERC20Metadata(address($.underlyingToken)).decimals() returns (uint8 decimals) { + underlyingTokenDecimals = decimals; + } catch { + // Default to 18 decimals if the underlying token reverted. + underlyingTokenDecimals = 18; + } + + // Check the tokens' decimals. + require( + customTokenDecimals == underlyingTokenDecimals, + NonMatchingTokenDecimals(customTokenDecimals, underlyingTokenDecimals) + ); + + $.customToken = CustomToken(customToken_); + } + // -----================= ::: UNDERLYING TOKEN ::: =================----- /// @notice Transfers the underlying token from an external account to itself. @@ -502,7 +642,7 @@ abstract contract NativeConverter is // -----================= ::: ADMIN ::: =================----- /// @notice Prevents usage of functions with the `whenNotPaused` modifier. - /// @notice This function can be called by the pauser only. + /// @notice This function can be called by a pauser only. function pause() external onlyRole(PAUSER_ROLE) nonReentrant { _pause(); } @@ -512,4 +652,12 @@ abstract contract NativeConverter is function unpause() external onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { _unpause(); } + + // -----================= ::: DEVELOPER ::: =================----- + + // @remind Document (the entire function). + function _mintCustomToken(address account, uint256 value) internal virtual; + + // @remind Document (the entire function). + function _burnCustomToken(address account, uint256 value) internal virtual; } diff --git a/src/secondary-chain/agglayer/CustomTokenAgglayer.sol b/src/secondary-chain/agglayer/CustomTokenAgglayer.sol new file mode 100644 index 00000000..90fb58e3 --- /dev/null +++ b/src/secondary-chain/agglayer/CustomTokenAgglayer.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/CustomTokenAgglayer.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomToken} from "../CustomToken.sol"; + +// External contracts. +import {NativeConverter} from "../NativeConverter.sol"; + +// @remind Document. +abstract contract CustomTokenAgglayer is CustomToken { + // -----================= ::: MODIFIERS ::: =================----- + + /// @dev Checks if the sender is Agglayer Bridge or Native Converter. + /// @dev This modifier is used to restrict minting and burning of Custom Token. + modifier onlyAgglayerBridgeAndNativeConverter() { + // Only Agglayer Bridge and Native Converter can mint and burn Custom Token. + require(msg.sender == bridge() || msg.sender == nativeConverter(), Unauthorized()); + _; + } + + // -----================= ::: CUSTOM TOKEN ::: =================----- + + // @remind Redocument (the entire function). + /// @notice Mints Custom Tokens to the recipient. + /// @notice This function can be called by Agglayer Bridge and Native Converter only. + /// @param account @note CAUTION! Minting to `address(0)` will result in no tokens minted! This is to enable vbToken on Primary Chain to bridge tokens to address zero on Secondary Chain at the end of the process of migrating backing from Native Converter to Primary Chain. Please refer to `NativeConverter.sol` for more information. + function mint(address account, uint256 value) + external + whenNotPaused + onlyAgglayerBridgeAndNativeConverter + nonReentrant + { + if (account == address(0)) { + NativeConverter(nativeConverter()).removeMigrationInProgress(value); + + emit Transfer(address(0), address(0), value); + + return; + } + + // Mint. + _mint(account, value); + } + + /// @notice Burns Custom Tokens from a holder. + /// @notice This function can be called by Agglayer Bridge and Native Converter only. + function burn(address account, uint256 value) + external + whenNotPaused + onlyAgglayerBridgeAndNativeConverter + nonReentrant + { + _burn(account, value); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_IS_MINTABLE_BURNABLE() internal override {} +} diff --git a/src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol b/src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol new file mode 100644 index 00000000..6b529933 --- /dev/null +++ b/src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/GenericCustomTokenAgglayer.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomTokenAgglayer} from "./CustomTokenAgglayer.sol"; +import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {CustomToken} from "../CustomToken.sol"; + +/// @title Generic Custom Token (Agglayer) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract can be used to deploy Custom Tokens that do not require any customization. +contract GenericCustomTokenAgglayer is CustomTokenAgglayer { + // -----================= ::: SETUP ::: =================----- + + constructor() { + _disableInitializers(); + } + + function reinitialize1() external {} + + /// @notice The reinitializers start from `2` because Agglayer Bridge has already initialized the token. + /// @dev @note (ATTENTION) There is no `reinitializer1`. + function reinitialize2( + address owner_, + uint8 originalUnderlyingTokenDecimals_, + address agglayerBridge_, + address nativeConverter_ + ) external reinitializer(2) nonReentrant { + // Preserve the `name` and `symbol` of the bridged vbToken. + string memory name_ = ERC20Upgradeable.name(); + string memory symbol_ = ERC20Upgradeable.symbol(); + + // Prevent a mistake while initializing. + assert(ERC20Upgradeable.decimals() == originalUnderlyingTokenDecimals_); + + // Initialize the base implementation. + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, agglayerBridge_, nativeConverter_); + } + + // @remind Document (the entire function). + function reinitialize3() external reinitializer(3) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + _incrementGlobalInitializationCounter(3); + + __CustomToken_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize4() + external + reinitializer(_incrementGlobalInitializationCounter(4)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](3); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + reinitializeSelectors[2] = this.reinitialize3.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} +} diff --git a/src/secondary-chain/agglayer/GenericNativeConverterAgglayer.sol b/src/secondary-chain/agglayer/GenericNativeConverterAgglayer.sol new file mode 100644 index 00000000..fa4dd0ee --- /dev/null +++ b/src/secondary-chain/agglayer/GenericNativeConverterAgglayer.sol @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/GenericNativeConverterAgglayer.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {NativeConverterAgglayer} from "./NativeConverterAgglayer.sol"; +import {NativeConverter} from "../NativeConverter.sol"; + +/// @title Generic Native Converter (Agglayer) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract can be used to deploy Native Converters that do not require any customization. +contract GenericNativeConverterAgglayer is NativeConverterAgglayer { + // -----================= ::: SETUP ::: =================----- + + constructor() { + _disableInitializers(); + } + + // @remind Document. + function reinitialize1( + address owner_, + address customToken_, + address underlyingToken_, + address agglayerBridge_, + uint32 primaryChainAgglayerId_, + uint256 nonMigratableBackingPercentage_, + address migrationManager_ + ) external reinitializer(1) nonReentrant { + // Initialize the base implementation. + __NativeConverter_init1( + owner_, + customToken_, + underlyingToken_, + agglayerBridge_, + primaryChainAgglayerId_, + nonMigratableBackingPercentage_, + migrationManager_ + ); + } + + // @remind Document (the entire function). + function reinitialize2() external reinitializer(2) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __NativeConverter_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize3() + external + reinitializer(_incrementGlobalInitializationCounter(3)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](2); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc NativeConverter + function _NATIVE_CONVERTER_INIT_2_COMPATIBLE() internal pure override {} +} diff --git a/src/secondary-chain/agglayer/NativeConverterAgglayer.sol b/src/secondary-chain/agglayer/NativeConverterAgglayer.sol new file mode 100644 index 00000000..4cc42087 --- /dev/null +++ b/src/secondary-chain/agglayer/NativeConverterAgglayer.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/NativeConverterAgglayer.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {NativeConverter} from "../NativeConverter.sol"; +import {GenericCustomTokenAgglayer} from "./GenericCustomTokenAgglayer.sol"; + +// @remind Document. +abstract contract NativeConverterAgglayer is NativeConverter { + // -----================= ::: DEVELOPER ::: =================----- + + // @remind Document (the entire function). + function _mintCustomToken(address account, uint256 value) internal virtual override { + GenericCustomTokenAgglayer(address(customToken())).mint(account, value); + } + + // @remind Document (the entire function). + function _burnCustomToken(address account, uint256 value) internal virtual override { + GenericCustomTokenAgglayer(address(customToken())).burn(account, value); + } +} diff --git a/src/secondary-chain/agglayer/README.md b/src/secondary-chain/agglayer/README.md new file mode 100644 index 00000000..13ed02e5 --- /dev/null +++ b/src/secondary-chain/agglayer/README.md @@ -0,0 +1,51 @@ +# Agglayer + +## Technology + +- [Agglayer](https://www.agglayer.dev/) + +## Compatibility + +### Agglayer Sovereign with Pessimistic Proof + +- Custom Token: Available (Upgradeable Wrapped Token) +- Native Converter: Available +- Bridged USDC Standard: Available +- Wrapped Token: Available (Upgradeable Wrapped Token) + +### Agglayer without Pessimistic Proof + +- Custom Token: N/A +- Native Converter: N/A +- Bridged USDC Standard: N/A +- Wrapped Token: Available (Wrapped Token) + +## Process + +### Agglayer Sovereign with Pessimistic Proof + +1. Determine whether Custom Token, Native Converter, and/or Bridged USDC Standard* are needed. If none is needed, no action is required. *For Bridged USDC Standard, please refer to [`README.md`](./vbUSDC/bridged-usdc-standard/README.md). +2. Bridge underlying token from Primary Chain and claim it on Secondary Chain. +3. Bridge vbToken from Primary Chain and claim it on Secondary Chain, so that Agglayer creates Upgradeable Wrapped Token. +4. Transfer ownership over Upgradeable Wrapped Token from Agglayer Bridge Manager to account you control. +5. Deploy Native Converter implementation and proxy, and initialize it. +6. Deploy Custom Token implementation, upgrade Upgradeable Wrapped Token to Custom Token, and initialize it. +7. Configure Native Converter in Migration Manager on Primary Chain. + +### Agglayer without Pessimistic Proof + +No action required. + +## Protection + +### Agglayer Sovereign with Pessimistic Proof + +- "Local balance tree" in Agglayer Bridge contract prevents bridging out more tokens from a chain that have been bridged in to the chain by reverting onchain immediately. + +### Agglayer without Pessimistic Proof + +- Tokens are not upgreadeable. + +## Reference + +- GitHub: [agglayer/agglayer-contracts](https://github.com/agglayer/agglayer-contracts) \ No newline at end of file diff --git a/src/secondary-chain/agglayer/vbETH/WethAgglayer.sol b/src/secondary-chain/agglayer/vbETH/WethAgglayer.sol new file mode 100644 index 00000000..3c53a6ee --- /dev/null +++ b/src/secondary-chain/agglayer/vbETH/WethAgglayer.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/vbETH/WethAgglayer.sol) + +pragma solidity 0.8.29; + +// @remind Document the entire file. + +import {CustomTokenAgglayer} from "../CustomTokenAgglayer.sol"; +import {CustomTokenWethExtension} from "../../CustomTokenWethExtension.sol"; +import {CustomToken} from "../../CustomToken.sol"; +import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {IAgglayerBridge} from "../../../etc/IAgglayerBridge.sol"; + +/// @title WETH (Agglayer) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev based on https://github.com/gnosis/canonical-weth/blob/master/contracts/WETH9.sol +contract WethAgglayer is CustomTokenAgglayer, CustomTokenWethExtension { + constructor() { + _disableInitializers(); + } + + // @remind Document. + function reinitialize1() external {} + + /// @notice The reinitializers start from `2` because Agglayer Bridge has already initialized the token. + /// @dev @note (ATTENTION) There is no `reinitializer1`. + /// @dev @note (ATTENTION) This reinitializer used to set `_gasTokenIsEth`, but that has been moved to `reinitialize3`. + function reinitialize2( + address owner_, + uint8 originalUnderlyingTokenDecimals_, + address agglayerBridge_, + address nativeConverter_ + ) external reinitializer(2) nonReentrant { + // Preserve the `name` and `symbol` of the bridged vbToken. + string memory name_ = ERC20Upgradeable.name(); + string memory symbol_ = ERC20Upgradeable.symbol(); + + // Prevent a mistake while initializing. + assert(ERC20Upgradeable.decimals() == originalUnderlyingTokenDecimals_); + + // Initialize the base implementation. + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, agglayerBridge_, nativeConverter_); + } + + function reinitialize3(bool wethFunctionalityEnabled_) external reinitializer(3) nonReentrant { + // Clean up the old ERC-7201 namespace where `bool _gasTokenIsEth` used to be stored. + // Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.WETH.storage")) - 1)) & ~bytes32(uint256(0xff))`. + assembly { + sstore(0xdf8caff5d0161908572492829df972cd19b1aabe3c3078d95299408cd561dc00, 0) + } + + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + _incrementGlobalInitializationCounter(3); + + __CustomToken_init2(); + + bool gasTokenIsEth_ = IAgglayerBridge(bridge()).gasTokenAddress() == address(0) + && IAgglayerBridge(bridge()).gasTokenNetwork() == 0; + + __CustomTokenWethExtension_init2_ext1(gasTokenIsEth_, wethFunctionalityEnabled_); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize4() + external + reinitializer(_incrementGlobalInitializationCounter(4)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](3); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + reinitializeSelectors[2] = this.reinitialize3.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} + + /// @inheritdoc CustomTokenWethExtension + function _CUSTOM_TOKEN_WETH_EXTENSION_INIT_2_EXT_1_COMPATIBLE() internal pure override {} +} diff --git a/src/custom-tokens/WETH/WETHNativeConverter.sol b/src/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol similarity index 51% rename from src/custom-tokens/WETH/WETHNativeConverter.sol rename to src/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol index ca2410ad..3a612bac 100644 --- a/src/custom-tokens/WETH/WETHNativeConverter.sol +++ b/src/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol @@ -1,37 +1,41 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol) + pragma solidity 0.8.29; +// @remind Document the entire file. + +import {NativeConverterAgglayer} from "../NativeConverterAgglayer.sol"; import {NativeConverter, Math} from "../../NativeConverter.sol"; -import {WETH} from "./WETH.sol"; -import {IVersioned} from "../../etc/IVersioned.sol"; -import {MigrationManager} from "../../MigrationManager.sol"; -import {ILxLyBridge} from "../../etc/ILxLyBridge.sol"; +import {WethAgglayer} from "./WethAgglayer.sol"; +import {MigrationManager} from "../../../primary-chain/MigrationManager.sol"; +import {IAgglayerBridge} from "../../../etc/IAgglayerBridge.sol"; -/// @title WETH Native Converter +/// @title WETH Native Converter (Agglayer) /// @author See https://github.com/agglayer/vault-bridge -contract WETHNativeConverter is NativeConverter { +contract WethNativeConverterAgglayer is NativeConverterAgglayer { /// @dev Storage of WETHNativeConverter contract. /// @dev It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions when using with upgradeable contracts. /// @custom:storage-location erc7201:agglayer.vault-bridge.WETHNativeConverter.storage struct WETHNativeConverterStorage { - WETH _weth; + WethAgglayer __DEPRECATED___weth; bool _gasTokenIsEth; uint256 nonMigratableGasBackingPercentage; } /// @dev The storage slot at which WETHNativeConverter storage starts, following the EIP-7201 standard. /// @dev Calculated as `keccak256(abi.encode(uint256(keccak256("agglayer.vault-bridge.WETHNativeConverter.storage")) - 1)) & ~bytes32(uint256(0xff))`. - bytes32 private constant _WETH_NATIVE_CONVERTER_STORAGE = + bytes32 private constant _WETH_NATIVE_CONVERTER_AGGLAYER_STORAGE = hex"f9565ea242552c2a1a216404344b0c8f6a3093382a21dd5bd6f5dc2ff1934d00"; - error FunctionNotSupportedOnThisNetwork(); + error FunctionNotSupportedOnThisChain(); error InvalidNonMigratableGasBackingPercentage(); event NonMigratableGasBackingPercentageSet(uint256 nonMigratableGasBackingPercentage_); modifier onlyIfGasTokenIsEth() { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); - require($._gasTokenIsEth, FunctionNotSupportedOnThisNetwork()); + WETHNativeConverterStorage storage $ = _getWethNativeConverterAgglayerStorage(); + require($._gasTokenIsEth, FunctionNotSupportedOnThisChain()); _; } @@ -39,77 +43,102 @@ contract WETHNativeConverter is NativeConverter { _disableInitializers(); } - function initialize( + function reinitialize1( address owner_, - uint8 originalUnderlyingTokenDecimals_, address customToken_, address underlyingToken_, - address lxlyBridge_, - uint32 layerXNetworkId_, + address agglayerBridge_, + uint32 primaryChainNetworkId_, uint256 nonMigratableBackingPercentage_, address migrationManager_, uint256 nonMigratableGasBackingPercentage_ - ) external initializer { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); + ) external reinitializer(1) { + WETHNativeConverterStorage storage $ = _getWethNativeConverterAgglayerStorage(); // Initialize the base implementation. - __NativeConverter_init( + __NativeConverter_init1( owner_, - originalUnderlyingTokenDecimals_, customToken_, underlyingToken_, - lxlyBridge_, - layerXNetworkId_, + agglayerBridge_, + primaryChainNetworkId_, nonMigratableBackingPercentage_, migrationManager_ ); require(nonMigratableGasBackingPercentage_ <= 1e18, InvalidNonMigratableBackingPercentage()); - $._weth = WETH(payable(customToken_)); - $._gasTokenIsEth = - ILxLyBridge(lxlyBridge_).gasTokenAddress() == address(0) && ILxLyBridge(lxlyBridge_).gasTokenNetwork() == 0; + $._gasTokenIsEth = IAgglayerBridge(agglayerBridge_).gasTokenAddress() == address(0) + && IAgglayerBridge(agglayerBridge_).gasTokenNetwork() == 0; $.nonMigratableGasBackingPercentage = nonMigratableGasBackingPercentage_; } + function reinitialize2() external nonReentrant reinitializer(2) { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __NativeConverter_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize3() + external + reinitializer(_incrementGlobalInitializationCounter(3)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](2); + + reinitializeSelectors[0] = this.reinitialize1.selector; + reinitializeSelectors[1] = this.reinitialize2.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc NativeConverter + function _NATIVE_CONVERTER_INIT_2_COMPATIBLE() internal pure override {} + function nonMigratableGasBackingPercentage() public view returns (uint256) { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); + WETHNativeConverterStorage storage $ = _getWethNativeConverterAgglayerStorage(); return $.nonMigratableGasBackingPercentage; } - function _getWETHNativeConverterStorage() private pure returns (WETHNativeConverterStorage storage $) { + function _getWethNativeConverterAgglayerStorage() private pure returns (WETHNativeConverterStorage storage $) { assembly { - $.slot := _WETH_NATIVE_CONVERTER_STORAGE + $.slot := _WETH_NATIVE_CONVERTER_AGGLAYER_STORAGE } } function migratableGasBacking() public view returns (uint256) { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); + WETHNativeConverterStorage storage $ = _getWethNativeConverterAgglayerStorage(); uint256 nonMigratableGasBacking = _convertToAssets(Math.mulDiv(customToken().totalSupply(), $.nonMigratableGasBackingPercentage, 1e18)); - uint256 gasBalance = address(customToken()).balance; + uint256 gasBacking = WethAgglayer(address(customToken())).gasBackingOnSecondaryChain(); - return gasBalance > nonMigratableGasBacking ? gasBalance - nonMigratableGasBacking : 0; + return gasBacking > nonMigratableGasBacking ? gasBacking - nonMigratableGasBacking : 0; } /// @dev This special function allows the NativeConverter owner to migrate the gas backing of the WETH Custom Token /// @dev It simply takes the amount of gas token from the WETH contract - /// @dev and performs the migration using a special CrossNetworkInstruction called WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION - /// @dev It instructs vbETH on Layer X to first wrap the gas token and then deposit it to complete the migration. + /// @dev and performs the migration using a special CrossChainInstruction called _1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION + /// @dev It instructs vbETH on Primary Chain to first wrap the gas token and then deposit it to complete the migration. /// @notice It is known that this can lead to WETH not being able to perform withdrawals, because of a lack of gas backing. /// @notice However, this is acceptable, because WETH is a vault backed token so its backing should actually be staked. - /// @notice Users can still bridge WETH back to Layer X to receive wETH or ETH. - function migrateGasBackingToLayerX(uint256 amount) + /// @notice Users can still bridge WETH back to Primary Chain to receive wETH or ETH. + function migrateGasBackingToPrimaryChain(uint256 amount) external whenNotPaused onlyIfGasTokenIsEth onlyRole(MIGRATOR_ROLE) nonReentrant { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); - WETH weth = $._weth; + WethAgglayer weth = WethAgglayer(address(customToken())); uint256 migratableGasBacking_ = migratableGasBacking(); @@ -117,22 +146,24 @@ contract WETHNativeConverter is NativeConverter { require(amount > 0, InvalidAssets()); require(amount <= migratableGasBacking_, AssetsTooLarge(migratableGasBacking_, amount)); + _addMigrationInProgress(amount); + // Precalculate the amount of Custom Token for which backing is being migrated. uint256 amountOfCustomToken = _convertToShares(amount); - // Taking lxlyBridge's gas balance here - weth.bridgeBackingToLayerX(amount); - lxlyBridge().bridgeAsset{value: amount}( - layerXLxlyId(), address(migrationManager()), amount, address(0), true, "" + // Taking agglayerBridge's gas balance here + weth.bridgeBackingToPrimaryChain(amount); + bridge().bridgeAsset{value: amount}( + primaryChainAgglayerId(), address(migrationManager()), amount, address(0), true, "" ); - // Bridge a message to Migration Manager on Layer X to complete the migration. - lxlyBridge().bridgeMessage( - layerXLxlyId(), + // Bridge a message to Migration Manager on Primary Chain to complete the migration. + bridge().bridgeMessage( + primaryChainAgglayerId(), address(migrationManager()), true, abi.encode( - MigrationManager.CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, + MigrationManager.CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, abi.encode(amountOfCustomToken, amount) ) ); @@ -148,7 +179,7 @@ contract WETHNativeConverter is NativeConverter { onlyRole(DEFAULT_ADMIN_ROLE) nonReentrant { - WETHNativeConverterStorage storage $ = _getWETHNativeConverterStorage(); + WETHNativeConverterStorage storage $ = _getWethNativeConverterAgglayerStorage(); // Check the input. require(nonMigratableGasBackingPercentage_ <= 1e18, InvalidNonMigratableGasBackingPercentage()); @@ -159,9 +190,4 @@ contract WETHNativeConverter is NativeConverter { // Emit the event. emit NonMigratableGasBackingPercentageSet(nonMigratableGasBackingPercentage_); } - - /// @inheritdoc IVersioned - function version() external pure virtual returns (string memory) { - return "0.5.0"; - } } diff --git a/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/README.md b/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/README.md new file mode 100644 index 00000000..6b15f335 --- /dev/null +++ b/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/README.md @@ -0,0 +1,18 @@ +# Bridged USDC Standard + +> [!IMPORTANT] +> `migrateBackingToPrimaryChain` in Native Converter is not supported yet. + +// @remind Improve wording. + +Based on `circlefin/stablecoin-evm` commit [`c8c31b2`](https://github.com/circlefin/stablecoin-evm/tree/c8c31b249341bf3ffb2e8dbff41977c392a260c5). + +1. Assume someone frontruns us with vbUSDC bridging+claiming on Secondary Chain. Bridge-wrapped vbUSDC gets created and initialized on the destination network. +2. We deploy FiatTokenV2_2 (proxy+implementation) on that network. +3. We custom-map FiatTokenV2_2 proxy to vbUSDC. Any vbUSDC claims from then on will result in FiatTokenV2_2 being minted directly by the bridge. +4. Whoever frontrun us will still be able to transfer or bridge back their bridge-wrapped vbUSDC and get vbUSDC on Ethereum. (Or use the migrate function on Agglayer Bridge). +5. Native Converter converts bridge-wrapped USDC to FiatTokenV2_2. +6. When the network wants Circle to take over, we double the totalSupply of FiatTokenV2_2, bridge a half to Ethereum, and unmap FiatTokenV2_2 from vbUSDC. +7. We upgrade bridge-wrapped vbUSDC to vbUSDC Custom Token. +8. We reinitialize Native Converter to convert bridge-wrapped USDC to vbUSDC Custom Token. +9. Circle burns USDC on Ethreum, takes over FiatTokenV2_2 on Secondary Chain. \ No newline at end of file diff --git a/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandard.sol b/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandard.sol new file mode 100644 index 00000000..34931c83 --- /dev/null +++ b/src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandard.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol) + +pragma solidity 0.8.29; + +import {GenericNativeConverterAgglayer} from "../../GenericNativeConverterAgglayer.sol"; +import {NativeConverterAgglayer} from "../../GenericNativeConverterAgglayer.sol"; +import {IFiatTokenV2_2} from "../../../../etc/IFiatTokenV2_2.sol"; + +// Libraries. +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +// @remind Improve documentation. +/// @title vbUSDC Native Converter (Agglayer + Bridged USDC Standard) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev @note CAUTION! `nonMigratableBackingPercentage` must be set to `1e18` (100%) because `migrateBackingToPrimaryChain` is not supported yet. +/// @dev This contract can be upgraded to `GenericNativeConverterAgglayer` after Circle takes over the token. +contract VbUsdcNativeConverterAgglayerBridgedUsdcStandard is GenericNativeConverterAgglayer { + // Libraries. + using SafeERC20 for IFiatTokenV2_2; + + // -----================= ::: DEVELOPER ::: =================----- + + // @remind Document (the entire function). + /// @inheritdoc NativeConverterAgglayer + function _burnCustomToken(address account, uint256 value) internal virtual override { + IFiatTokenV2_2 vbUsdc = IFiatTokenV2_2(address(customToken())); + + vbUsdc.safeTransferFrom(account, address(this), value); + + vbUsdc.burn(value); + } +} diff --git a/src/secondary-chain/agglayer/vbUSDC/generic/VbUsdcAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDC/generic/VbUsdcAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbUSDC/generic/VbUsdcNativeConverterAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDC/generic/VbUsdcNativeConverterAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbUSDS/VbUsdsAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDS/VbUsdsAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbUSDS/VbUsdsNativeConverterAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDS/VbUsdsNativeConverterAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbUSDT/VbUsdtAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDT/VbUsdtAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbUSDT/VbUsdtNativeConverterAgglayer.sol.generic b/src/secondary-chain/agglayer/vbUSDT/VbUsdtNativeConverterAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbWBTC/VbWbtcAgglayer.sol.generic b/src/secondary-chain/agglayer/vbWBTC/VbWbtcAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/agglayer/vbWBTC/VbWbtcNativeConverterAgglayer.sol.generic b/src/secondary-chain/agglayer/vbWBTC/VbWbtcNativeConverterAgglayer.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/polygon/CustomTokenPolygon.sol b/src/secondary-chain/polygon/CustomTokenPolygon.sol new file mode 100644 index 00000000..77a11ab8 --- /dev/null +++ b/src/secondary-chain/polygon/CustomTokenPolygon.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/polygon/CustomTokenPolygon.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomToken} from "../CustomToken.sol"; + +// @remind Document. +abstract contract CustomTokenPolygon is CustomToken { + // -----================= ::: MODIFIERS ::: =================----- + + /// @dev Checks if the sender is Child Chain Manager. + /// @dev This modifier is used to restrict minting of Custom Token. + modifier onlyChildChainManager() { + // Only Child Chain Manager can mint Custom Token. + require(msg.sender == bridge(), Unauthorized()); + _; + } + + // -----================= ::: CUSTOM TOKEN ::: =================----- + + // @remind Document (the entire function). + function deposit(address account, bytes calldata data) external whenNotPaused onlyChildChainManager nonReentrant { + uint256 value = abi.decode(data, (uint256)); + _mint(account, value); + } + + // @remind Document (the entire function). + function withdraw(uint256 value) external whenNotPaused nonReentrant { + _burn(msg.sender, value); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_IS_MINTABLE_BURNABLE() internal override {} +} diff --git a/src/secondary-chain/polygon/GenericCustomTokenPolygon.sol b/src/secondary-chain/polygon/GenericCustomTokenPolygon.sol new file mode 100644 index 00000000..dc53ef31 --- /dev/null +++ b/src/secondary-chain/polygon/GenericCustomTokenPolygon.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/polygon/GenericCustomTokenPolygon.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomTokenPolygon} from "./CustomTokenPolygon.sol"; +import {CustomToken} from "../CustomToken.sol"; + +// @remind Update documentation. +/// @title Generic Custom Token (Polygon) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract can be used to deploy Custom Tokens that do not require any customization. +contract GenericCustomTokenPolygon is CustomTokenPolygon { + // -----================= ::: SETUP ::: =================----- + + constructor() { + _disableInitializers(); + } + + // @remind Document (the entire function). + function reinitialize1( + address owner_, + string memory name_, + string memory symbol_, + uint8 originalUnderlyingTokenDecimals_, + address childChainManager_ + ) external reinitializer(_incrementGlobalInitializationCounter(1)) nonReentrant { + // Initialize the base implementation. + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, childChainManager_, address(0)); + + __CustomToken_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize2() + external + reinitializer(_incrementGlobalInitializationCounter(2)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](1); + + reinitializeSelectors[0] = this.reinitialize1.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} +} diff --git a/src/secondary-chain/polygon/README.md b/src/secondary-chain/polygon/README.md new file mode 100644 index 00000000..ef82df6c --- /dev/null +++ b/src/secondary-chain/polygon/README.md @@ -0,0 +1,25 @@ +# Polygon + +## Technology + +- [PoS Portal](https://polygon.technology/polygon-pos) + +## Compatibility + +- Custom Token: Available (Custom Mapping) +- ~~Native Converter~~ +- ~~Bridged USDC Standard~~ +- Wrapped Token: N/A + +## Process + +1. Deploy Custom Token implementation and proxy on Secondary Chain, and initialize it. +2. Custom map vbToken to Custom Token on PoS Portal. + +## Protection + +- Bidirectional bridge. + +## Reference + +- GitHub: [UChildERC20.sol#L1520-L1544](https://github.com/maticnetwork/pos-portal/blob/5ff7bab80182d1ebeaea3d5a3648eea96e5431e0/flat/UChildERC20.sol#L1520-L1544) \ No newline at end of file diff --git a/src/secondary-chain/polygon/vbETH/VbEthPolygon.sol.generic b/src/secondary-chain/polygon/vbETH/VbEthPolygon.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/polygon/vbUSDC/VbUsdcPolygon.sol.generic b/src/secondary-chain/polygon/vbUSDC/VbUsdcPolygon.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/polygon/vbUSDS/VbUsdsPolygon.sol.generic b/src/secondary-chain/polygon/vbUSDS/VbUsdsPolygon.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/polygon/vbUSDT/VbUsdtPolygon.sol.generic b/src/secondary-chain/polygon/vbUSDT/VbUsdtPolygon.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/polygon/vbWBTC/VbWbtcPolygon.sol.generic b/src/secondary-chain/polygon/vbWBTC/VbWbtcPolygon.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/wormhole/CustomTokenWormhole.sol b/src/secondary-chain/wormhole/CustomTokenWormhole.sol new file mode 100644 index 00000000..fd3bb40c --- /dev/null +++ b/src/secondary-chain/wormhole/CustomTokenWormhole.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/wormhole/CustomTokenWormhole.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomToken} from "../CustomToken.sol"; + +// @remind Document. +abstract contract CustomTokenWormhole is CustomToken { + // -----================= ::: MODIFIERS ::: =================----- + + /// @dev Checks if the sender is NTT Manager. + /// @dev This modifier is used to restrict minting and burning of Custom Token. + modifier onlyNttManager() { + // Only NTT Manager can mint and burn Custom Token. + require(msg.sender == bridge(), Unauthorized()); + _; + } + + // -----================= ::: CUSTOM TOKEN ::: =================----- + + // @remind Document (the entire function). + function mint(address account, uint256 value) external whenNotPaused onlyNttManager nonReentrant { + _mint(account, value); + } + + // @remind Document (the entire function). + function burn(uint256 value) external whenNotPaused onlyNttManager nonReentrant { + _burn(msg.sender, value); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_IS_MINTABLE_BURNABLE() internal override {} +} diff --git a/src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol b/src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol new file mode 100644 index 00000000..6b1d5525 --- /dev/null +++ b/src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/wormhole/GenericCustomTokenWormhole.sol) + +pragma solidity 0.8.29; + +// Main functionality. +import {CustomTokenWormhole} from "./CustomTokenWormhole.sol"; +import {CustomToken} from "../CustomToken.sol"; + +/// @title Generic Custom Token (Wormhole) +/// @author See https://github.com/agglayer/vault-bridge +/// @dev This contract can be used to deploy Custom Tokens that do not require any customization. +contract GenericCustomTokenWormhole is CustomTokenWormhole { + // -----================= ::: SETUP ::: =================----- + + constructor() { + _disableInitializers(); + } + + // @remind Document (the entire function). + function reinitialize1( + address owner_, + string memory name_, + string memory symbol_, + uint8 originalUnderlyingTokenDecimals_, + address nttManager_ + ) external reinitializer(_incrementGlobalInitializationCounter(1)) nonReentrant { + // Initialize the base implementation. + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, nttManager_, address(0)); + + __CustomToken_init2(); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize2() + external + reinitializer(_incrementGlobalInitializationCounter(2)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](1); + + reinitializeSelectors[0] = this.reinitialize1.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} +} diff --git a/src/secondary-chain/wormhole/README.md b/src/secondary-chain/wormhole/README.md new file mode 100644 index 00000000..a9545fb0 --- /dev/null +++ b/src/secondary-chain/wormhole/README.md @@ -0,0 +1,55 @@ +# Wormhole + +## Technology + +- [Native Token Transfers](https://wormhole.com/products/native-token-transfers) +- [Wrapped Token Transfers](https://wormhole.com/docs/products/token-transfers/wrapped-token-transfers/overview/) + +## Compatibility + +### Native Token Transfers + +- Custom Token: Available (NTT Token) +- Native Converter: TBD +- Bridged USDC Standard: TBD +- Wrapped Token: N/A + +### Wrapped Token Transfers + +- Custom Token: N/A +- Native Converter: N/A +- Bridged USDC Standard: N/A +- Wrapped Token: Avaialble (WTT Token) + +## Process + +### Native Token Transfers + +1. Deploy Custom Token implementation and proxy to Secondary Chain, and initialize it. You will need to set `nttManager` to Reverting Contract. Please refer to [`DeployRevertingContract.s.sol`](../../../script/etc/DeployRevertingContract.s.sol) for more information. +2. Deploy NTT Manager and Wormhole Transceiver implementation and proxy on Secondary Chain using NTT CLI. +3. Execute `setBridge(address)` on Custom Token with address of NTT Manager. +4. Update peers of NTT Manager on Primary Chain and all Wormhole Secondary Chains using NTT CLI. +5. BEFORE ANY TRANSFERS: Enable NTT Global Accountant for Wormhole Transceiver on Secondary Chain. @remind: Document the process. + +### Wrapped Token Transfers + +No action required. + + +## Protection + +### Native Token Transfers + +- NTT Global Accountant prevents bridging out more tokens from a chain that have been bridged in to the chain by blocking the transfer offchain later. This can result in irreversible loss of tokens. + +### Wrapped Token Transfers + +- Tokens are upgreadeable; controlled by Guardians. + +## Reference + +- Wormhole Docs: [Native Token Transfers](https://wormhole.com/docs/products/token-transfers/native-token-transfers/overview/) +- GitHub: [wormhole-foundation/example-ntt-token-evm](https://github.com/wormhole-foundation/example-ntt-token-evm) +- GitHub: [wormhole-foundation/native-token-transfers](https://github.com/wormhole-foundation/native-token-transfers) +- Wormhole Docs: [Wrapped Token Transfers](https://wormhole.com/docs/products/token-transfers/wrapped-token-transfers/overview/) +- GitHub: [Native Token Transfer - Global Accountant # Caveats](https://github.com/wormhole-foundation/wormhole/blob/main/cosmwasm/contracts/ntt-global-accountant/README.md#caveats) diff --git a/src/secondary-chain/wormhole/vbETH/WethWormhole.sol b/src/secondary-chain/wormhole/vbETH/WethWormhole.sol new file mode 100644 index 00000000..e4ae1313 --- /dev/null +++ b/src/secondary-chain/wormhole/vbETH/WethWormhole.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +// Vault Bridge (last updated v1.0.0) (secondary-chain/agglayer/vbETH/WethWormhole.sol) + +pragma solidity 0.8.29; + +// @remind Document the entire file. + +import {CustomTokenWormhole} from "../CustomTokenWormhole.sol"; +import {CustomTokenWethExtension} from "../../CustomTokenWethExtension.sol"; +import {CustomToken} from "../../CustomToken.sol"; +import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; + +/// @title WETH (Wormhole) +/// @author See https://github.com/agglayer/vault-bridge +contract WethWormhole is CustomTokenWormhole, CustomTokenWethExtension { + constructor() { + _disableInitializers(); + } + + function reinitialize1( + address owner_, + string memory name_, + string memory symbol_, + uint8 originalUnderlyingTokenDecimals_, + address nttManager_, + bool gasTokenIsEth_, + bool wethFunctionalityEnabled_ + ) external reinitializer(_incrementGlobalInitializationCounter(1)) nonReentrant { + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, nttManager_, address(0)); + + __CustomToken_init2(); + + __CustomTokenWethExtension_init2_ext1(gasTokenIsEth_, wethFunctionalityEnabled_); + } + + /* + /// @dev How to add a new reinitializer: + function reinitialize2() + external + reinitializer(_incrementGlobalInitializationCounter(2)) + nonReentrant + {} + */ + + // @remind Document (the entire function). + function reinitialize(bytes[] calldata reinitializeData) external { + bytes4[] memory reinitializeSelectors = new bytes4[](1); + + reinitializeSelectors[0] = this.reinitialize1.selector; + + _reinitialize(reinitializeSelectors, reinitializeData); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} + + /// @inheritdoc CustomTokenWethExtension + function _CUSTOM_TOKEN_WETH_EXTENSION_INIT_2_EXT_1_COMPATIBLE() internal pure override {} +} diff --git a/src/secondary-chain/wormhole/vbUSDC/VbUsdcWormhole.sol.generic b/src/secondary-chain/wormhole/vbUSDC/VbUsdcWormhole.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/wormhole/vbUSDS/VbUsdsWormhole.sol.generic b/src/secondary-chain/wormhole/vbUSDS/VbUsdsWormhole.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/wormhole/vbUSDT/VbUsdtWormhole.sol.generic b/src/secondary-chain/wormhole/vbUSDT/VbUsdtWormhole.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/secondary-chain/wormhole/vbWBTC/VbWbtcWormhole.sol.generic b/src/secondary-chain/wormhole/vbWBTC/VbWbtcWormhole.sol.generic new file mode 100644 index 00000000..e69de29b diff --git a/src/vault-bridge-tokens/README.md b/src/vault-bridge-tokens/README.md deleted file mode 100644 index 14dabb36..00000000 --- a/src/vault-bridge-tokens/README.md +++ /dev/null @@ -1 +0,0 @@ -

LAYER X

\ No newline at end of file diff --git a/src/vault-bridge-tokens/vbUSDC/VbUSDC.sol.generic b/src/vault-bridge-tokens/vbUSDC/VbUSDC.sol.generic deleted file mode 100644 index c9cdd108..00000000 --- a/src/vault-bridge-tokens/vbUSDC/VbUSDC.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericVbToken} from "../GenericVbToken.sol"; - -/// @title Vault Bridge USDC -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDC` proxy to `GenericVbToken` instead. -contract VbUSDC is GenericVbToken {} diff --git a/src/vault-bridge-tokens/vbUSDS/VbUSDS.sol.generic b/src/vault-bridge-tokens/vbUSDS/VbUSDS.sol.generic deleted file mode 100644 index b07fc6a3..00000000 --- a/src/vault-bridge-tokens/vbUSDS/VbUSDS.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericVbToken} from "../GenericVbToken.sol"; - -/// @title Vault Bridge USDS -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDS` proxy to `GenericVbToken` instead. -contract VbUSDS is GenericVbToken {} diff --git a/src/vault-bridge-tokens/vbUSDT/VbUSDT.sol.generic b/src/vault-bridge-tokens/vbUSDT/VbUSDT.sol.generic deleted file mode 100644 index 519a0f33..00000000 --- a/src/vault-bridge-tokens/vbUSDT/VbUSDT.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericVbToken} from "../GenericVbToken.sol"; - -/// @title Vault Bridge USDT -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbUSDT` proxy to `GenericVbToken` instead. -contract VbUSDT is GenericVbToken {} diff --git a/src/vault-bridge-tokens/vbWBTC/VbWBTC.sol.generic b/src/vault-bridge-tokens/vbWBTC/VbWBTC.sol.generic deleted file mode 100644 index 226d002b..00000000 --- a/src/vault-bridge-tokens/vbWBTC/VbWBTC.sol.generic +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -// Main functionality. -import {GenericVbToken} from "../GenericVbToken.sol"; - -/// @title Vault Bridge WBTC -/// @author See https://github.com/agglayer/vault-bridge -/// @dev No customization is required. -/// @dev This contract does not need to be deployed. You can point `VbWBTC` proxy to `GenericVbToken` instead. -contract VbWBTC is GenericVbToken {} diff --git a/test/GenericNativeConverter.t.sol b/test/GenericNativeConverter.t.sol deleted file mode 100644 index 76dac573..00000000 --- a/test/GenericNativeConverter.t.sol +++ /dev/null @@ -1,582 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import "forge-std/Test.sol"; -import "src/custom-tokens/GenericNativeConverter.sol"; -import "src/NativeConverter.sol"; -import "src/MigrationManager.sol"; -import "src/custom-tokens/GenericCustomToken.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; -import {IBridgeL2SovereignChain} from "test/interfaces/IBridgeL2SovereignChain.sol"; - -import {IAccessControl} from "@openzeppelin-contracts/access/IAccessControl.sol"; -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -contract MockERC20MintableBurnable is MockERC20 { - function mint(address account, uint256 amount) external { - _mint(account, amount); - } - - function burn(address account, uint256 amount) external { - _burn(account, amount); - } -} - -contract GenericNativeConverterTest is Test { - bytes internal constant SOVEREIGN_BRIDGE_BYTECODE = - hex"6080604052600436106101e4575f3560e01c80638c0dd470116101075780638c0dd470146104ba5780638ed7e3f2146104d95780639e76158f146104f8578063aaa13cc214610517578063b458696214610536578063b8b284d014610555578063bab161bf14610574578063be5831c714610595578063bf130d7f146105b8578063c00f14ab146105d7578063c0f49163146105f6578063cc46163214610624578063ccaa2d1114610643578063cd58657914610662578063d02103ca14610675578063dbc1697614610246578063eabd372a1461069b578063ee25560b146106ba578063f5efcd79146106e5578063f811bff714610704578063fb57083414610723575f80fd5b806314cc01a0146101e857806315064c961461021d5780632072f6c51461024657806322e95f2c1461025c578063240ff3781461027b57806327aef4e81461028e5780632dfdf0b5146102af578063318aee3d146102d25780633c351e10146103395780633cbc795b146103585780633e197043146103905780634b2f336d146103af57806357cfbee3146103ce5780635ca1e165146103ed5780637843298b1461040157806379e2cf971461042057806381b1c1741461043457806383c43a551461046857806383f244031461047c5780638781a5c51461049b575b5f80fd5b3480156101f3575f80fd5b5060a354610207906001600160a01b031681565b60405161021491906131df565b60405180910390f35b348015610228575f80fd5b506068546102369060ff1681565b6040519015158152602001610214565b348015610251575f80fd5b5061025a610742565b005b348015610267575f80fd5b5061020761027636600461321d565b61075b565b61025a6102893660046132a3565b6107a9565b348015610299575f80fd5b506102a2610819565b6040516102149190613364565b3480156102ba575f80fd5b506102c460535481565b604051908152602001610214565b3480156102dd575f80fd5b506103156102ec36600461337d565b606b6020525f908152604090205463ffffffff811690600160201b90046001600160a01b031682565b6040805163ffffffff90931683526001600160a01b03909116602083015201610214565b348015610344575f80fd5b50606d54610207906001600160a01b031681565b348015610363575f80fd5b50606d5461037b90600160a01b900463ffffffff1681565b60405163ffffffff9091168152602001610214565b34801561039b575f80fd5b506102c46103aa3660046133a6565b6108a5565b3480156103ba575f80fd5b50606f54610207906001600160a01b031681565b3480156103d9575f80fd5b5061025a6103e8366004613556565b610931565b3480156103f8575f80fd5b506102c4610a27565b34801561040c575f80fd5b5061020761041b366004613655565b610b03565b34801561042b575f80fd5b5061025a610b2c565b34801561043f575f80fd5b5061020761044e36600461369b565b606a6020525f90815260409020546001600160a01b031681565b348015610473575f80fd5b506102a2610b4f565b348015610487575f80fd5b506102c46104963660046136c3565b610b6e565b3480156104a6575f80fd5b5061025a6104b53660046136ff565b610c43565b3480156104c5575f80fd5b5061025a6104d43660046137d1565b610cee565b3480156104e4575f80fd5b50606c54610207906001600160a01b031681565b348015610503575f80fd5b5061025a61051236600461389a565b610fcf565b348015610522575f80fd5b506102076105313660046138c4565b611107565b348015610541575f80fd5b5061025a61055036600461337d565b611207565b348015610560575f80fd5b5061025a61056f366004613959565b611368565b34801561057f575f80fd5b5060685461037b90610100900463ffffffff1681565b3480156105a0575f80fd5b5060685461037b90600160c81b900463ffffffff1681565b3480156105c3575f80fd5b5061025a6105d23660046139d6565b6113e1565b3480156105e2575f80fd5b506102a26105f136600461337d565b6114ab565b348015610601575f80fd5b5061023661061036600461337d565b60a26020525f908152604090205460ff1681565b34801561062f575f80fd5b5061023661063e366004613a02565b6114f0565b34801561064e575f80fd5b5061025a61065d366004613a33565b611540565b61025a610670366004613b16565b611924565b348015610680575f80fd5b5060685461020790600160281b90046001600160a01b031681565b3480156106a6575f80fd5b5061025a6106b536600461337d565b611cc4565b3480156106c5575f80fd5b506102c46106d436600461369b565b60696020525f908152604090205481565b3480156106f0575f80fd5b5061025a6106ff366004613a33565b611d46565b34801561070f575f80fd5b5061025a61071e366004613ba5565b611f8c565b34801561072e575f80fd5b5061023661073d366004613c34565b612031565b60405163441845b160e01b815260040160405180910390fd5b5f606a5f8484604051602001610772929190613c79565b60408051601f198184030181529181528151602092830120835290820192909252015f20546001600160a01b031690505b92915050565b60685460ff16156107cd57604051630bc011ff60e21b815260040160405180910390fd5b34158015906107e65750606f546001600160a01b031615155b15610804576040516301bd897160e61b815260040160405180910390fd5b610812858534868686612048565b5050505050565b606e805461082690613ca3565b80601f016020809104026020016040519081016040528092919081815260200182805461085290613ca3565b801561089d5780601f106108745761010080835404028352916020019161089d565b820191905f5260205f20905b81548152906001019060200180831161088057829003601f168201915b505050505081565b6040516001600160f81b031960f889901b1660208201526001600160e01b031960e088811b821660218401526001600160601b0319606089811b821660258601529188901b909216603984015285901b16603d82015260518101839052607181018290525f90609101604051602081830303815290604052805190602001209050979650505050505050565b60a3546001600160a01b0316331461095c576040516357b738d160e11b815260040160405180910390fd5b8251845114158061096f57508151845114155b8061097c57508051845114155b1561099a5760405163434f49f560e11b815260040160405180910390fd5b5f5b825181101561081257610a158582815181106109ba576109ba613cdb565b60200260200101518583815181106109d4576109d4613cdb565b60200260200101518584815181106109ee576109ee613cdb565b6020026020010151858581518110610a0857610a08613cdb565b6020026020010151612112565b80610a1f81613d03565b91505061099c565b6053545f90819081805b6020811015610afa578083901c600116600103610a8e5760338160208110610a5b57610a5b613cdb565b01546040805160208101929092528101859052606001604051602081830303815290604052805190602001209350610abb565b60408051602081018690529081018390526060016040516020818303038152906040528051906020012093505b60408051602081018490529081018390526060016040516020818303038152906040528051906020012091508080610af290613d03565b915050610a31565b50919392505050565b5f610b248484610b12856122c2565b610b1b8661237d565b61053187612431565b949350505050565b605354606854600160c81b900463ffffffff161015610b4d57610b4d6124e5565b565b60405180611ba00160405280611b66815260200161443b611b66913981565b5f83815b6020811015610c3a57600163ffffffff8516821c81169003610bdd57848160208110610ba057610ba0613cdb565b602002013582604051602001610bc0929190918252602082015260400190565b604051602081830303815290604052805190602001209150610c28565b81858260208110610bf057610bf0613cdb565b6020020135604051602001610c0f929190918252602082015260400190565b6040516020818303038152906040528051906020012091505b80610c3281613d03565b915050610b72565b50949350505050565b60a3546001600160a01b03163314610c6e576040516357b738d160e11b815260040160405180910390fd5b8051825114610c905760405163434f49f560e11b815260040160405180910390fd5b5f5b8251811015610ce957610cd7838281518110610cb057610cb0613cdb565b6020026020010151838381518110610cca57610cca613cdb565b6020026020010151612579565b80610ce181613d03565b915050610c92565b505050565b5f54610100900460ff1615808015610d0c57505f54600160ff909116105b80610d2c5750610d1b30612627565b158015610d2c57505f5460ff166001145b610d515760405162461bcd60e51b8152600401610d4890613d1b565b60405180910390fd5b5f805460ff191660011790558015610d72575f805461ff0019166101001790555b60688054610100600160c81b03191661010063ffffffff8d1602600160281b600160c81b03191617600160281b6001600160a01b038a81169190910291909117909155606c80546001600160a01b03199081168984161790915560a380549091168683161790558916610e3b5763ffffffff881615610e0457604051630d43a60960e11b815260040160405180910390fd5b6001600160a01b038316151580610e185750815b15610e3657604051630e6e237560e11b815260040160405180910390fd5b610f76565b606d805463ffffffff8a16600160a01b026001600160c01b03199091166001600160a01b038c1617179055606e610e728682613dae565b506001600160a01b038316610f3e57811515600103610ea457604051630e6e237560e11b815260040160405180910390fd5b610f195f801b6012604051602001610f0591906060808252600d908201526c2bb930b83832b21022ba3432b960991b608082015260a060208201819052600490820152630ae8aa8960e31b60c082015260ff91909116604082015260e00190565b604051602081830303815290604052612636565b606f80546001600160a01b0319166001600160a01b0392909216919091179055610f76565b606f80546001600160a01b0319166001600160a01b0385169081179091555f90815260a260205260409020805460ff19168315151790555b610f7e6126b0565b8015610fc3575f805461ff0019169055604051600181527f7f26b83ff96e1f2b6a682f133852f6798a09c465da95921460cefb38474024989060200160405180910390a15b50505050505050505050565b6001600160a01b038083165f908152606b602090815260409182902082518084019093525463ffffffff81168352600160201b90049092169181018290529061102b5760405163828d566360e01b815260040160405180910390fd5b5f606a5f835f01518460200151604051602001611049929190613c79565b60408051601f198184030181529181528151602092830120835290820192909252015f20546001600160a01b0390811691508416810361109c5760405163e273c4a160e01b815260040160405180910390fd5b6110a684846126de565b6110b181338561276e565b604080513381526001600160a01b0386811660208301528316818301526060810185905290517fb7f8fd4d1faf9b2929dc269f59c53e3a2bccc44e9950f33a568fcbcb37eb69a99181900360800190a150505050565b5f80868660405160200161111c929190613c79565b6040516020818303038152906040528051906020012090505f60ff60f81b308360405180611ba00160405280611b66815260200161443b611b66913989898960405160200161116d93929190613e69565b60408051601f198184030181529082905261118b9291602001613ea1565b604051602081830303815290604052805190602001206040516020016111e394939291906001600160f81b031994909416845260609290921b6001600160601b03191660018401526015830152603582015260550190565b60408051808303601f19018152919052805160209091012098975050505050505050565b60a3546001600160a01b03163314611232576040516357b738d160e11b815260040160405180910390fd5b6001600160a01b038082165f908152606b6020908152604080832081518083018352905463ffffffff8116808352600160201b909104909516818401819052915190946112829390929101613c79565b60408051601f1981840301815291815281516020928301205f818152606a9093529120549091506001600160a01b031615806112d657505f818152606a60205260409020546001600160a01b038481169116145b156112f45760405163e0c897a760e01b815260040160405180910390fd5b6001600160a01b0383165f908152606b6020908152604080832080546001600160c01b031916905560a290915290819020805460ff19169055517fc2ae0bd0ec0fd0352bfe5bacac49637af342c1e40f1b80a7f74440dc7fe3f0639061135b9085906131df565b60405180910390a1505050565b60685460ff161561138c57604051630bc011ff60e21b815260040160405180910390fd5b606f546001600160a01b03166113b55760405163dde3cda760e01b815260040160405180910390fd5b606f546113cb906001600160a01b0316856126de565b6113d9868686868686612048565b505050505050565b60a3546001600160a01b0316331461140c576040516357b738d160e11b815260040160405180910390fd5b606d546001600160a01b031661143557604051634cb4711360e11b815260040160405180910390fd5b606f80546001600160a01b0319166001600160a01b0384169081179091555f81815260a26020908152604091829020805460ff19168515159081179091558251938452908301527fc7318b7ed6ba4f2908a3de396d8ab49b1dadb55db5b55123247a401f29ff8d82910160405180910390a15050565b60606114b6826122c2565b6114bf8361237d565b6114c884612431565b6040516020016114da93929190613e69565b6040516020818303038152906040529050919050565b5f80611506600160201b63ffffffff8516613ecf565b6115169063ffffffff8616613ee6565b600881901c5f90815260696020526040902054600160ff9092169190911b90811614949350505050565b60685460ff161561156457604051630bc011ff60e21b815260040160405180910390fd5b60685463ffffffff8681166101009092041614611594576040516302caf51760e11b815260040160405180910390fd5b6115c78c8c8c8c8c6115c25f8e8e8e8e8e8e8e6040516115b5929190613ef9565b60405180910390206108a5565b6127f9565b6001600160a01b0386166116ae57606f546001600160a01b0316611692575f6001600160a01b03851684825b6040519080825280601f01601f19166020018201604052801561161d576020820181803683370190505b5060405161162b9190613f08565b5f6040518083038185875af1925050503d805f8114611665576040519150601f19603f3d011682016040523d82523d5f602084013e61166a565b606091505b505090508061168c57604051630ce8f45160e31b815260040160405180910390fd5b506118d7565b606f546116a9906001600160a01b0316858561276e565b6118d7565b606d546001600160a01b0387811691161480156116dc5750606d5463ffffffff888116600160a01b90920416145b156116f3575f6001600160a01b03851684826115f3565b60685463ffffffff61010090910481169088160361171f576116a96001600160a01b0387168585612952565b5f8787604051602001611733929190613c79565b60408051601f1981840301815291815281516020928301205f818152606a9093529120549091506001600160a01b0316806118c9575f6117a88386868080601f0160208091040260200160405190810160405280939291908181526020018383808284375f9201919091525061263692505050565b90506117b581888861276e565b80606a5f8581526020019081526020015f205f6101000a8154816001600160a01b0302191690836001600160a01b0316021790555060405180604001604052808b63ffffffff1681526020018a6001600160a01b0316815250606b5f836001600160a01b03166001600160a01b031681526020019081526020015f205f820151815f015f6101000a81548163ffffffff021916908363ffffffff1602179055506020820151815f0160046101000a8154816001600160a01b0302191690836001600160a01b031602179055509050507f490e59a1701b938786ac72570a1efeac994a3dbe96e2e883e19e902ace6e6a398a8a8388886040516118bb959493929190613f4b565b60405180910390a1506118d4565b6118d481878761276e565b50505b7f1df3f2a973a00d6635911755c260704e95e8a5876997546798770f76396fda4d8a8888878760405161190e959493929190613f83565b60405180910390a1505050505050505050505050565b60685460ff161561194857604051630bc011ff60e21b815260040160405180910390fd5b6119506129a8565b60685463ffffffff610100909104811690881603611981576040516302caf51760e11b815260040160405180910390fd5b5f806060876001600160a01b038816611a64578834146119b45760405163b89240f560e01b815260040160405180910390fd5b606d54606e80546001600160a01b0383169650600160a01b90920463ffffffff169450906119e190613ca3565b80601f0160208091040260200160405190810160405280929190818152602001828054611a0d90613ca3565b8015611a585780601f10611a2f57610100808354040283529160200191611a58565b820191905f5260205f20905b815481529060010190602001808311611a3b57829003601f168201915b50505050509150611c3b565b3415611a835760405163798ee6f160e01b815260040160405180910390fd5b606f546001600160a01b0390811690891603611aa857611aa3888a6126de565b611c3b565b6001600160a01b038089165f908152606b602090815260409182902082518084019093525463ffffffff81168352600160201b90049092169181018290529015611b0757611af6898b6126de565b602081015181519095509350611c2e565b8515611b1957611b19898b8989612a01565b6040516370a0823160e01b81525f906001600160a01b038b16906370a0823190611b479030906004016131df565b602060405180830381865afa158015611b62573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611b869190613fb5565b9050611b9d6001600160a01b038b1633308e612cb2565b6040516370a0823160e01b81525f906001600160a01b038c16906370a0823190611bcb9030906004016131df565b602060405180830381865afa158015611be6573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611c0a9190613fb5565b9050611c168282613fcc565b6068548c9850610100900463ffffffff169650935050505b611c37896114ab565b9250505b7f501781209a1f8899323b96b4ef08b168df93e0a90c673d1e4cce39366cb62f9b5f84868e8e8688605354604051611c7a989796959493929190613fdf565b60405180910390a1611ca0611c9b5f85878f8f8789805190602001206108a5565b612cea565b8615611cae57611cae6124e5565b50505050611cbb60018055565b50505050505050565b60a3546001600160a01b03163314611cef576040516357b738d160e11b815260040160405180910390fd5b60a380546001600160a01b0319166001600160a01b0383169081179091556040517f32cf74f8a6d5f88593984d2cd52be5592bfa6884f5896175801a5069ef09cd6791611d3b916131df565b60405180910390a150565b60685460ff1615611d6a57604051630bc011ff60e21b815260040160405180910390fd5b60685463ffffffff8681166101009092041614611d9a576040516302caf51760e11b815260040160405180910390fd5b611dbc8c8c8c8c8c6115c260018e8e8e8e8e8e8e6040516115b5929190613ef9565b606f545f906001600160a01b0316611e6f57846001600160a01b031684888a8686604051602401611df09493929190614049565b60408051601f198184030181529181526020820180516001600160e01b0316630c035af960e11b17905251611e259190613f08565b5f6040518083038185875af1925050503d805f8114611e5f576040519150601f19603f3d011682016040523d82523d5f602084013e611e64565b606091505b505080915050611f20565b606f54611e86906001600160a01b0316868661276e565b846001600160a01b031687898585604051602401611ea79493929190614049565b60408051601f198184030181529181526020820180516001600160e01b0316630c035af960e11b17905251611edc9190613f08565b5f604051808303815f865af19150503d805f8114611f15576040519150601f19603f3d011682016040523d82523d5f602084013e611f1a565b606091505b50909150505b80611f3e576040516337e391c360e01b815260040160405180910390fd5b7f1df3f2a973a00d6635911755c260704e95e8a5876997546798770f76396fda4d8b89898888604051611f75959493929190613f83565b60405180910390a150505050505050505050505050565b5f54610100900460ff1615808015611faa57505f54600160ff909116105b80611fca5750611fb930612627565b158015611fca57505f5460ff166001145b611fe65760405162461bcd60e51b8152600401610d4890613d1b565b5f805460ff191660011790558015612007575f805461ff0019166101001790555b60405163f57ac68360e01b815260040160405180910390fd5b60405180910390a150505050505050565b5f8161203e868686610b6e565b1495945050505050565b60685463ffffffff610100909104811690871603612079576040516302caf51760e11b815260040160405180910390fd5b7f501781209a1f8899323b96b4ef08b168df93e0a90c673d1e4cce39366cb62f9b6001606860019054906101000a900463ffffffff163389898988886053546040516120cd99989796959493929190614083565b60405180910390a1612104611c9b6001606860019054906101000a900463ffffffff16338a8a8a89896040516115b5929190613ef9565b82156113d9576113d96124e5565b6001600160a01b038316158061212f57506001600160a01b038216155b1561214d5760405163f6b2911f60e01b815260040160405180910390fd5b60685463ffffffff61010090910481169085160361217e5760405163658b23ad60e01b815260040160405180910390fd5b6001600160a01b038281165f908152606b6020526040902054600160201b900416156121bd576040516317abdeeb60e21b815260040160405180910390fd5b5f84846040516020016121d1929190613c79565b60408051808303601f1901815282825280516020918201205f818152606a835283812080546001600160a01b0319166001600160a01b038a8116918217909255868601865263ffffffff8c81168089528c8416878a01818152848752606b89528987209a518b54915194166001600160c01b031990911617600160201b93909516929092029390931790975560a2855291859020805460ff191689151590811790915585519182529381019590955292840192909252606083015291507fdbe8a5da6a7a916d9adfda9160167a0f8a3da415ee6610e810e753853597fce79060800160405180910390a15050505050565b60408051600481526024810182526020810180516001600160e01b03166306fdde0360e01b17905290516060915f9182916001600160a01b038616916123089190613f08565b5f60405180830381855afa9150503d805f8114612340576040519150601f19603f3d011682016040523d82523d5f602084013e612345565b606091505b50915091508161237457604051806040016040528060078152602001664e4f5f4e414d4560c81b815250610b24565b610b2481612dd2565b60408051600481526024810182526020810180516001600160e01b03166395d89b4160e01b17905290516060915f9182916001600160a01b038616916123c39190613f08565b5f60405180830381855afa9150503d805f81146123fb576040519150601f19603f3d011682016040523d82523d5f602084013e612400565b606091505b50915091508161237457604051806040016040528060098152602001681393d7d4d6535093d360ba1b815250610b24565b60408051600481526024810182526020810180516001600160e01b031663313ce56760e01b17905290515f91829182916001600160a01b038616916124769190613f08565b5f60405180830381855afa9150503d805f81146124ae576040519150601f19603f3d011682016040523d82523d5f602084013e6124b3565b606091505b50915091508180156124c6575080516020145b6124d1576012610b24565b80806020019051810190610b2491906140ef565b6053546068805463ffffffff909216600160c81b0263ffffffff60c81b1990921691909117908190556001600160a01b03600160281b909104166333d6247d61252c610a27565b6040518263ffffffff1660e01b815260040161254a91815260200190565b5f604051808303815f87803b158015612561575f80fd5b505af1158015612573573d5f803e3d5ffd5b50505050565b5f61258e600160201b63ffffffff8416613ecf565b61259e9063ffffffff8516613ee6565b600881901c5f8181526069602052604090208054600160ff851690811b9182189283905593945091929190808216156125ea57604051630631b5f760e31b815260040160405180910390fd5b6040805163ffffffff808a168252881660208201527f4a402ac607e71571d0543be8fcccc358a4df62dcd019a1e7f7e98ca6175e8f2a9101612020565b6001600160a01b03163b151590565b5f8060405180611ba00160405280611b66815260200161443b611b66913983604051602001612666929190613ea1565b6040516020818303038152906040529050838151602083015ff591506001600160a01b0382166126a9576040516305f7d84960e51b815260040160405180910390fd5b5092915050565b5f54610100900460ff166126d65760405162461bcd60e51b8152600401610d489061410a565b610b4d612f5b565b6001600160a01b0382165f90815260a2602052604090205460ff1615612717576127136001600160a01b038316333084612cb2565b5050565b604051632770a7eb60e21b81526001600160a01b03831690639dc29fac906127459033908590600401614155565b5f604051808303815f87803b15801561275c575f80fd5b505af11580156113d9573d5f803e3d5ffd5b6001600160a01b0383165f90815260a2602052604090205460ff16156127a257610ce96001600160a01b0384168383612952565b6040516340c10f1960e01b81526001600160a01b038416906340c10f19906127d09085908590600401614155565b5f604051808303815f87803b1580156127e7575f80fd5b505af1158015611cbb573d5f803e3d5ffd5b606854604080516020808201879052818301869052825180830384018152606083019384905280519101206312bd9b1960e11b90925260648101919091525f91600160281b90046001600160a01b03169063257b3632906084016020604051808303815f875af115801561286f573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906128939190613fb5565b9050805f036128b457604051622f6fad60e01b815260040160405180910390fd5b5f80600160401b8716156128f3578691506128d1848a8489612031565b6128ee576040516338105f3b60e21b815260040160405180910390fd5b61293d565b602087901c61290381600161416e565b915087925061291e612916868c86610b6e565b8a8389612031565b61293b576040516338105f3b60e21b815260040160405180910390fd5b505b6129478282612f81565b505050505050505050565b610ce98363a9059cbb60e01b8484604051602401612971929190614155565b60408051601f198184030181529190526020810180516001600160e01b03166001600160e01b031990931692909217909152612ff3565b6002600154036129fa5760405162461bcd60e51b815260206004820152601f60248201527f5265656e7472616e637947756172643a207265656e7472616e742063616c6c006044820152606401610d48565b6002600155565b5f612a0f600482848661418b565b612a18916141b2565b9050632afa533160e01b6001600160e01b0319821601612b4c575f808080808080612a46896004818d61418b565b810190612a5391906141e2565b96509650965096509650965096508a8514612a81576040516303fffc4b60e01b815260040160405180910390fd5b604080516001600160a01b0389811660248301528881166044830152606482018890526084820187905260ff861660a483015260c4820185905260e48083018590528351808403909101815261010490920183526020820180516001600160e01b031663d505accf60e01b1790529151918e1691612aff9190613f08565b5f604051808303815f865af19150503d805f8114612b38576040519150601f19603f3d011682016040523d82523d5f602084013e612b3d565b606091505b50505050505050505050610812565b6001600160e01b031981166323f2ebc360e21b14612b7d57604051637141605d60e11b815260040160405180910390fd5b5f80808080808080612b928a6004818e61418b565b810190612b9f9190614231565b975097509750975097509750975097508c6001600160a01b0316638fcbaf0c60e01b8989898989898989604051602401612c249897969594939291906001600160a01b039889168152969097166020870152604086019490945260608501929092521515608084015260ff1660a083015260c082015260e08101919091526101000190565b60408051601f198184030181529181526020820180516001600160e01b03166001600160e01b0319909416939093179092529051612c629190613f08565b5f604051808303815f865af19150503d805f8114612c9b576040519150601f19603f3d011682016040523d82523d5f602084013e612ca0565b606091505b50505050505050505050505050505050565b6040516001600160a01b03808516602483015283166044820152606481018290526125739085906323b872dd60e01b90608401612971565b806001612cf96020600261438f565b612d039190613fcc565b60535410612d24576040516377ae67b360e11b815260040160405180910390fd5b5f60535f8154612d3390613d03565b918290555090505f5b6020811015612dc3578082901c600116600103612d6f578260338260208110612d6757612d67613cdb565b015550505050565b60338160208110612d8257612d82613cdb565b015460408051602081019290925281018490526060016040516020818303038152906040528051906020012092508080612dbb90613d03565b915050612d3c565b50610ce961439a565b60018055565b60606040825110612df157818060200190518101906107a391906143ae565b8151602003612f28575f5b602081108015612e2b5750828181518110612e1957612e19613cdb565b01602001516001600160f81b03191615155b15612e425780612e3a81613d03565b915050612dfc565b805f03612e795750506040805180820190915260128152714e4f545f56414c49445f454e434f44494e4760701b6020820152919050565b5f816001600160401b03811115612e9257612e92613420565b6040519080825280601f01601f191660200182016040528015612ebc576020820181803683370190505b5090505f5b82811015612f2057848181518110612edb57612edb613cdb565b602001015160f81c60f81b828281518110612ef857612ef8613cdb565b60200101906001600160f81b03191690815f1a90535080612f1881613d03565b915050612ec1565b509392505050565b50506040805180820190915260128152714e4f545f56414c49445f454e434f44494e4760701b602082015290565b919050565b5f54610100900460ff16612dcc5760405162461bcd60e51b8152600401610d489061410a565b5f612f96600160201b63ffffffff8416613ecf565b612fa69063ffffffff8516613ee6565b600881901c5f8181526069602052604081208054600160ff861690811b91821892839055949550929392918183169003611cbb57604051630c8d9eab60e31b815260040160405180910390fd5b5f613047826040518060400160405280602081526020017f5361666545524332303a206c6f772d6c6576656c2063616c6c206661696c6564815250856001600160a01b03166130c49092919063ffffffff16565b805190915015610ce95780806020019051810190613065919061441f565b610ce95760405162461bcd60e51b815260206004820152602a60248201527f5361666545524332303a204552433230206f7065726174696f6e20646964206e6044820152691bdd081cdd58d8d9595960b21b6064820152608401610d48565b6060610b2484845f85855f80866001600160a01b031685876040516130e99190613f08565b5f6040518083038185875af1925050503d805f8114613123576040519150601f19603f3d011682016040523d82523d5f602084013e613128565b606091505b509150915061313987838387613144565b979650505050505050565b606083156131b05782515f036131a95761315d85612627565b6131a95760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610d48565b5081610b24565b610b2483838151156131c55781518083602001fd5b8060405162461bcd60e51b8152600401610d489190613364565b6001600160a01b0391909116815260200190565b803563ffffffff81168114612f56575f80fd5b6001600160a01b038116811461321a575f80fd5b50565b5f806040838503121561322e575f80fd5b613237836131f3565b9150602083013561324781613206565b809150509250929050565b801515811461321a575f80fd5b5f8083601f84011261326f575f80fd5b5081356001600160401b03811115613285575f80fd5b60208301915083602082850101111561329c575f80fd5b9250929050565b5f805f805f608086880312156132b7575f80fd5b6132c0866131f3565b945060208601356132d081613206565b935060408601356132e081613252565b925060608601356001600160401b038111156132fa575f80fd5b6133068882890161325f565b969995985093965092949392505050565b5f5b83811015613331578181015183820152602001613319565b50505f910152565b5f8151808452613350816020860160208601613317565b601f01601f19169290920160200192915050565b602081525f6133766020830184613339565b9392505050565b5f6020828403121561338d575f80fd5b813561337681613206565b60ff8116811461321a575f80fd5b5f805f805f805f60e0888a0312156133bc575f80fd5b87356133c781613398565b96506133d5602089016131f3565b955060408801356133e581613206565b94506133f3606089016131f3565b9350608088013561340381613206565b9699959850939692959460a0840135945060c09093013592915050565b634e487b7160e01b5f52604160045260245ffd5b604051601f8201601f191681016001600160401b038111828210171561345c5761345c613420565b604052919050565b5f6001600160401b0382111561347c5761347c613420565b5060051b60200190565b5f82601f830112613495575f80fd5b813560206134aa6134a583613464565b613434565b82815260059290921b840181019181810190868411156134c8575f80fd5b8286015b848110156134ea576134dd816131f3565b83529183019183016134cc565b509695505050505050565b5f82601f830112613504575f80fd5b813560206135146134a583613464565b82815260059290921b84018101918181019086841115613532575f80fd5b8286015b848110156134ea57803561354981613206565b8352918301918301613536565b5f805f8060808587031215613569575f80fd5b84356001600160401b038082111561357f575f80fd5b61358b88838901613486565b95506020915081870135818111156135a1575f80fd5b6135ad89828a016134f5565b9550506040870135818111156135c1575f80fd5b6135cd89828a016134f5565b9450506060870135818111156135e1575f80fd5b87019050601f810188136135f3575f80fd5b80356136016134a582613464565b81815260059190911b8201830190838101908a83111561361f575f80fd5b928401925b8284101561364657833561363781613252565b82529284019290840190613624565b979a9699509497505050505050565b5f805f60608486031215613667575f80fd5b613670846131f3565b9250602084013561368081613206565b9150604084013561369081613206565b809150509250925092565b5f602082840312156136ab575f80fd5b5035919050565b8061040081018310156107a3575f80fd5b5f805f61044084860312156136d6575f80fd5b833592506136e785602086016136b2565b91506136f661042085016131f3565b90509250925092565b5f8060408385031215613710575f80fd5b82356001600160401b0380821115613726575f80fd5b61373286838701613486565b93506020850135915080821115613747575f80fd5b5061375485828601613486565b9150509250929050565b5f6001600160401b0382111561377657613776613420565b50601f01601f191660200190565b5f82601f830112613793575f80fd5b81356137a16134a58261375e565b8181528460208386010111156137b5575f80fd5b816020850160208301375f918101602001919091529392505050565b5f805f805f805f805f6101208a8c0312156137ea575f80fd5b6137f38a6131f3565b985060208a013561380381613206565b975061381160408b016131f3565b965060608a013561382181613206565b955060808a013561383181613206565b945060a08a01356001600160401b0381111561384b575f80fd5b6138578c828d01613784565b94505060c08a013561386881613206565b925060e08a013561387881613206565b91506101008a013561388981613252565b809150509295985092959850929598565b5f80604083850312156138ab575f80fd5b82356138b681613206565b946020939093013593505050565b5f805f805f60a086880312156138d8575f80fd5b6138e1866131f3565b945060208601356138f181613206565b935060408601356001600160401b038082111561390c575f80fd5b61391889838a01613784565b9450606088013591508082111561392d575f80fd5b5061393a88828901613784565b925050608086013561394b81613398565b809150509295509295909350565b5f805f805f8060a0878903121561396e575f80fd5b613977876131f3565b9550602087013561398781613206565b945060408701359350606087013561399e81613252565b925060808701356001600160401b038111156139b8575f80fd5b6139c489828a0161325f565b979a9699509497509295939492505050565b5f80604083850312156139e7575f80fd5b82356139f281613206565b9150602083013561324781613252565b5f8060408385031215613a13575f80fd5b613a1c836131f3565b9150613a2a602084016131f3565b90509250929050565b5f805f805f805f805f805f806109208d8f031215613a4f575f80fd5b613a598e8e6136b2565b9b50613a698e6104008f016136b2565b9a506108008d013599506108208d013598506108408d01359750613a906108608e016131f3565b9650613aa06108808e0135613206565b6108808d01359550613ab56108a08e016131f3565b9450613ac56108c08e0135613206565b6108c08d013593506108e08d013592506001600160401b036109008e01351115613aed575f80fd5b613afe8e6109008f01358f0161325f565b81935080925050509295989b509295989b509295989b565b5f805f805f805f60c0888a031215613b2c575f80fd5b613b35886131f3565b96506020880135613b4581613206565b9550604088013594506060880135613b5c81613206565b93506080880135613b6c81613252565b925060a08801356001600160401b03811115613b86575f80fd5b613b928a828b0161325f565b989b979a50959850939692959293505050565b5f805f805f8060c08789031215613bba575f80fd5b613bc3876131f3565b95506020870135613bd381613206565b9450613be1604088016131f3565b93506060870135613bf181613206565b92506080870135613c0181613206565b915060a08701356001600160401b03811115613c1b575f80fd5b613c2789828a01613784565b9150509295509295509295565b5f805f806104608587031215613c48575f80fd5b84359350613c5986602087016136b2565b9250613c6861042086016131f3565b939692955092936104400135925050565b60e09290921b6001600160e01b031916825260601b6001600160601b031916600482015260180190565b600181811c90821680613cb757607f821691505b602082108103613cd557634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52603260045260245ffd5b634e487b7160e01b5f52601160045260245ffd5b5f60018201613d1457613d14613cef565b5060010190565b6020808252602e908201527f496e697469616c697a61626c653a20636f6e747261637420697320616c72656160408201526d191e481a5b9a5d1a585b1a5e995960921b606082015260800190565b601f821115610ce9575f81815260208120601f850160051c81016020861015613d8f5750805b601f850160051c820191505b818110156113d957828155600101613d9b565b81516001600160401b03811115613dc757613dc7613420565b613ddb81613dd58454613ca3565b84613d69565b602080601f831160018114613e0e575f8415613df75750858301515b5f19600386901b1c1916600185901b1785556113d9565b5f85815260208120601f198616915b82811015613e3c57888601518255948401946001909101908401613e1d565b5085821015613e5957878501515f19600388901b60f8161c191681555b5050505050600190811b01905550565b606081525f613e7b6060830186613339565b8281036020840152613e8d8186613339565b91505060ff83166040830152949350505050565b5f8351613eb2818460208801613317565b835190830190613ec6818360208801613317565b01949350505050565b80820281158282048414176107a3576107a3613cef565b808201808211156107a3576107a3613cef565b818382375f9101908152919050565b5f8251613f19818460208701613317565b9190910192915050565b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b63ffffffff861681526001600160a01b038581166020830152841660408201526080606082018190525f906131399083018486613f23565b94855263ffffffff9390931660208501526001600160a01b039182166040850152166060830152608082015260a00190565b5f60208284031215613fc5575f80fd5b5051919050565b818103818111156107a3576107a3613cef565b60ff8916815263ffffffff88811660208301526001600160a01b03888116604084015287821660608401528616608083015260a0820185905261010060c083018190525f9161403084830187613339565b925080851660e085015250509998505050505050505050565b6001600160a01b038516815263ffffffff841660208201526060604082018190525f906140799083018486613f23565b9695505050505050565b60ff8a16815263ffffffff89811660208301526001600160a01b03898116604084015288821660608401528716608083015260a0820186905261010060c083018190525f916140d58483018789613f23565b925080851660e085015250509a9950505050505050505050565b5f602082840312156140ff575f80fd5b815161337681613398565b6020808252602b908201527f496e697469616c697a61626c653a20636f6e7472616374206973206e6f74206960408201526a6e697469616c697a696e6760a81b606082015260800190565b6001600160a01b03929092168252602082015260400190565b63ffffffff8181168382160190808211156126a9576126a9613cef565b5f8085851115614199575f80fd5b838611156141a5575f80fd5b5050820193919092039150565b6001600160e01b031981358181169160048510156141da5780818660040360031b1b83161692505b505092915050565b5f805f805f805f60e0888a0312156141f8575f80fd5b873561420381613206565b9650602088013561421381613206565b95506040880135945060608801359350608088013561340381613398565b5f805f805f805f80610100898b031215614249575f80fd5b883561425481613206565b9750602089013561426481613206565b96506040890135955060608901359450608089013561428281613252565b935060a089013561429281613398565b979a969950949793969295929450505060c08201359160e0013590565b600181815b808511156142e957815f19048211156142cf576142cf613cef565b808516156142dc57918102915b93841c93908002906142b4565b509250929050565b5f826142ff575060016107a3565b8161430b57505f6107a3565b8160018114614321576002811461432b57614347565b60019150506107a3565b60ff84111561433c5761433c613cef565b50506001821b6107a3565b5060208310610133831016604e8410600b841016171561436a575081810a6107a3565b61437483836142af565b805f190482111561438757614387613cef565b029392505050565b5f61337683836142f1565b634e487b7160e01b5f52600160045260245ffd5b5f602082840312156143be575f80fd5b81516001600160401b038111156143d3575f80fd5b8201601f810184136143e3575f80fd5b80516143f16134a58261375e565b818152856020838501011115614405575f80fd5b614416826020830160208601613317565b95945050505050565b5f6020828403121561442f575f80fd5b81516133768161325256fe6101006040523480156200001257600080fd5b5060405162001b6638038062001b6683398101604081905262000035916200028d565b82826003620000458382620003a1565b506004620000548282620003a1565b50503360c0525060ff811660e052466080819052620000739062000080565b60a052506200046d915050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f620000ad6200012e565b805160209182012060408051808201825260018152603160f81b90840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b6060600380546200013f9062000312565b80601f01602080910402602001604051908101604052809291908181526020018280546200016d9062000312565b8015620001be5780601f106200019257610100808354040283529160200191620001be565b820191906000526020600020905b815481529060010190602001808311620001a057829003601f168201915b5050505050905090565b634e487b7160e01b600052604160045260246000fd5b600082601f830112620001f057600080fd5b81516001600160401b03808211156200020d576200020d620001c8565b604051601f8301601f19908116603f01168101908282118183101715620002385762000238620001c8565b816040528381526020925086838588010111156200025557600080fd5b600091505b838210156200027957858201830151818301840152908201906200025a565b600093810190920192909252949350505050565b600080600060608486031215620002a357600080fd5b83516001600160401b0380821115620002bb57600080fd5b620002c987838801620001de565b94506020860151915080821115620002e057600080fd5b50620002ef86828701620001de565b925050604084015160ff811681146200030757600080fd5b809150509250925092565b600181811c908216806200032757607f821691505b6020821081036200034857634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200039c57600081815260208120601f850160051c81016020861015620003775750805b601f850160051c820191505b81811015620003985782815560010162000383565b5050505b505050565b81516001600160401b03811115620003bd57620003bd620001c8565b620003d581620003ce845462000312565b846200034e565b602080601f8311600181146200040d5760008415620003f45750858301515b600019600386901b1c1916600185901b17855562000398565b600085815260208120601f198616915b828110156200043e578886015182559484019460019091019084016200041d565b50858210156200045d5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60805160a05160c05160e0516116aa620004bc6000396000610237015260008181610307015281816105c001526106a70152600061053a015260008181610379015261050401526116aa6000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806370a08231116100d8578063a457c2d71161008c578063d505accf11610066578063d505accf1461039b578063dd62ed3e146103ae578063ffa1ad74146103f457600080fd5b8063a457c2d71461034e578063a9059cbb14610361578063cd0d00961461037457600080fd5b806395d89b41116100bd57806395d89b41146102e75780639dc29fac146102ef578063a3c573eb1461030257600080fd5b806370a08231146102915780637ecebe00146102c757600080fd5b806330adf81f1161012f5780633644e515116101145780633644e51514610261578063395093511461026957806340c10f191461027c57600080fd5b806330adf81f14610209578063313ce5671461023057600080fd5b806318160ddd1161016057806318160ddd146101bd57806320606b70146101cf57806323b872dd146101f657600080fd5b806306fdde031461017c578063095ea7b31461019a575b600080fd5b610184610430565b60405161019191906113e4565b60405180910390f35b6101ad6101a8366004611479565b6104c2565b6040519015158152602001610191565b6002545b604051908152602001610191565b6101c17f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f81565b6101ad6102043660046114a3565b6104dc565b6101c17f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152602001610191565b6101c1610500565b6101ad610277366004611479565b61055c565b61028f61028a366004611479565b6105a8565b005b6101c161029f3660046114df565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205490565b6101c16102d53660046114df565b60056020526000908152604090205481565b610184610680565b61028f6102fd366004611479565b61068f565b6103297f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610191565b6101ad61035c366004611479565b61075e565b6101ad61036f366004611479565b61082f565b6101c17f000000000000000000000000000000000000000000000000000000000000000081565b61028f6103a9366004611501565b61083d565b6101c16103bc366004611574565b73ffffffffffffffffffffffffffffffffffffffff918216600090815260016020908152604080832093909416825291909152205490565b6101846040518060400160405280600181526020017f310000000000000000000000000000000000000000000000000000000000000081525081565b60606003805461043f906115a7565b80601f016020809104026020016040519081016040528092919081815260200182805461046b906115a7565b80156104b85780601f1061048d576101008083540402835291602001916104b8565b820191906000526020600020905b81548152906001019060200180831161049b57829003601f168201915b5050505050905090565b6000336104d0818585610b73565b60019150505b92915050565b6000336104ea858285610d27565b6104f5858585610dfe565b506001949350505050565b60007f00000000000000000000000000000000000000000000000000000000000000004614610537576105324661106d565b905090565b507f000000000000000000000000000000000000000000000000000000000000000090565b33600081815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff871684529091528120549091906104d090829086906105a3908790611629565b610b73565b3373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614610672576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d4272696467650000000000000000000000000000000060648201526084015b60405180910390fd5b61067c8282611135565b5050565b60606004805461043f906115a7565b3373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614610754576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d427269646765000000000000000000000000000000006064820152608401610669565b61067c8282611228565b33600081815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716845290915281205490919083811015610822576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760448201527f207a65726f0000000000000000000000000000000000000000000000000000006064820152608401610669565b6104f58286868403610b73565b6000336104d0818585610dfe565b834211156108cc576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f546f6b656e577261707065643a3a7065726d69743a204578706972656420706560448201527f726d6974000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8716600090815260056020526040812080547f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9918a918a918a9190866109268361163c565b9091555060408051602081019690965273ffffffffffffffffffffffffffffffffffffffff94851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090506000610991610500565b6040517f19010000000000000000000000000000000000000000000000000000000000006020820152602281019190915260428101839052606201604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181528282528051602091820120600080855291840180845281905260ff89169284019290925260608301879052608083018690529092509060019060a0016020604051602081039080840390855afa158015610a55573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811615801590610ad057508973ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b610b5c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f546f6b656e577261707065643a3a7065726d69743a20496e76616c696420736960448201527f676e6174757265000000000000000000000000000000000000000000000000006064820152608401610669565b610b678a8a8a610b73565b50505050505050505050565b73ffffffffffffffffffffffffffffffffffffffff8316610c15576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460448201527f72657373000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8216610cb8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f20616464726560448201527f73730000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff83811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b73ffffffffffffffffffffffffffffffffffffffff8381166000908152600160209081526040808320938616835292905220547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610df85781811015610deb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610669565b610df88484848403610b73565b50505050565b73ffffffffffffffffffffffffffffffffffffffff8316610ea1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f20616460448201527f64726573730000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8216610f44576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201527f65737300000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff831660009081526020819052604090205481811015610ffa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e742065786365656473206260448201527f616c616e636500000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610df8565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f611098610430565b8051602091820120604080518082018252600181527f310000000000000000000000000000000000000000000000000000000000000090840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b73ffffffffffffffffffffffffffffffffffffffff82166111b2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610669565b80600260008282546111c49190611629565b909155505073ffffffffffffffffffffffffffffffffffffffff8216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b73ffffffffffffffffffffffffffffffffffffffff82166112cb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360448201527f73000000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff821660009081526020819052604090205481811015611381576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60448201527f63650000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff83166000818152602081815260408083208686039055600280548790039055518581529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9101610d1a565b600060208083528351808285015260005b81811015611411578581018301518582016040015282016113f5565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461147457600080fd5b919050565b6000806040838503121561148c57600080fd5b61149583611450565b946020939093013593505050565b6000806000606084860312156114b857600080fd5b6114c184611450565b92506114cf60208501611450565b9150604084013590509250925092565b6000602082840312156114f157600080fd5b6114fa82611450565b9392505050565b600080600080600080600060e0888a03121561151c57600080fd5b61152588611450565b965061153360208901611450565b95506040880135945060608801359350608088013560ff8116811461155757600080fd5b9699959850939692959460a0840135945060c09093013592915050565b6000806040838503121561158757600080fd5b61159083611450565b915061159e60208401611450565b90509250929050565b600181811c908216806115bb57607f821691505b6020821081036115f4577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b808201808211156104d6576104d66115fa565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361166d5761166d6115fa565b506001019056fea26469706673582212208d88fee561cff7120d381c345cfc534cef8229a272dc5809d4bbb685ad67141164736f6c63430008110033a2646970667358221220ca7a7fd14ec73edf6b3d053ef2133e4a8110a83daf9abb364c35f9d9eb2a083564736f6c63430008140033"; - - uint8 internal constant LEAF_TYPE_ASSET = 0; - uint8 internal constant LEAF_TYPE_MESSAGE = 1; - address internal constant LXLY_BRIDGE = 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe; - string internal constant NATIVE_CONVERTER_VERSION = "0.5.0"; - uint32 internal constant NETWORK_ID_L1 = 0; - uint32 internal constant NETWORK_ID_L2 = 1; - uint8 internal constant ORIGINAL_UNDERLYING_TOKEN_DECIMALS = 18; - bytes32 internal constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes4 internal constant PERMIT_SIGNATURE = 0xd505accf; - uint256 constant MAX_NON_MIGRATABLE_BACKING_PERCENTAGE = 1e17; - - MockERC20MintableBurnable internal customToken; - MockERC20MintableBurnable internal underlyingToken; - uint256 internal zkevmFork; - uint256 internal beforeInit; - bytes internal underlyingTokenMetadata; - address internal migrationManager; - - GenericNativeConverter internal nativeConverter; - - // initialization arguments - address internal owner = makeAddr("owner"); - address internal recipient = makeAddr("recipient"); - address internal originUnderlyingToken = makeAddr("originUnderlyingToken"); - - uint256 internal senderPrivateKey = 0xBEEF; - address internal sender = vm.addr(senderPrivateKey); - - event BridgeEvent( - uint8 leafType, - uint32 originNetwork, - address originAddress, - uint32 destinationNetwork, - address destinationAddress, - uint256 amount, - bytes metadata, - uint32 depositCount - ); - - function setUp() public virtual { - zkevmFork = vm.createSelectFork("polygon_zkevm", 19164969); - - // Setup tokens - underlyingToken = new MockERC20MintableBurnable(); - underlyingToken.initialize("Underlying Token", "uTKN", 18); - - GenericCustomToken _customToken = new GenericCustomToken(); - address calculatedNativeConverterAddr = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2); - vm.etch(LXLY_BRIDGE, SOVEREIGN_BRIDGE_BYTECODE); - bytes memory initData = abi.encodeCall( - GenericCustomToken.reinitialize, - (address(this), "Custom Token", "cTKN", 18, LXLY_BRIDGE, calculatedNativeConverterAddr) - ); - _customToken = GenericCustomToken( - payable(address(new TransparentUpgradeableProxy(address(_customToken), address(this), initData))) - ); - - // assign variables for generic testing - customToken = MockERC20MintableBurnable(address(_customToken)); - migrationManager = makeAddr("migrationManager"); - underlyingTokenMetadata = abi.encode("Underlying Token", "uTKN", 18); - - // Deploy and initialize converter - nativeConverter = GenericNativeConverter(address(new GenericNativeConverter())); - - /// important to assign customToken, underlyingToken, and nativeConverter - /// before the snapshot, so test_initialize will work - beforeInit = vm.snapshotState(); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - 18, // decimals - address(customToken), // custom token - address(underlyingToken), // wrapped underlying token - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - nativeConverter = GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - assertEq(address(nativeConverter), calculatedNativeConverterAddr); - - _mapCustomToken(originUnderlyingToken, address(underlyingToken), false); - - vm.label(address(customToken), "cTKN"); - vm.label(address(this), "testerAddress"); - vm.label(LXLY_BRIDGE, "lxlyBridge"); - vm.label(migrationManager, "migrationManager"); - vm.label(owner, "owner"); - vm.label(recipient, "recipient"); - vm.label(sender, "sender"); - vm.label(address(nativeConverter), "NativeConverter"); - vm.label(address(underlyingToken), "uTKN"); - } - - function test_setup() public view { - assertEq(nativeConverter.layerXLxlyId(), NETWORK_ID_L1); - assertEq(address(nativeConverter.migrationManager()), migrationManager); - assertEq(address(nativeConverter.customToken()), address(customToken)); - assertEq(address(nativeConverter.lxlyBridge()), LXLY_BRIDGE); - assertEq(address(nativeConverter.underlyingToken()), address(underlyingToken)); - } - - function test_initialize() public virtual { - vm.revertToState(beforeInit); - - bytes memory initData; - initData = abi.encodeCall( - nativeConverter.initialize, - ( - address(0), - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(NativeConverter.InvalidOwner.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(0), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(NativeConverter.InvalidCustomToken.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(0), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(NativeConverter.InvalidUnderlyingToken.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - address(0), - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(NativeConverter.InvalidLxLyBridge.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - MockERC20 dummyToken = new MockERC20(); - dummyToken.initialize("Dummy Token", "DT", 6); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(dummyToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(abi.encodeWithSelector(NativeConverter.NonMatchingCustomTokenDecimals.selector, 6, 18)); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - dummyToken = new MockERC20(); - dummyToken.initialize("Dummy Token", "DT", 6); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(dummyToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager - ) - ); - vm.expectRevert(abi.encodeWithSelector(NativeConverter.NonMatchingUnderlyingTokenDecimals.selector, 6, 18)); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - vm.revertToState(beforeInit); - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - 1e19, - migrationManager - ) - ); - vm.expectRevert(NativeConverter.InvalidNonMigratableBackingPercentage.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - nativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - address(0) - ) - ); - vm.expectRevert(NativeConverter.InvalidMigrationManager.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - } - - function test_convert() public { - uint256 amount = 100; - - vm.startPrank(owner); - nativeConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - nativeConverter.convert(amount, recipient); - nativeConverter.unpause(); - vm.stopPrank(); - - vm.startPrank(sender); - vm.expectRevert(NativeConverter.InvalidAssets.selector); - nativeConverter.convert(0, recipient); - - vm.expectRevert(NativeConverter.InvalidReceiver.selector); - nativeConverter.convert(amount, address(0)); - - deal(address(underlyingToken), sender, amount); - - vm.expectRevert("ERC20: subtraction underflow"); - nativeConverter.convert(amount, recipient); - - underlyingToken.approve(address(nativeConverter), amount); - nativeConverter.convert(amount, recipient); - - assertEq(underlyingToken.balanceOf(sender), 0); - assertEq(underlyingToken.balanceOf(address(nativeConverter)), amount); - assertEq(customToken.balanceOf(recipient), amount); - assertEq(nativeConverter.backingOnLayerY(), amount); - vm.stopPrank(); - } - - function test_convertWithPermit() public { - uint256 amount = 100; - - vm.startPrank(owner); - nativeConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - nativeConverter.convertWithPermit(amount, recipient, ""); - nativeConverter.unpause(); - vm.stopPrank(); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign( - senderPrivateKey, - keccak256( - abi.encodePacked( - "\x19\x01", - underlyingToken.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - PERMIT_TYPEHASH, - sender, - address(nativeConverter), - amount, - vm.getNonce(sender), - block.timestamp - ) - ) - ) - ) - ); - bytes memory permitData = - abi.encodeWithSelector(PERMIT_SIGNATURE, sender, address(nativeConverter), amount, block.timestamp, v, r, s); - - vm.startPrank(sender); - - vm.expectRevert(NativeConverter.InvalidPermitData.selector); - nativeConverter.convertWithPermit(amount, recipient, ""); - - vm.expectRevert(NativeConverter.InvalidAssets.selector); - nativeConverter.convertWithPermit(0, recipient, permitData); - - vm.expectRevert(NativeConverter.InvalidReceiver.selector); - nativeConverter.convertWithPermit(amount, address(0), permitData); - - vm.expectRevert("ERC20: subtraction underflow"); - nativeConverter.convertWithPermit(amount, recipient, permitData); - - deal(address(underlyingToken), sender, amount); - nativeConverter.convertWithPermit(amount, recipient, permitData); - - assertEq(underlyingToken.balanceOf(sender), 0); - assertEq(underlyingToken.balanceOf(address(nativeConverter)), amount); - assertEq(customToken.balanceOf(recipient), amount); - assertEq(nativeConverter.backingOnLayerY(), amount); - vm.stopPrank(); - } - - function test_maxDeconvert() public { - uint256 amount = 100; - - vm.startPrank(owner); - nativeConverter.pause(); - vm.assertEq(nativeConverter.maxDeconvert(sender), 0); - nativeConverter.unpause(); - - vm.assertEq(nativeConverter.maxDeconvert(sender), 0); // owner has 0 shares - - deal(address(customToken), sender, amount); // mint shares - - uint256 backingOnLayerY = 0; - assertEq(nativeConverter.maxDeconvert(sender), backingOnLayerY); - - // create backing on layer Y - deal(address(underlyingToken), owner, amount); - - underlyingToken.approve(address(nativeConverter), amount); - backingOnLayerY += nativeConverter.convert(amount, recipient); - vm.stopPrank(); - - deal(address(customToken), sender, amount); // mint shares - assertEq(nativeConverter.maxDeconvert(sender), backingOnLayerY); - - deal(address(customToken), sender, amount); // mint additional shares - assertLe(nativeConverter.maxDeconvert(sender), backingOnLayerY); // sender has more shares than the backing on layer Y - } - - function test_deconvert() public { - uint256 amount = 100; - - vm.startPrank(owner); - nativeConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - nativeConverter.deconvert(amount, recipient); - nativeConverter.unpause(); - vm.stopPrank(); - - vm.startPrank(sender); - vm.expectRevert(NativeConverter.InvalidShares.selector); - nativeConverter.deconvert(0, recipient); - - vm.expectRevert(NativeConverter.InvalidReceiver.selector); - nativeConverter.deconvert(amount, address(0)); - - vm.expectRevert(abi.encodeWithSelector(NativeConverter.AssetsTooLarge.selector, 0, amount)); - nativeConverter.deconvert(amount, recipient); // no backing on layer Y - - // create backing on layer Y - uint256 backingOnLayerY = 0; - deal(address(underlyingToken), owner, amount); - vm.startPrank(owner); - underlyingToken.approve(address(nativeConverter), amount); - backingOnLayerY = nativeConverter.convert(amount, recipient); - vm.stopPrank(); - - vm.startPrank(sender); - vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, sender, 0, amount)); - nativeConverter.deconvert(amount, recipient); // sender has 0 shares - - deal(address(customToken), sender, amount); // mint shares - - uint256 returnedAssets = nativeConverter.deconvert(amount, recipient); - vm.stopPrank(); - - assertEq(returnedAssets, backingOnLayerY); - assertEq(underlyingToken.balanceOf(recipient), amount); - assertEq(underlyingToken.balanceOf(address(nativeConverter)), 0); - assertEq(customToken.balanceOf(sender), 0); - assertEq(nativeConverter.backingOnLayerY(), 0); - } - - function test_deconvertAndBridge() public { - uint256 amount = 100; - - vm.startPrank(owner); - nativeConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L1, true); - nativeConverter.unpause(); - vm.stopPrank(); - - vm.prank(sender); - vm.expectRevert(NativeConverter.InvalidDestinationNetworkId.selector); - nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L2, true); - - // create backing on layer Y - uint256 backingOnLayerY = 0; - underlyingToken.mint(owner, amount); - vm.startPrank(owner); - underlyingToken.approve(address(nativeConverter), amount); - backingOnLayerY = nativeConverter.convert(amount, recipient); - vm.stopPrank(); - - deal(address(customToken), sender, amount); // mint shares - - vm.prank(sender); - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_ASSET, - NETWORK_ID_L1, - originUnderlyingToken, - NETWORK_ID_L1, - recipient, - amount, - underlyingTokenMetadata, - 55413 - ); - uint256 returnedAssets = nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L1, true); - - assertEq(returnedAssets, backingOnLayerY); - assertEq(underlyingToken.balanceOf(address(nativeConverter)), 0); - assertEq(customToken.balanceOf(sender), 0); - assertEq(nativeConverter.backingOnLayerY(), 0); - } - - function test_migrateBackingToLayerX() public { - uint256 amount = 100; - uint256 amountToMigrate = 90; - - // Try to migrate as the owner with a specific amount - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), nativeConverter.MIGRATOR_ROLE() - ) - ); // only owner can call this function - nativeConverter.migrateBackingToLayerX(amount); - - underlyingToken.mint(owner, amount); - - vm.startPrank(owner); - - nativeConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - nativeConverter.migrateBackingToLayerX(amount); - nativeConverter.unpause(); - - vm.expectRevert(NativeConverter.InvalidAssets.selector); - nativeConverter.migrateBackingToLayerX(0); // try with 0 backing - - uint256 currentBacking = nativeConverter.backingOnLayerY(); - - vm.expectRevert( - abi.encodeWithSelector(NativeConverter.AssetsTooLarge.selector, currentBacking, currentBacking + 1) - ); - nativeConverter.migrateBackingToLayerX(currentBacking + 1); - - // create backing on layer Y - uint256 backingOnLayerY = 0; - underlyingToken.approve(address(nativeConverter), amount); - backingOnLayerY = nativeConverter.convert(amount, recipient); - - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_ASSET, - NETWORK_ID_L1, - originUnderlyingToken, - NETWORK_ID_L1, - migrationManager, - amountToMigrate, - underlyingTokenMetadata, - 55413 - ); - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_MESSAGE, - NETWORK_ID_L2, - address(nativeConverter), - NETWORK_ID_L1, - migrationManager, - 0, - abi.encode( - MigrationManager.CrossNetworkInstruction.COMPLETE_MIGRATION, - abi.encode(amountToMigrate, amountToMigrate) - ), - 55414 - ); - vm.expectEmit(); - emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); - nativeConverter.migrateBackingToLayerX(amountToMigrate); - assertEq(underlyingToken.balanceOf(address(nativeConverter)), backingOnLayerY - amountToMigrate); - - vm.stopPrank(); - } - - function test_version() public view { - assertEq(nativeConverter.version(), NATIVE_CONVERTER_VERSION); - } - - function _mapCustomToken(address _originTokenAddress, address _sovereignTokenAddress, bool _isNotMintable) - internal - { - uint32[] memory originNetworks = new uint32[](1); - originNetworks[0] = NETWORK_ID_L1; - address[] memory originTokenAddresses = new address[](1); - originTokenAddresses[0] = _originTokenAddress; - address[] memory sovereignTokenAddresses = new address[](1); - sovereignTokenAddresses[0] = _sovereignTokenAddress; - bool[] memory isNotMintable = new bool[](1); - isNotMintable[0] = _isNotMintable; - - (, bytes memory data) = address(LXLY_BRIDGE).staticcall(abi.encodeWithSignature("bridgeManager()")); - address bridgeManager = abi.decode(data, (address)); - - vm.prank(bridgeManager); - IBridgeL2SovereignChain(LXLY_BRIDGE).setMultipleSovereignTokenAddress( - originNetworks, originTokenAddresses, sovereignTokenAddresses, isNotMintable - ); - } - - function _proxify(address logic, address admin, bytes memory initData) internal returns (address proxy) { - proxy = address(new TransparentUpgradeableProxy(logic, admin, initData)); - } -} diff --git a/test/MigrationManager.t.sol b/test/MigrationManager.t.sol deleted file mode 100644 index 84df8310..00000000 --- a/test/MigrationManager.t.sol +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import "forge-std/Test.sol"; - -import {MigrationManager, PausableUpgradeable} from "../src/MigrationManager.sol"; - -import {ERC20} from "@openzeppelin-contracts/token/ERC20/ERC20.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import {IAccessControl} from "@openzeppelin-contracts/access/IAccessControl.sol"; -import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; - -contract MockLxlyBridge { - function networkID() external pure returns (uint32) { - return 0; - } -} - -contract MockVbToken { - IERC20 public underlyingToken; - - function setUnderlyingToken(address _underlyingToken) external { - underlyingToken = IERC20(_underlyingToken); - } - - function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) external {} -} - -contract MockERC20 is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} -} - -contract MockERC20WithDeposit is ERC20 { - bool public canDeposit; - - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} - - function setCanDeposit(bool _canDeposit) external { - canDeposit = _canDeposit; - } - - function deposit() external payable { - if (canDeposit) { - _mint(msg.sender, msg.value); - } - } -} - -contract MigrationManagerTest is Test { - MigrationManager internal migrationManager; - address internal migrationManagerImpl; - MockERC20 internal underlyingToken; - MockLxlyBridge lxlyBridge; - MockVbToken vbToken; - - uint32 constant NETWORK_ID_X = 0; // mainnet/sepolia - uint32 constant NETWORK_ID_Y = 29; // katana-apex - - address internal owner = makeAddr("owner"); - address internal nativeConverter = makeAddr("nativeConverter"); - - uint256 stateBeforeInitialize; - - function setUp() public { - // deploy migration manager - migrationManagerImpl = address(new MigrationManager()); - - // deploy mock lxly bridge - lxlyBridge = new MockLxlyBridge(); - - // deploy mock underlying token - underlyingToken = new MockERC20("Underlying Token", "UT"); - - // deploy mock vbToken - vbToken = new MockVbToken(); - vbToken.setUnderlyingToken(address(underlyingToken)); - - stateBeforeInitialize = vm.snapshotState(); - - // initialize migration manager - _initialize(migrationManagerImpl, owner, address(lxlyBridge)); - - vm.label(address(lxlyBridge), "LxlyBridgeX"); - vm.label(address(migrationManager), "Migration Manager"); - vm.label(address(owner), "Owner"); - vm.label(address(underlyingToken), "Underlying Token"); - vm.label(payable(address(vbToken)), "VbToken"); - vm.label(migrationManagerImpl, "Migration Manager Impl"); - vm.label(nativeConverter, "Native Converter"); - } - - function test_setup() public view { - assertEq(address(migrationManager.lxlyBridge()), address(lxlyBridge)); - } - - function test_initialize() public { - vm.revertToState(stateBeforeInitialize); - - bytes memory initData; - initData = abi.encodeCall(MigrationManager.initialize, (address(0), address(lxlyBridge))); - vm.expectRevert(MigrationManager.InvalidOwner.selector); - _initialize(migrationManagerImpl, address(0), address(lxlyBridge)); - - initData = abi.encodeCall(MigrationManager.initialize, (owner, address(0))); - vm.expectRevert(MigrationManager.InvalidLxLyBridge.selector); - _initialize(migrationManagerImpl, owner, address(0)); - } - - function test_configureNativeConverters_reverts() public { - uint32[] memory layerYLxlyIds = new uint32[](1); - layerYLxlyIds[0] = NETWORK_ID_Y; - address[] memory nativeConverters = new address[](1); - nativeConverters[0] = nativeConverter; - - // test pause and unpause - bytes memory callData = abi.encodeCall( - migrationManager.configureNativeConverters, (layerYLxlyIds, nativeConverters, payable(address(vbToken))) - ); - _testPauseUnpause(owner, address(migrationManager), callData); - - // test only callable by the default admin - vm.expectRevert( - abi.encodeWithSelector( - IAccessControl.AccessControlUnauthorizedAccount.selector, - address(this), - migrationManager.DEFAULT_ADMIN_ROLE() - ) - ); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - vm.startPrank(owner); - - // test mismatched inputs: layerYLxlyIds - vm.expectRevert(MigrationManager.NonMatchingInputLengths.selector); - migrationManager.configureNativeConverters(new uint32[](2), nativeConverters, payable(address(vbToken))); - - // test mismatched inputs: nativeConverters - vm.expectRevert(MigrationManager.NonMatchingInputLengths.selector); - migrationManager.configureNativeConverters(layerYLxlyIds, new address[](2), payable(address(vbToken))); - - // test invalid layerYLxlyId - layerYLxlyIds[0] = NETWORK_ID_X; - vm.expectRevert(MigrationManager.InvalidLayerYLxLyId.selector); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - // test invalid native converter - layerYLxlyIds[0] = NETWORK_ID_Y; - nativeConverters[0] = address(0); - vm.expectRevert(MigrationManager.InvalidNativeConverter.selector); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - // test invalid underlying token - nativeConverters[0] = nativeConverter; - vbToken.setUnderlyingToken(address(0)); - vm.expectRevert(MigrationManager.InvalidUnderlyingToken.selector); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - vbToken.setUnderlyingToken(address(underlyingToken)); - - vm.stopPrank(); - } - - function test_configureNativeConverters() public { - uint32[] memory layerYLxlyIds = new uint32[](1); - layerYLxlyIds[0] = NETWORK_ID_Y; - address[] memory nativeConverters = new address[](1); - nativeConverters[0] = nativeConverter; - - // configure native converter - vm.expectEmit(); - emit MigrationManager.NativeConverterConfigured(NETWORK_ID_Y, nativeConverter, (address(vbToken))); - vm.startPrank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - MigrationManager.TokenPair memory tokenPair = - migrationManager.nativeConvertersConfiguration(NETWORK_ID_Y, nativeConverter); - - assertEq(address(tokenPair.vbToken), address(vbToken)); - assertEq(address(tokenPair.underlyingToken), address(underlyingToken)); - assertEq(underlyingToken.allowance(address(migrationManager), address(vbToken)), type(uint256).max); - - // change vbToken - MockERC20 newUnderlyingToken = new MockERC20("New Underlying Token", "NUT"); - MockVbToken newVbToken = new MockVbToken(); - newVbToken.setUnderlyingToken(address(newUnderlyingToken)); - - vm.expectEmit(); - emit MigrationManager.NativeConverterConfigured(NETWORK_ID_Y, nativeConverter, payable(address(newVbToken))); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(newVbToken))); - - tokenPair = migrationManager.nativeConvertersConfiguration(NETWORK_ID_Y, nativeConverter); - - assertEq(address(tokenPair.vbToken), address(newVbToken)); - assertEq(address(tokenPair.underlyingToken), address(newUnderlyingToken)); - assertEq(underlyingToken.allowance(address(migrationManager), payable(address(vbToken))), 0); - assertEq( - newUnderlyingToken.allowance(address(migrationManager), payable(address(newVbToken))), type(uint256).max - ); - - // unset vbToken - vm.expectEmit(); - emit MigrationManager.NativeConverterConfigured(NETWORK_ID_Y, nativeConverter, address(0)); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(0))); - - tokenPair = migrationManager.nativeConvertersConfiguration(NETWORK_ID_Y, nativeConverter); - assertEq(address(tokenPair.vbToken), address(0)); - assertEq(address(tokenPair.underlyingToken), address(0)); - assertEq(newUnderlyingToken.allowance(address(migrationManager), address(newVbToken)), 0); - - vm.stopPrank(); - } - - function test_onMessageReceived_reverts() public { - uint32[] memory layerYLxlyIds = new uint32[](1); - layerYLxlyIds[0] = NETWORK_ID_Y; - address[] memory nativeConverters = new address[](1); - nativeConverters[0] = nativeConverter; - - // test pause and unpause - bytes memory callData = - abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_Y, bytes(""))); - _testPauseUnpause(owner, address(migrationManager), callData); - - // test only callable by the lxly bridge - vm.expectRevert(MigrationManager.Unauthorized.selector); - migrationManager.onMessageReceived(nativeConverter, NETWORK_ID_Y, bytes("")); - - bytes memory data = abi.encode( - MigrationManager.CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, abi.encode(100, 100) - ); - - // test unset vbToken - vm.expectRevert(MigrationManager.Unauthorized.selector); - vm.prank(address(lxlyBridge)); - migrationManager.onMessageReceived(nativeConverter, NETWORK_ID_Y, data); - - vm.prank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - // test unwrapped native token - vm.prank(address(lxlyBridge)); - vm.expectRevert(MigrationManager.CannotWrapGasToken.selector); - migrationManager.onMessageReceived(nativeConverter, NETWORK_ID_Y, data); - - // test wrapped native token with insufficient balance (balance does not match after receiving native token) - MockERC20WithDeposit mockERC20WithDeposit = new MockERC20WithDeposit("Mock ERC20", "MERC20"); - vbToken.setUnderlyingToken(address(mockERC20WithDeposit)); - vm.prank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - deal(address(lxlyBridge), 100); - - bytes memory onMessageReceivedCallData = - abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_Y, data)); - vm.expectRevert( - abi.encodeWithSelector(MigrationManager.InsufficientUnderlyingTokenBalanceAfterWrapping.selector, 0, 100) - ); - vm.prank(address(lxlyBridge)); - (bool _ignored,) = address(migrationManager).call{value: 100}(onMessageReceivedCallData); - _ignored = _ignored; // silence unused variable warning - } - - function test_onMessageReceived_working() public { - uint32[] memory layerYLxlyIds = new uint32[](1); - layerYLxlyIds[0] = NETWORK_ID_Y; - address[] memory nativeConverters = new address[](1); - nativeConverters[0] = nativeConverter; - - vm.prank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - MockERC20WithDeposit mockERC20WithDeposit = new MockERC20WithDeposit("Mock ERC20", "MERC20"); - mockERC20WithDeposit.setCanDeposit(true); - vbToken.setUnderlyingToken(address(mockERC20WithDeposit)); - vm.prank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - - deal(address(lxlyBridge), 100); - - bytes memory data = abi.encode( - MigrationManager.CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, abi.encode(100, 100) - ); - - vm.prank(address(lxlyBridge)); - (bool success,) = address(migrationManager).call{value: 100}( - abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_Y, data)) - ); - assertTrue(success); - - data = abi.encode(MigrationManager.CrossNetworkInstruction.COMPLETE_MIGRATION, abi.encode(100, 100)); - - vm.prank(address(lxlyBridge)); - (success,) = address(migrationManager).call( - abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_Y, data)) - ); - assertTrue(success); - } - - function _initialize(address _migrationManagerImpl, address _owner, address _lxlyBridge) internal { - bytes memory migrationManagerInitData = abi.encodeCall(MigrationManager.initialize, (_owner, _lxlyBridge)); - migrationManager = - MigrationManager(payable(_proxify(address(_migrationManagerImpl), address(this), migrationManagerInitData))); - } - - function _testPauseUnpause(address caller, address callee, bytes memory callData) internal { - vm.startPrank(caller); - (bool success, /* bytes memory data */ ) = callee.call(abi.encodeCall(migrationManager.pause, ())); - - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - (success, /* bytes memory data */ ) = callee.call(callData); - - (success, /* bytes memory data */ ) = callee.call(abi.encodeCall(migrationManager.unpause, ())); - vm.stopPrank(); - } - - function _proxify(address logic, address admin, bytes memory initData) internal returns (address proxy) { - proxy = address(new TransparentUpgradeableProxy(logic, admin, initData)); - } -} diff --git a/test/base/InitializationCounterTestBase.sol b/test/base/InitializationCounterTestBase.sol new file mode 100644 index 00000000..a0f83b20 --- /dev/null +++ b/test/base/InitializationCounterTestBase.sol @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity 0.8.29; + +// Test infrastructure +import {TestConstants} from "test/base/TestConstants.sol"; + +// Core contract +import {InitializationCounterUpgradeable} from "src/etc/InitializationCounterUpgradeable.sol"; + +// Test utilities +import {MockInitializationCounterUpgradeable} from "test/utils/mocks/MockInitializationCounterUpgradeable.sol"; + +/// @title InitializationCounter Test Base +/// @notice Base contract for testing InitializationCounterUpgradeable functionality +/// @dev Provides common setup and utilities for testing initialization counter behavior +abstract contract InitializationCounterTestBase is TestConstants { + // ========= MAIN CONTRACT ========= + MockInitializationCounterUpgradeable internal initCounter; + + // ========= TEST STATE ========= + uint64 internal constant INITIAL_COUNTER_VALUE = 0; + uint64 internal constant FIRST_INCREMENT = 1; + uint64 internal constant SECOND_INCREMENT = 2; + uint64 internal constant THIRD_INCREMENT = 3; + + // ========= SETUP ========= + + /// @notice Deploy InitializationCounter testing infrastructure + /// @dev Sets up the mock contract and verifies initial state + function deployInitializationCounterInfrastructure() internal { + deployInitializationCounter(); + verifyInitializationCounterSetup(); + setupLabels(); + } + + /// @notice Deploy the mock InitializationCounterUpgradeable contract + function deployInitializationCounter() internal { + initCounter = new MockInitializationCounterUpgradeable(); + } + + /// @notice Verify the initial setup of the initialization counter + function verifyInitializationCounterSetup() internal view { + // Verify all counters start at 0 + assertEq(initCounter.globalInitializationCounter(), INITIAL_COUNTER_VALUE); + assertEq(initCounter.localInitializationCounter(), INITIAL_COUNTER_VALUE); + assertEq(initCounter.extensionInitializationCounter(), INITIAL_COUNTER_VALUE); + + // Verify storage slot is correct + assertEq(initCounter.getStorageSlot(), 0x8d679e361eeeac0b879fa197c8b3bda76a3db4f57c9f89335c04a065390bbb00); + } + + /// @notice Set up test labels for better debugging + function setupLabels() internal { + vm.label(address(initCounter), "InitializationCounter"); + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Helper to get all current counter values + /// @return global Current global initialization counter + /// @return local Current local initialization counter + /// @return extension Current extension initialization counter + function getCurrentCounters() internal view returns (uint64 global, uint64 local, uint64 extension) { + global = initCounter.globalInitializationCounter(); + local = initCounter.localInitializationCounter(); + extension = initCounter.extensionInitializationCounter(); + } + + /// @notice Helper to verify counter values match expected values + /// @param expectedGlobal Expected global counter value + /// @param expectedLocal Expected local counter value + /// @param expectedExtension Expected extension counter value + function assertCounterValues(uint64 expectedGlobal, uint64 expectedLocal, uint64 expectedExtension) internal view { + assertEq(initCounter.globalInitializationCounter(), expectedGlobal, "Global counter mismatch"); + assertEq(initCounter.localInitializationCounter(), expectedLocal, "Local counter mismatch"); + assertEq(initCounter.extensionInitializationCounter(), expectedExtension, "Extension counter mismatch"); + } + + /// @notice Helper to simulate successful local counter increment + /// @param expectedValue The expected new value after increment + function incrementLocalCounterSuccessfully(uint64 expectedValue) internal { + initCounter.incrementLocalInitializationCounterWithModifier(expectedValue); + assertEq(initCounter.localInitializationCounter(), expectedValue); + } + + /// @notice Helper to simulate successful global counter increment + /// @param expectedValue The expected new value after increment + function incrementGlobalCounterSuccessfully(uint64 expectedValue) internal { + uint64 returnedValue = initCounter.incrementGlobalInitializationCounter(expectedValue); + assertEq(returnedValue, expectedValue, "Returned value should match expected"); + assertEq(initCounter.globalInitializationCounter(), expectedValue, "Global counter should be updated"); + } + + /// @notice Helper to simulate successful extension counter increment + /// @param requiredLocalValue The required local counter value + /// @param expectedExtensionValue The expected new extension counter value + function incrementExtensionCounterSuccessfully(uint64 requiredLocalValue, uint64 expectedExtensionValue) internal { + initCounter.incrementExtensionInitializationCounterWithModifier(requiredLocalValue, expectedExtensionValue); + assertEq(initCounter.extensionInitializationCounter(), expectedExtensionValue); + } + + /// @notice Helper to test local counter increment failure with wrong expected value + /// @param wrongExpectedValue Incorrect expected value that should cause assertion failure + function expectLocalCounterIncrementFailure(uint64 wrongExpectedValue) internal { + vm.expectRevert(); + initCounter.incrementLocalInitializationCounterWithModifier(wrongExpectedValue); + } + + /// @notice Helper to test global counter increment failure with wrong expected value + /// @param wrongExpectedValue Incorrect expected value that should cause revert + function expectGlobalCounterIncrementFailure(uint64 wrongExpectedValue) internal { + uint64 currentGlobal = initCounter.globalInitializationCounter(); + uint64 actualExpected = currentGlobal + 1; + + vm.expectRevert( + abi.encodeWithSelector( + InitializationCounterUpgradeable.IncorrectInitializationOrder.selector, + wrongExpectedValue, + actualExpected + ) + ); + initCounter.incrementGlobalInitializationCounter(wrongExpectedValue); + } + + /// @notice Helper to test extension counter increment failure with wrong local requirement + /// @param wrongLocalValue Incorrect local counter requirement + /// @param expectedExtensionValue Expected extension value (doesn't matter since local check fails first) + function expectExtensionCounterIncrementFailure(uint64 wrongLocalValue, uint64 expectedExtensionValue) internal { + vm.expectRevert(); + initCounter.incrementExtensionInitializationCounterWithModifier(wrongLocalValue, expectedExtensionValue); + } + + /// @notice Helper to test extension counter increment failure with wrong expected extension value + /// @param correctLocalValue Correct local counter requirement + /// @param wrongExpectedExtensionValue Incorrect expected extension value + function expectExtensionCounterIncrementFailureWithWrongExtension( + uint64 correctLocalValue, + uint64 wrongExpectedExtensionValue + ) internal { + vm.expectRevert(); + initCounter.incrementExtensionInitializationCounterWithModifier(correctLocalValue, wrongExpectedExtensionValue); + } +} diff --git a/test/base/TestConstants.sol b/test/base/TestConstants.sol new file mode 100644 index 00000000..2ce654ef --- /dev/null +++ b/test/base/TestConstants.sol @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import "forge-std/Test.sol"; + +// OpenZeppelin +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Test Constants +/// @notice Centralized constants for Vault Bridge test suites +/// @dev Contains all shared constants used across different test contracts +contract TestConstants is Test { + // ========= NETWORK IDs ========= + uint32 constant NETWORK_ID_X = 0; // mainnet/sepolia (Primary Chain) + uint32 constant NETWORK_ID_Y = 37; // katana-apex (Secondary Chain) + uint32 constant NETWORK_ID_L1 = 0; // Primary Chain + uint32 constant NETWORK_ID_L2 = 1; // Secondary Chain + + // ========= BRIDGE ADDRESSES ========= + address constant LXLY_BRIDGE = 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe; + address constant LXLY_BRIDGE_X = 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582; + address constant LXLY_BRIDGE_Y = 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582; + + // ========= INTEGRATION TEST ADDRESSES ========= + address internal constant BRIDGE_MANAGER = 0xAb3506507449bF1880f3337825efd19ac89E235E; + address constant GER_X = 0xAd1490c248c5d3CbAE399Fd529b79B42984277DF; + address constant GER_Y = 0xa40D5f56745a118D0906a34E69aeC8C0Db1cB8fA; + address constant GER_Y_UPDATER = 0x2caeD842621FF58AaaeC1A06e487d9975F9bFe8A; + address constant ROLLUP_MANAGER = 0x32d33D5137a7cFFb54c5Bf8371172bcEc5f310ff; + + // ========= TOKEN ADDRESSES ========= + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + // ========= BRIDGE CONSTANTS ========= + uint8 constant LEAF_TYPE_ASSET = 0; + uint8 constant LEAF_TYPE_MESSAGE = 1; + + // ========= PERMIT CONSTANTS ========= + bytes32 constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes4 constant PERMIT_SIGNATURE = 0xd505accf; + + // ========= VAULT BRIDGE TOKEN CONSTANTS ========= + uint256 internal constant MAX_RESERVE_PERCENTAGE = 1e18; // 100% + uint256 internal constant MAX_DEPOSIT = 10e18; + uint256 internal constant MAX_WITHDRAW = 10e18; + uint256 internal constant MIGRATION_MANAGER_INITIAL_BALANCE = 1000000e18; + uint256 internal constant MINIMUM_YIELD_VAULT_DEPOSIT = 1e12; + uint256 internal constant YIELD_VAULT_ALLOWED_SLIPPAGE = 1e16; // 1% + bytes32 internal constant RESERVE_ASSET_STORAGE = + hex"f082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44702"; + + // ========= NATIVE CONVERTER CONSTANTS ========= + uint256 constant MAX_NON_MIGRATABLE_BACKING_PERCENTAGE = 1e17; // 10% + uint256 constant MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE = 1e17; // 10% + + // ========= INTEGRATION TEST CONSTANTS ========= + uint256 internal constant MINIMUM_RESERVE_PERCENTAGE = 1e17; // 10% + + // ========= TOKEN NAME AND SYMBOL CONSTANTS ========= + string internal constant UNDERLYING_ASSET_NAME = "Underlying Asset"; + string internal constant UNDERLYING_ASSET_SYMBOL = "UAT"; + uint8 internal constant UNDERLYING_ASSET_DECIMALS = 18; + + string internal constant BW_UNDERLYING_ASSET_NAME = "Bridge Wrapped Underlying Asset"; + string internal constant BW_UNDERLYING_ASSET_SYMBOL = "BWUAT"; + uint8 internal constant BW_UNDERLYING_ASSET_DECIMALS = 18; + + string internal constant VBTOKEN_NAME = "Vault Bridge Token"; + string internal constant VBTOKEN_SYMBOL = "VBTK"; + uint8 internal constant VBTOKEN_DECIMALS = 18; + uint256 internal constant MINIMUM_YIELD_VAULT_DEPOSIT_INTEGRATION = 1e18; + + string internal constant CUSTOM_TOKEN_NAME = "Custom Token"; + string internal constant CUSTOM_TOKEN_SYMBOL = "CT"; + uint8 internal constant CUSTOM_TOKEN_DECIMALS = 18; + + string internal constant BW_VBTOKEN_NAME = "Bridge Wrapped VbToken"; + string internal constant BW_VBTOKEN_SYMBOL = "BWVBTK"; + uint8 internal constant BW_VBTOKEN_DECIMALS = 18; + + // ========= PRIVATE KEYS ========= + uint256 senderPrivateKey = 0xBEEF; + + // ========= ADMIN SLOT ========= + bytes32 constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + // ========= SOVEREIGN BRIDGE BYTECODE ========= + bytes internal constant SOVEREIGN_BRIDGE_BYTECODE = + hex"6101006040523480156200001257600080fd5b5060405162001b6638038062001b6683398101604081905262000035916200028d565b82826003620000458382620003a1565b506004620000548282620003a1565b50503360c0525060ff811660e052466080819052620000739062000080565b60a052506200046d915050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f620000ad6200012e565b805160209182012060408051808201825260018152603160f81b90840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b6060600380546200013f9062000312565b80601f01602080910402602001604051908101604052809291908181526020018280546200016d9062000312565b8015620001be5780601f106200019257610100808354040283529160200191620001be565b820191906000526020600020905b815481529060010190602001808311620001a057829003601f168201915b5050505050905090565b634e487b7160e01b600052604160045260246000fd5b600082601f830112620001f057600080fd5b81516001600160401b03808211156200020d576200020d620001c8565b604051601f8301601f19908116603f01168101908282118183101715620002385762000238620001c8565b816040528381526020925086838588010111156200025557600080fd5b600091505b838210156200027957858201830151818301840152908201906200025a565b600093810190920192909252949350505050565b600080600060608486031215620002a357600080fd5b83516001600160401b0380821115620002bb57600080fd5b620002c987838801620001de565b94506020860151915080821115620002e057600080fd5b50620002ef86828701620001de565b925050604084015160ff811681146200030757600080fd5b809150509250925092565b600181811c908216806200032757607f821691505b6020821081036200034857634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200039c57600081815260208120601f850160051c81016020861015620003775750805b601f850160051c820191505b81811015620003985782815560010162000383565b5050505b505050565b81516001600160401b03811115620003bd57620003bd620001c8565b620003d581620003ce845462000312565b846200034e565b602080601f8311600181146200040d5760008415620003f45750858301515b600019600386901b1c1916600185901b17855562000398565b600085815260208120601f198616915b828110156200043e578886015182559484019460019091019084016200041d565b50858210156200045d5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60805160a05160c05160e0516116aa620004bc6000396000610237015260008181610307015281816105c001526106a70152600061053a015260008181610379015261050401526116aa6000f3fe608060405234801561001057600080fd5b50600436106101775760003560e01c806370a08231116100d8578063a457c2d71161008c578063d505accf11610066578063d505accf1461039b578063dd62ed3e146103ae578063ffa1ad74146103f457600080fd5b8063a457c2d71461034e578063a9059cbb14610361578063cd0d00961461037457600080fd5b806395d89b41116100bd57806395d89b41146102e75780639dc29fac146102ef578063a3c573eb1461030257600080fd5b806370a08231146102915780637ecebe00146102c757600080fd5b806330adf81f1161012f5780633644e515116101145780633644e51514610261578063395093511461026957806340c10f191461027c57600080fd5b806330adf81f14610209578063313ce5671461023057600080fd5b806318160ddd1161016057806318160ddd146101bd57806320606b70146101cf57806323b872dd146101f657600080fd5b806306fdde031461017c578063095ea7b31461019a575b600080fd5b610184610430565b60405161019191906113e4565b60405180910390f35b6101ad6101a8366004611479565b6104c2565b6040519015158152602001610191565b6002545b604051908152602001610191565b6101c17f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f81565b6101ad6102043660046114a3565b6104dc565b6101c17f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c981565b60405160ff7f0000000000000000000000000000000000000000000000000000000000000000168152602001610191565b6101c1610500565b6101ad610277366004611479565b61055c565b61028f61028a366004611479565b6105a8565b005b6101c161029f3660046114df565b73ffffffffffffffffffffffffffffffffffffffff1660009081526020819052604090205490565b6101c16102d53660046114df565b60056020526000908152604090205481565b610184610680565b61028f6102fd366004611479565b61068f565b6103297f000000000000000000000000000000000000000000000000000000000000000081565b60405173ffffffffffffffffffffffffffffffffffffffff9091168152602001610191565b6101ad61035c366004611479565b61075e565b6101ad61036f366004611479565b61082f565b6101c17f000000000000000000000000000000000000000000000000000000000000000081565b61028f6103a9366004611501565b61083d565b6101c16103bc366004611574565b73ffffffffffffffffffffffffffffffffffffffff918216600090815260016020908152604080832093909416825291909152205490565b6101846040518060400160405280600181526020017f310000000000000000000000000000000000000000000000000000000000000081525081565b60606003805461043f906115a7565b80601f016020809104026020016040519081016040528092919081815260200182805461046b906115a7565b80156104b85780601f1061048d576101008083540402835291602001916104b8565b820191906000526020600020905b81548152906001019060200180831161049b57829003601f168201915b5050505050905090565b6000336104d0818585610b73565b60019150505b92915050565b6000336104ea858285610d27565b6104f5858585610dfe565b506001949350505050565b60007f00000000000000000000000000000000000000000000000000000000000000004614610537576105324661106d565b905090565b507f000000000000000000000000000000000000000000000000000000000000000090565b33600081815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff871684529091528120549091906104d090829086906105a3908790611629565b610b73565b3373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614610672576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d4272696467650000000000000000000000000000000060648201526084015b60405180910390fd5b61067c8282611135565b5050565b60606004805461043f906115a7565b3373ffffffffffffffffffffffffffffffffffffffff7f00000000000000000000000000000000000000000000000000000000000000001614610754576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152603060248201527f546f6b656e577261707065643a3a6f6e6c794272696467653a204e6f7420506f60448201527f6c79676f6e5a6b45564d427269646765000000000000000000000000000000006064820152608401610669565b61067c8282611228565b33600081815260016020908152604080832073ffffffffffffffffffffffffffffffffffffffff8716845290915281205490919083811015610822576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f7760448201527f207a65726f0000000000000000000000000000000000000000000000000000006064820152608401610669565b6104f58286868403610b73565b6000336104d0818585610dfe565b834211156108cc576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f546f6b656e577261707065643a3a7065726d69743a204578706972656420706560448201527f726d6974000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8716600090815260056020526040812080547f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9918a918a918a9190866109268361163c565b9091555060408051602081019690965273ffffffffffffffffffffffffffffffffffffffff94851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090506000610991610500565b6040517f19010000000000000000000000000000000000000000000000000000000000006020820152602281019190915260428101839052606201604080517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08184030181528282528051602091820120600080855291840180845281905260ff89169284019290925260608301879052608083018690529092509060019060a0016020604051602081039080840390855afa158015610a55573d6000803e3d6000fd5b50506040517fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0015191505073ffffffffffffffffffffffffffffffffffffffff811615801590610ad057508973ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16145b610b5c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602760248201527f546f6b656e577261707065643a3a7065726d69743a20496e76616c696420736960448201527f676e6174757265000000000000000000000000000000000000000000000000006064820152608401610669565b610b678a8a8a610b73565b50505050505050505050565b73ffffffffffffffffffffffffffffffffffffffff8316610c15576040517f08c379a0000000000000000000000000000000000000000000000000000000008152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f2061646460448201527f72657373000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8216610cb8576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f20616464726560448201527f73730000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff83811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a3505050565b73ffffffffffffffffffffffffffffffffffffffff8381166000908152600160209081526040808320938616835292905220547fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114610df85781811015610deb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601d60248201527f45524332303a20696e73756666696369656e7420616c6c6f77616e63650000006044820152606401610669565b610df88484848403610b73565b50505050565b73ffffffffffffffffffffffffffffffffffffffff8316610ea1576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f20616460448201527f64726573730000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff8216610f44576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201527f65737300000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff831660009081526020819052604090205481811015610ffa576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e742065786365656473206260448201527f616c616e636500000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff848116600081815260208181526040808320878703905593871680835291849020805487019055925185815290927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a3610df8565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f611098610430565b8051602091820120604080518082018252600181527f310000000000000000000000000000000000000000000000000000000000000090840152805192830193909352918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc66060820152608081018390523060a082015260c001604051602081830303815290604052805190602001209050919050565b73ffffffffffffffffffffffffffffffffffffffff82166111b2576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601f60248201527f45524332303a206d696e7420746f20746865207a65726f2061646472657373006044820152606401610669565b80600260008282546111c49190611629565b909155505073ffffffffffffffffffffffffffffffffffffffff8216600081815260208181526040808320805486019055518481527fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef910160405180910390a35050565b73ffffffffffffffffffffffffffffffffffffffff82166112cb576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602160248201527f45524332303a206275726e2066726f6d20746865207a65726f2061646472657360448201527f73000000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff821660009081526020819052604090205481811015611381576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152602260248201527f45524332303a206275726e20616d6f756e7420657863656564732062616c616e60448201527f63650000000000000000000000000000000000000000000000000000000000006064820152608401610669565b73ffffffffffffffffffffffffffffffffffffffff83166000818152602081815260408083208686039055600280548790039055518581529192917fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9101610d1a565b600060208083528351808285015260005b81811015611411578581018301518582016040015282016113f5565b5060006040828601015260407fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0601f8301168501019250505092915050565b803573ffffffffffffffffffffffffffffffffffffffff8116811461147457600080fd5b919050565b6000806040838503121561148c57600080fd5b61149583611450565b946020939093013593505050565b6000806000606084860312156114b857600080fd5b6114c184611450565b92506114cf60208501611450565b9150604084013590509250925092565b6000602082840312156114f157600080fd5b6114fa82611450565b9392505050565b600080600080600080600060e0888a03121561151c57600080fd5b61152588611450565b965061153360208901611450565b95506040880135945060608801359350608088013560ff8116811461155757600080fd5b9699959850939692959460a0840135945060c09093013592915050565b6000806040838503121561158757600080fd5b61159083611450565b915061159e60208401611450565b90509250929050565b600181811c908216806115bb57607f821691505b6020821081036115f4577f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b808201808211156104d6576104d66115fa565b60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361166d5761166d6115fa565b506001019056fea26469706673582212208d88fee561cff7120d381c345cfc534cef8229a272dc5809d4bbb685ad67141164736f6c63430008110033a2646970667358221220ca7a7fd14ec73edf6b3d053ef2133e4a8110a83daf9abb364c35f9d9eb2a083564736f6c63430008140033"; + + /// @notice Test helper to verify pause/unpause functionality + /// @param _caller The address that will call pause/unpause + /// @param _callee The address of the contract to pause/unpause + /// @param _callData The data to call the contract with + function _testPauseUnpause(address _caller, address _callee, bytes memory _callData) internal { + vm.startPrank(_caller); + (bool success, /* bytes memory data */ ) = _callee.call(abi.encodeWithSignature("pause()")); + + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + (success, /* bytes memory data */ ) = _callee.call(_callData); + + (success, /* bytes memory data */ ) = _callee.call(abi.encodeWithSignature("unpause()")); + vm.stopPrank(); + } + + /// @notice Create and deploy a proxy contract + /// @param _implementation The implementation contract address + /// @param _admin The proxy admin address + /// @param _data The initialization data + /// @return The deployed proxy address + function _proxify(address _implementation, address _admin, bytes memory _data) internal returns (address) { + TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(_implementation, _admin, _data); + return address(proxy); + } + + /// @notice Deploy a contract given its name and constructor arguments + /// @param _name The name of the contract to be deployed + /// @param _args The constructor arguments for the contract to be deployed + /// @return deployedAddr The deployed contract address + function deployContract(string memory _name, bytes memory _args) internal returns (address deployedAddr) { + string memory contractPath = string.concat("out/", _name, ".sol/", _name, ".json"); + deployedAddr = deployCode(contractPath, _args); + } +} diff --git a/test/base/primary-chain/MigrationManagerTestBase.sol b/test/base/primary-chain/MigrationManagerTestBase.sol new file mode 100644 index 00000000..4e285f6d --- /dev/null +++ b/test/base/primary-chain/MigrationManagerTestBase.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {IERC20, MockAgglayerBridge, MockERC20, PrimaryChainBase} from "test/base/primary-chain/PrimaryChainBase.sol"; +import {MockWETH} from "test/utils/mocks/MockWETH.sol"; +import {MockVbToken} from "test/utils/mocks/MockVbToken.sol"; + +// Core contracts +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; + +/// @title Migration Manager Test Base +/// @notice Base contract for testing MigrationManager functionality +abstract contract MigrationManagerTestBase is PrimaryChainBase { + // ========= MAIN CONTRACTS ========= + MigrationManager internal migrationManager; + address internal migrationManagerImpl; + + // ========= MOCK CONTRACTS ========= + MockVbToken internal vbToken; + MockWETH internal wrappedGasToken; + + /// @notice Deploy MigrationManager-specific infrastructure + /// @dev Sets up migration manager, mock contracts, and related infrastructure + function deployMigrationManagerInfrastructure() internal { + setupStandardTestAddresses(); + deployMigrationManager(); + verifyMigrationManagerSetup(); + setupLabels(); + } + + /// @notice Deploy MigrationManager with proxy setup + /// @dev Creates a complete MigrationManager deployment ready for testing + function deployMigrationManager() internal { + // Deploy the MigrationManager implementation + migrationManagerImpl = address(new MigrationManager()); + + // Deploy mock agglayer bridge + agglayerBridge = address(new MockAgglayerBridge()); + MockAgglayerBridge(agglayerBridge).setNetworkId(0); + + // Deploy mock underlying token + underlyingToken = address(new MockERC20("Underlying Token", "UT", 18)); + + // Deploy mock vbToken + vbToken = new MockVbToken(); + vbToken.setUnderlyingToken(underlyingToken); + + // Deploy mock wrapped gas token + wrappedGasToken = new MockWETH(); + + // Take snapshot before initialization + stateBeforeInitialize = vm.snapshotState(); + + // Initialize migration manager + bytes memory migrationManagerInitData = + abi.encodeCall(MigrationManager.reinitialize1, (owner, address(agglayerBridge))); + address migrationManagerProxy = _proxify(migrationManagerImpl, address(this), migrationManagerInitData); + migrationManager = MigrationManager(payable(migrationManagerProxy)); + migrationManager.reinitialize2(address(wrappedGasToken)); + } + + /// @notice verify MigrationManager setup + function verifyMigrationManagerSetup() internal view { + assertEq(address(migrationManager.agglayerBridge()), address(agglayerBridge)); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(agglayerBridge), "AgglayerBridge"); + vm.label(address(migrationManager), "Migration Manager"); + vm.label(address(vbToken), "VbToken"); + vm.label(address(wrappedGasToken), "Wrapped Gas Token"); + vm.label(migrationManagerImpl, "Migration Manager Impl"); + vm.label(nativeConverter, "Native Converter"); + vm.label(owner, "Owner"); + vm.label(underlyingToken, "Underlying Token"); + } +} diff --git a/test/base/primary-chain/PrimaryChainBase.sol b/test/base/primary-chain/PrimaryChainBase.sol new file mode 100644 index 00000000..d1b6e53b --- /dev/null +++ b/test/base/primary-chain/PrimaryChainBase.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {TestConstants} from "test/base/TestConstants.sol"; + +// Core contracts +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; +import {VaultBridgeToken} from "src/primary-chain/VaultBridgeToken.sol"; +import {VaultBridgeTokenPart2} from "src/primary-chain/VaultBridgeTokenPart2.sol"; +import {VaultBridgeTokenInitializer} from "src/primary-chain/VaultBridgeTokenInitializer.sol"; + +// Mock contracts +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockERC20} from "test/utils/mocks/MockERC20.sol"; +import {MockVault} from "test/utils/mocks/MockVault.sol"; + +// OpenZeppelin +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title VaultBridgeToken Harness +/// @notice A customizable implementation of VaultBridgeToken for testing purposes +/// @dev This provides the same functionality as GenericVaultBridgeToken +contract TestHarnessVaultBridgeToken is VaultBridgeToken { + constructor() { + _disableInitializers(); + } + + function reinitialize1(address initializer_, VaultBridgeToken.InitializationParameters calldata initParams) + external + whenNotPaused + reinitializer(1) + nonReentrant + { + // Initialize the base implementation. + __VaultBridgeToken_init1(initializer_, initParams); + } + + function reinitialize2() external whenNotPaused reinitializer(2) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __VaultBridgeToken_init2(); + } + + /// @inheritdoc VaultBridgeToken + function _VAULT_BRIDGE_TOKEN_INIT_2_COMPATIBLE() internal pure override {} +} + +/// @title Primary Chain Base +/// @notice Base contract for setting up Primary Chain infrastructure in tests +abstract contract PrimaryChainBase is TestConstants { + // ========= VAULT BRIDGE VERSION ========= + string internal constant VAULT_BRIDGE_PROTOCOL = "1.0.0"; + + // ========= STATE VARIABLES ========= + address internal underlyingToken; + string internal underlyingTokenName; + string internal underlyingTokenSymbol; + uint256 internal migrationManagerInitialBalance; + uint256 internal stateBeforeInitialize; + uint256 internal yieldVaultMaxDeposit; + uint256 internal yieldVaultMaxWithdraw; + uint8 internal underlyingTokenDecimals; + + // ========= ADDRESSES ========= + address internal agglayerBridge; + address internal migrationManagerAddr; + address internal owner; + address internal recipient; + address internal sender; + address internal yieldRecipient; + address internal nativeConverter; + + // ========= VAULT BRIDGE TOKEN COMMON CONTRACTS ========= + VaultBridgeTokenInitializer internal initializer; + VaultBridgeTokenPart2 internal vbTokenPart2Implementation; + MockVault internal yieldVault; + + // ========= VAULT BRIDGE TOKEN METADATA ========= + string internal tokenName; + string internal tokenSymbol; + uint256 internal tokenDecimals; + uint256 internal minimumReservePercentage; + bytes internal tokenMetadata; + string internal version; + + /// @notice Configure Primary Chain infrastructure + function deployPrimaryChainInfrastructure() internal virtual { + // Setup vault bridge version + version = VAULT_BRIDGE_PROTOCOL; + + // Set standard test addresses + setupStandardTestAddresses(); + + // Deploy underlying token (Dynamic deployment based on specified token name) + underlyingToken = deployContract( + underlyingTokenName, abi.encode(underlyingTokenName, underlyingTokenSymbol, underlyingTokenDecimals) + ); + + // Deploy & configure Migration Manager + migrationManagerAddr = address(new MigrationManager()); + fundMigrationManager(migrationManagerAddr, underlyingToken, migrationManagerInitialBalance); + + // Deploy mock bridge for unit tests (vb specific configuration happens in vb test base) + agglayerBridge = address(new MockAgglayerBridge()); + MockAgglayerBridge(agglayerBridge).setNetworkId(0); + + // Set Vault Bridge Token metadata + minimumReservePercentage = minimumReservePercentage; + tokenMetadata = abi.encode(tokenName, tokenSymbol, tokenDecimals); + + // Deploy & configure yield vault + yieldVault = new MockVault(underlyingToken); + yieldVault.setMaxDeposit(yieldVaultMaxDeposit); + yieldVault.setMaxWithdraw(yieldVaultMaxWithdraw); + + // Deploy Vault Bridge Token common parts + initializer = new VaultBridgeTokenInitializer(); + vbTokenPart2Implementation = new VaultBridgeTokenPart2(); + + // Label contracts for easier debugging + setupCommonLabels(); + } + + /// @notice Setup standard test addresses + function setupStandardTestAddresses() internal { + owner = makeAddr("owner"); + recipient = makeAddr("recipient"); + sender = vm.addr(0xBEEF); + yieldRecipient = makeAddr("yieldRecipient"); + nativeConverter = makeAddr("nativeConverter"); + } + + /// @notice Configure Migration Manager + function configureMigrationManager(address _vbToken, uint256 _amount) internal { + require(migrationManagerAddr != address(0), "Migration manager not deployed"); + require(_vbToken != address(0), "VB token address is zero"); + vm.prank(migrationManagerAddr); + IERC20(underlyingToken).approve(_vbToken, _amount); + } + + /// @notice Setup debugging labels + function setupCommonLabels() internal { + vm.label(address(initializer), "Initializer"); + vm.label(address(vbTokenPart2Implementation), "VbToken Part2 Implementation"); + vm.label(address(yieldVault), "Yield Vault"); + vm.label(agglayerBridge, "Mock Agglayer Bridge"); + vm.label(migrationManagerAddr, "Migration Manager Address"); + vm.label(owner, "Owner"); + vm.label(recipient, "Recipient"); + vm.label(sender, "Sender"); + vm.label(underlyingToken, underlyingTokenName); + vm.label(yieldRecipient, "Yield Recipient"); + } + + /// @notice Fund migration manager with assets for testing + /// @dev Generalized function that works with any token type + /// @param _migrationManagerAddress The migration manager address to fund + /// @param _tokenAddress The token address to fund with + /// @param _amount The amount to fund (default: 10M tokens) + function fundMigrationManager(address _migrationManagerAddress, address _tokenAddress, uint256 _amount) + internal + virtual + { + deal(_tokenAddress, _migrationManagerAddress, _amount); + vm.prank(_migrationManagerAddress); + IERC20(_tokenAddress).approve(_migrationManagerAddress, _amount); + } + + /// @notice Calculate reserve assets based on deposit amount and vault limits + /// @param _amount The total amount being deposited + /// @param _vaultMaxDeposit The maximum amount the yield vault can accept + /// @return reserveAssets The amount that will be kept in reserve + function calculateReserveAssets(uint256 _amount, uint256 _vaultMaxDeposit) internal view returns (uint256) { + uint256 reserveAssets = (_amount * minimumReservePercentage) / MAX_RESERVE_PERCENTAGE; + uint256 assetsToDeposit = _amount - reserveAssets; + uint256 assetsToDepositMax = (assetsToDeposit > _vaultMaxDeposit) ? _vaultMaxDeposit : assetsToDeposit; + return _amount - assetsToDepositMax; + } +} diff --git a/test/base/primary-chain/VaultBridgeTokenTestBase.sol b/test/base/primary-chain/VaultBridgeTokenTestBase.sol new file mode 100644 index 00000000..f9e5bea7 --- /dev/null +++ b/test/base/primary-chain/VaultBridgeTokenTestBase.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import { + IERC20, + MockERC20, + MockVault, + PrimaryChainBase, + TestHarnessVaultBridgeToken, + VaultBridgeTokenPart2 +} from "test/base/primary-chain/PrimaryChainBase.sol"; + +// Core contracts +import {VaultBridgeToken} from "src/primary-chain/VaultBridgeToken.sol"; + +/// @title VaultBridgeToken Test Base +/// @notice Base contract for testing VaultBridgeToken and VaultBridgeTokenPart2 as standalone contracts +abstract contract VaultBridgeTokenTestBase is PrimaryChainBase { + // ========= MAIN CONTRACTS ========= + TestHarnessVaultBridgeToken internal vbToken; + VaultBridgeTokenPart2 internal vbTokenPart2; + address internal vbTokenImplementation; + + /// @notice Deploy VaultBridgeToken-specific infrastructure + /// @dev Sets up token, yield vault, and related contracts + function deployVaultBridgeTokenInfrastructure() internal { + tokenName = "Vault Bridge Token"; + tokenSymbol = "vbTOKEN"; + tokenDecimals = 6; + underlyingTokenName = "MockERC20"; + underlyingTokenSymbol = "mERC20"; + underlyingTokenDecimals = 6; + minimumReservePercentage = MINIMUM_RESERVE_PERCENTAGE; + migrationManagerInitialBalance = MIGRATION_MANAGER_INITIAL_BALANCE; + yieldVaultMaxDeposit = MAX_DEPOSIT; + yieldVaultMaxWithdraw = MAX_WITHDRAW; + + deployPrimaryChainInfrastructure(); + deployVaultBridgeToken(); + configureMigrationManager(address(vbToken), migrationManagerInitialBalance); + verifyVaultBridgeTokenSetup(); + setupLabels(); + } + + /// @notice Deploy VaultBridgeToken harness with proxy setup + /// @dev Creates a complete VaultBridgeToken deployment ready for testing + function deployVaultBridgeToken() internal { + // Deploy the VaultBridgeToken implementation + vbTokenImplementation = address(new TestHarnessVaultBridgeToken()); + + // Take snapshot before initialization + stateBeforeInitialize = vm.snapshotState(); + + // Get initialization parameters + VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ + owner: owner, + name: tokenName, + symbol: tokenSymbol, + underlyingToken: underlyingToken, + minimumReservePercentage: MINIMUM_RESERVE_PERCENTAGE, + yieldVault: address(yieldVault), + yieldRecipient: yieldRecipient, + agglayerBridge: agglayerBridge, + minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, + migrationManager: migrationManagerAddr, + yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, + vaultBridgeTokenPart2: address(vbTokenPart2Implementation) + }); + + // Prepare initialization data + // Initialize the proxy through the reinitialize1 function + bytes memory initData = abi.encodeCall( + TestHarnessVaultBridgeToken(vbTokenImplementation).reinitialize1, (address(initializer), initParams) + ); + + // Deploy proxy and initialize + address vbTokenProxy = _proxify(vbTokenImplementation, address(this), initData); + vbToken = TestHarnessVaultBridgeToken(payable(vbTokenProxy)); + vbToken.reinitialize2(); + + // Set vbTokenPart2 to point to the proxy (delegation pattern) + vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); + } + + /// @notice Helper to verify basic VaultBridgeToken setup + function verifyVaultBridgeTokenSetup() internal view { + assertEq(vbToken.allowance(address(vbToken), agglayerBridge), type(uint256).max); + assertEq(vbToken.asset(), underlyingToken); + assertEq(vbToken.decimals(), tokenDecimals); + assertEq(vbToken.migrationManager(), migrationManagerAddr); + assertEq(vbToken.minimumReservePercentage(), minimumReservePercentage); + assertEq(vbToken.name(), tokenName); + assertEq(vbToken.symbol(), tokenSymbol); + assertEq(vbToken.yieldRecipient(), yieldRecipient); + assertEq(address(vbToken.yieldVault()), address(yieldVault)); + assertEq(address(vbToken.agglayerBridge()), agglayerBridge); + assertTrue(vbToken.hasRole(vbToken.DEFAULT_ADMIN_ROLE(), owner)); + assertEq(IERC20(underlyingToken).allowance(address(vbToken), address(yieldVault)), type(uint256).max); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(vbToken), "VaultBridgeToken (Proxy)"); + vm.label(vbTokenImplementation, "VaultBridgeToken Implementation"); + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Helper to deal tokens and approve spending + /// @param _token The token to deal + /// @param _to The recipient address + /// @param _amount The amount to deal + /// @param _spender The address to approve for spending + function _dealAndApprove(address _token, address _to, uint256 _amount, address _spender) internal { + deal(underlyingToken, _to, _amount); + vm.prank(_to); + IERC20(_token).approve(_spender, _amount); + } + + /// @notice Calculate the withdrawable amount from the vault + /// @param _amount The total amount to withdraw + /// @return The maximum amount that can be withdrawn + function _calculateWithdrawableAmount(uint256 _amount) internal view returns (uint256) { + return _amount - vbToken.reservedAssets() > MAX_WITHDRAW ? MAX_WITHDRAW : _amount - vbToken.reservedAssets(); + } +} diff --git a/test/base/primary-chain/VbETHTestBase.sol b/test/base/primary-chain/VbETHTestBase.sol new file mode 100644 index 00000000..27338be4 --- /dev/null +++ b/test/base/primary-chain/VbETHTestBase.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import { + IERC20, + MockAgglayerBridge, + MockVault, + PrimaryChainBase, + VaultBridgeTokenPart2 +} from "test/base/primary-chain/PrimaryChainBase.sol"; + +// Core contracts +import {VbEth} from "src/primary-chain/ethereum/vbETH/VbETH.sol"; +import {VaultBridgeToken} from "src/primary-chain/VaultBridgeToken.sol"; + +// Mock contracts +import {MockWETH} from "test/utils/mocks/MockWETH.sol"; + +/// @title VbEth Test Base +/// @notice Base contract for testing VbEth +abstract contract VbETHTestBase is PrimaryChainBase { + // ========= MAIN CONTRACTS ========= + VbEth internal vbETH; + VaultBridgeTokenPart2 internal vbETHPart2; + address internal vbETHImplementation; + + /// @notice Deploy Vault Bridge ETH-specific infrastructure + /// @dev Sets up token, yield vault, and related contracts + function deployVbETHInfrastructure() internal virtual { + tokenName = "Vault Bridge ETH"; + tokenSymbol = "vbETH"; + tokenDecimals = 18; + underlyingTokenName = "MockWETH"; + underlyingTokenSymbol = "mWETH"; + underlyingTokenDecimals = 18; + minimumReservePercentage = MINIMUM_RESERVE_PERCENTAGE; + migrationManagerInitialBalance = MIGRATION_MANAGER_INITIAL_BALANCE; + yieldVaultMaxDeposit = MAX_DEPOSIT; + yieldVaultMaxWithdraw = MAX_WITHDRAW; + + deployPrimaryChainInfrastructure(); + configureAgglayerBridge(); + deployVaultBridgeToken(); + configureMigrationManager(address(vbETH), migrationManagerInitialBalance); + verifyVbETHSetup(); + setupLabels(); + } + + /// @notice Configure the Agglayer Bridge mock + function configureAgglayerBridge() internal { + MockAgglayerBridge(agglayerBridge).setGasTokenAddress(address(0)); + MockAgglayerBridge(agglayerBridge).setGasTokenNetwork(0); + } + + /// @notice Deploy VbEth implementation and proxy + function deployVaultBridgeToken() internal { + // Deploy VbEth implementation + vbETHImplementation = address(new VbEth()); + + // Take snapshot before initialization + stateBeforeInitialize = vm.snapshotState(); + + // Create initialization parameters + VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ + owner: owner, + name: tokenName, + symbol: tokenSymbol, + underlyingToken: underlyingToken, + minimumReservePercentage: minimumReservePercentage, + yieldVault: address(yieldVault), + yieldRecipient: yieldRecipient, + agglayerBridge: agglayerBridge, + minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, + migrationManager: migrationManagerAddr, + yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, + vaultBridgeTokenPart2: address(vbTokenPart2Implementation) + }); + + // Prepare initialization data + bytes memory initData = + abi.encodeCall(VbEth(vbETHImplementation).reinitialize1, (address(initializer), initParams)); + + // Deploy proxy and initialize + address vbETHProxy = _proxify(vbETHImplementation, address(this), initData); + vbETH = VbEth(payable(vbETHProxy)); + + // Set vbETHPart2 to point to the proxy (delegation pattern) + vbETHPart2 = VaultBridgeTokenPart2(payable(address(vbETH))); + } + + /// @notice Helper to verify basic VbEth setup + function verifyVbETHSetup() internal view { + assertEq(vbETH.allowance(address(vbETH), agglayerBridge), type(uint256).max); + assertEq(vbETH.asset(), underlyingToken); + assertEq(vbETH.balanceOf(address(this)), 0); + assertEq(vbETH.decimals(), tokenDecimals); + assertEq(vbETH.migrationManager(), migrationManagerAddr); + assertEq(vbETH.minimumReservePercentage(), minimumReservePercentage); + assertEq(vbETH.name(), tokenName); + assertEq(vbETH.symbol(), tokenSymbol); + assertEq(vbETH.totalSupply(), 0); + assertEq(vbETH.yieldRecipient(), yieldRecipient); + assertTrue(vbETH.hasRole(vbETH.DEFAULT_ADMIN_ROLE(), owner)); + assertEq(address(vbETH.yieldVault()), address(yieldVault)); + assertEq(address(vbETH.agglayerBridge()), agglayerBridge); + assertEq(IERC20(underlyingToken).allowance(address(vbETH), address(yieldVault)), type(uint256).max); + // Validate the gas token constraints for VbEth + assertEq(MockAgglayerBridge(agglayerBridge).gasTokenAddress(), address(0)); + assertEq(MockAgglayerBridge(agglayerBridge).gasTokenNetwork(), 0); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(vbETH), "vbETH (Proxy)"); + vm.label(vbETHImplementation, "vbETH Implementation"); + } + + // ========= HELPER FUNCTIONS ========= + + /// @notice Helper function to deal WETH directly to an address + /// @param _to Address to send WETH to + /// @param _amount Amount of WETH to provide + function _dealWETH(address _to, uint256 _amount) internal { + deal(underlyingToken, _to, _amount); + } +} diff --git a/test/base/secondary-chain/CustomTokenTestBase.sol b/test/base/secondary-chain/CustomTokenTestBase.sol new file mode 100644 index 00000000..93ba34c1 --- /dev/null +++ b/test/base/secondary-chain/CustomTokenTestBase.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import { + MockERC20Upgradeable, + SecondaryChainBase, + TestHarnessCustomToken +} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Custom Token Test Base +/// @notice Base contract for testing CustomToken as a standalone contract +abstract contract CustomTokenTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + TestHarnessCustomToken internal customTokenHarness; + address internal customTokenImpl; + TransparentUpgradeableProxy existingCustomTokenProxy; + + /// @notice Deploy CustomToken-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for CustomToken testing + function deployCustomTokenInfrastructure() internal { + customTokenName = "Custom Token"; + customTokenSymbol = "cTKN"; + customTokenDecimals = 18; + + deploySecondaryChainInfrastructure(); + deployCustomToken(); + verifyCustomTokenSetup(); + setupLabels(); + } + + /// @notice Deploy Custom Token and related contracts + function deployCustomToken() internal { + MockERC20Upgradeable existingCustomTokenImpl = new MockERC20Upgradeable(); + existingCustomTokenProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingCustomTokenImpl), + proxyAdmin, + abi.encodeCall(MockERC20Upgradeable.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + customTokenImpl = address(new TestHarnessCustomToken()); + stateBeforeInitialize = vm.snapshotState(); + + bytes memory customTokenInitData = abi.encodeCall( + TestHarnessCustomToken.reinitialize2, + (owner, customTokenDecimals, address(mockAgglayerBridge), dummyNativeConverter) + ); + + bytes memory customTokenUpgradeData = + abi.encodeCall(ITransparentUpgradeableProxy.upgradeToAndCall, (customTokenImpl, customTokenInitData)); + + vm.prank(_getProxyAdmin(address(existingCustomTokenProxy))); + (address(existingCustomTokenProxy).call(customTokenUpgradeData)); + + customTokenHarness = TestHarnessCustomToken(address(existingCustomTokenProxy)); + customTokenHarness.reinitialize3(); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(customToken), "CustomToken"); + vm.label(address(customTokenImpl), "CustomTokenImplementation"); + } + + /// @notice Helper to verify basic CustomToken setup + function verifyCustomTokenSetup() internal view { + assertEq(customTokenHarness.name(), customTokenName); + assertEq(customTokenHarness.symbol(), customTokenSymbol); + assertEq(customTokenHarness.decimals(), customTokenDecimals); + assertEq(customTokenHarness.bridge(), address(mockAgglayerBridge)); + assertEq(customTokenHarness.nativeConverter(), dummyNativeConverter); + assertTrue(customTokenHarness.hasRole(customTokenHarness.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(customTokenHarness.hasRole(customTokenHarness.PAUSER_ROLE(), owner)); + assertEq(customTokenHarness.totalSupply(), 0); + assertFalse(customTokenHarness.paused()); + } +} diff --git a/test/base/secondary-chain/GenericCustomTokenAgglayerTestBase.sol b/test/base/secondary-chain/GenericCustomTokenAgglayerTestBase.sol new file mode 100644 index 00000000..5a8f9c20 --- /dev/null +++ b/test/base/secondary-chain/GenericCustomTokenAgglayerTestBase.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {MockERC20Upgradeable, SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {GenericCustomTokenAgglayer} from "src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Generic Custom Token Agglayer Test Base +/// @notice Base contract for testing GenericCustomTokenAgglayer as a standalone contract +abstract contract GenericCustomTokenAgglayerTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + GenericCustomTokenAgglayer internal genericCustomTokenAgglayer; + address internal genericCustomTokenAgglayerImpl; + TransparentUpgradeableProxy existingGenericCustomTokenAgglayerProxy; + + /// @notice Deploy GenericCustomTokenAgglayer-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for GenericCustomTokenAgglayer testing + function deployGenericCustomTokenAgglayerInfrastructure() internal { + customTokenName = "Generic Custom Token Agglayer"; + customTokenSymbol = "gcTKN"; + customTokenDecimals = 18; + + deploySecondaryChainInfrastructure(); + deployGenericCustomTokenAgglayer(); + verifyGenericCustomTokenAgglayerSetup(); + setupLabels(); + } + + /// @notice Deploy Generic Custom Token Agglayer and related contracts + function deployGenericCustomTokenAgglayer() internal { + MockERC20Upgradeable existingGenericCustomTokenAgglayerImpl = new MockERC20Upgradeable(); + existingGenericCustomTokenAgglayerProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingGenericCustomTokenAgglayerImpl), + proxyAdmin, + abi.encodeCall(MockERC20Upgradeable.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + genericCustomTokenAgglayerImpl = address(new GenericCustomTokenAgglayer()); + stateBeforeInitialize = vm.snapshotState(); + + bytes memory genericCustomTokenAgglayerInitData = abi.encodeCall( + GenericCustomTokenAgglayer.reinitialize2, + (owner, customTokenDecimals, address(mockAgglayerBridge), dummyNativeConverter) + ); + + bytes memory genericCustomTokenAgglayerUpgradeData = abi.encodeCall( + ITransparentUpgradeableProxy.upgradeToAndCall, + (genericCustomTokenAgglayerImpl, genericCustomTokenAgglayerInitData) + ); + + vm.prank(_getProxyAdmin(address(existingGenericCustomTokenAgglayerProxy))); + (address(existingGenericCustomTokenAgglayerProxy).call(genericCustomTokenAgglayerUpgradeData)); + + genericCustomTokenAgglayer = GenericCustomTokenAgglayer(address(existingGenericCustomTokenAgglayerProxy)); + genericCustomTokenAgglayer.reinitialize3(); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(genericCustomTokenAgglayer), "GenericCustomTokenAgglayer"); + vm.label(address(genericCustomTokenAgglayerImpl), "GenericCustomTokenAgglayerImplementation"); + } + + /// @notice Helper to verify basic GenericCustomTokenAgglayer setup + function verifyGenericCustomTokenAgglayerSetup() internal view { + assertEq(genericCustomTokenAgglayer.name(), customTokenName); + assertEq(genericCustomTokenAgglayer.symbol(), customTokenSymbol); + assertEq(genericCustomTokenAgglayer.decimals(), customTokenDecimals); + assertEq(genericCustomTokenAgglayer.bridge(), address(mockAgglayerBridge)); + assertEq(genericCustomTokenAgglayer.nativeConverter(), dummyNativeConverter); + assertTrue(genericCustomTokenAgglayer.hasRole(genericCustomTokenAgglayer.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(genericCustomTokenAgglayer.hasRole(genericCustomTokenAgglayer.PAUSER_ROLE(), owner)); + assertEq(genericCustomTokenAgglayer.totalSupply(), 0); + assertFalse(genericCustomTokenAgglayer.paused()); + } +} diff --git a/test/base/secondary-chain/GenericCustomTokenPolygonTestBase.sol b/test/base/secondary-chain/GenericCustomTokenPolygonTestBase.sol new file mode 100644 index 00000000..64b857b9 --- /dev/null +++ b/test/base/secondary-chain/GenericCustomTokenPolygonTestBase.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {GenericCustomTokenPolygon} from "src/secondary-chain/polygon/GenericCustomTokenPolygon.sol"; + +// OpenZeppelin +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Generic Custom Token Polygon Test Base +/// @notice Base contract for testing GenericCustomTokenPolygon as a standalone contract +abstract contract GenericCustomTokenPolygonTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + GenericCustomTokenPolygon internal genericCustomTokenPolygon; + address internal genericCustomTokenPolygonImpl; + TransparentUpgradeableProxy existingGenericCustomTokenPolygonProxy; + + // ========= VARIABLES ========= + address internal childChainManager = makeAddr("ChildChainManager"); + uint8 internal originalUnderlyingTokenDecimals = 18; + + /// @notice Deploy GenericCustomTokenPolygon-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for GenericCustomTokenPolygon testing + function deployGenericCustomTokenPolygonInfrastructure() internal { + customTokenName = "Generic Custom Token Polygon"; + customTokenSymbol = "gcTKN"; + customTokenDecimals = 18; + + deploySecondaryChainInfrastructure(); + deployGenericCustomTokenPolygon(); + verifyGenericCustomTokenPolygonSetup(); + setupLabels(); + } + + /// @notice Deploy Generic Custom Token Polygon and related contracts + function deployGenericCustomTokenPolygon() internal { + genericCustomTokenPolygonImpl = address(new GenericCustomTokenPolygon()); + + existingGenericCustomTokenPolygonProxy = TransparentUpgradeableProxy( + payable( + _proxify( + genericCustomTokenPolygonImpl, + proxyAdmin, + abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, + (owner, customTokenName, customTokenSymbol, originalUnderlyingTokenDecimals, childChainManager) + ) + ) + ) + ); + + genericCustomTokenPolygon = GenericCustomTokenPolygon(address(existingGenericCustomTokenPolygonProxy)); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(genericCustomTokenPolygon), "GenericCustomTokenPolygon"); + vm.label(address(genericCustomTokenPolygonImpl), "GenericCustomTokenPolygonImplementation"); + vm.label(childChainManager, "MockChildChainManager"); + } + + /// @notice Helper to verify basic GenericCustomTokenPolygon setup + function verifyGenericCustomTokenPolygonSetup() internal view { + assertEq(genericCustomTokenPolygon.name(), customTokenName); + assertEq(genericCustomTokenPolygon.symbol(), customTokenSymbol); + assertEq(genericCustomTokenPolygon.decimals(), customTokenDecimals); + assertEq(genericCustomTokenPolygon.bridge(), childChainManager); + assertTrue(genericCustomTokenPolygon.hasRole(genericCustomTokenPolygon.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(genericCustomTokenPolygon.hasRole(genericCustomTokenPolygon.PAUSER_ROLE(), owner)); + assertEq(genericCustomTokenPolygon.totalSupply(), 0); + assertFalse(genericCustomTokenPolygon.paused()); + } + + /// @notice Helper function to simulate deposit through ChildChainManager + /// @param user The user to deposit tokens for + /// @param amount The amount to deposit + function simulateDeposit(address user, uint256 amount) internal { + bytes memory depositData = abi.encode(amount); + vm.prank(childChainManager); + genericCustomTokenPolygon.deposit(user, depositData); + } + + /// @notice Helper function to simulate withdraw by user + /// @param user The user withdrawing tokens + /// @param amount The amount to withdraw + function simulateWithdraw(address user, uint256 amount) internal { + vm.prank(user); + genericCustomTokenPolygon.withdraw(amount); + } +} diff --git a/test/base/secondary-chain/GenericCustomTokenWormholeTestBase.sol b/test/base/secondary-chain/GenericCustomTokenWormholeTestBase.sol new file mode 100644 index 00000000..c1791c09 --- /dev/null +++ b/test/base/secondary-chain/GenericCustomTokenWormholeTestBase.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {GenericCustomTokenWormhole} from "src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Generic Custom Token Wormhole Test Base +/// @notice Base contract for testing GenericCustomTokenWormhole as a standalone contract +abstract contract GenericCustomTokenWormholeTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + GenericCustomTokenWormhole internal genericCustomTokenWormhole; + address internal genericCustomTokenWormholeImpl; + TransparentUpgradeableProxy existingGenericCustomTokenWormholeProxy; + + // ========= VARIABLES ========= + address internal nttManager = makeAddr("NttManager"); + uint8 internal originalUnderlyingTokenDecimals = 18; + + /// @notice Deploy GenericCustomTokenWormhole-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for GenericCustomTokenWormhole testing + function deployGenericCustomTokenWormholeInfrastructure() internal { + customTokenName = "Generic Custom Token Wormhole"; + customTokenSymbol = "gcTKN"; + customTokenDecimals = 18; + + deploySecondaryChainInfrastructure(); + deployGenericCustomTokenWormhole(); + verifyGenericCustomTokenWormholeSetup(); + setupLabels(); + } + + /// @notice Deploy Generic Custom Token Wormhole and related contracts + function deployGenericCustomTokenWormhole() internal { + genericCustomTokenWormholeImpl = address(new GenericCustomTokenWormhole()); + + existingGenericCustomTokenWormholeProxy = TransparentUpgradeableProxy( + payable( + _proxify( + genericCustomTokenWormholeImpl, + proxyAdmin, + abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (owner, customTokenName, customTokenSymbol, originalUnderlyingTokenDecimals, nttManager) + ) + ) + ) + ); + + genericCustomTokenWormhole = GenericCustomTokenWormhole(address(existingGenericCustomTokenWormholeProxy)); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(genericCustomTokenWormhole), "GenericCustomTokenWormhole"); + vm.label(address(genericCustomTokenWormholeImpl), "GenericCustomTokenWormholeImplementation"); + vm.label(nttManager, "MockNttManager"); + } + + /// @notice Helper to verify basic GenericCustomTokenWormhole setup + function verifyGenericCustomTokenWormholeSetup() internal view { + assertEq(genericCustomTokenWormhole.name(), customTokenName); + assertEq(genericCustomTokenWormhole.symbol(), customTokenSymbol); + assertEq(genericCustomTokenWormhole.decimals(), customTokenDecimals); + assertEq(genericCustomTokenWormhole.bridge(), nttManager); + assertTrue(genericCustomTokenWormhole.hasRole(genericCustomTokenWormhole.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(genericCustomTokenWormhole.hasRole(genericCustomTokenWormhole.PAUSER_ROLE(), owner)); + assertEq(genericCustomTokenWormhole.totalSupply(), 0); + assertFalse(genericCustomTokenWormhole.paused()); + } +} diff --git a/test/base/secondary-chain/NativeConverterTestBase.sol b/test/base/secondary-chain/NativeConverterTestBase.sol new file mode 100644 index 00000000..8a15f659 --- /dev/null +++ b/test/base/secondary-chain/NativeConverterTestBase.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import { + MockERC20Upgradeable, + SecondaryChainBase, + TestHarnessNativeConverter, + TestHarnessCustomToken +} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {NativeConverter} from "src/secondary-chain/NativeConverter.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title Native Converter Test Base +/// @notice Base contract for testing NativeConverter as a standalone contract +abstract contract NativeConverterTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + TestHarnessNativeConverter internal nativeConverter; + address internal nativeConverterImpl; + + /// @notice Deploy NativeConverter-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for NativeConverter testing + function deployNativeConverterInfrastructure() internal { + underlyingTokenName = "Underlying Token"; + underlyingTokenSymbol = "uTKN"; + underlyingTokenDecimals = 18; + customTokenName = "Custom Token"; + customTokenSymbol = "cTKN"; + customTokenDecimals = 18; + maxNonMigratableBackingPercentage = MAX_NON_MIGRATABLE_BACKING_PERCENTAGE; + primaryChainAgglayerId = NETWORK_ID_L1; + + deploySecondaryChainInfrastructure(); + deployNativeConverter(); + verifyNativeConverterSetup(); + setupLabels(); + } + + /// @notice Deploy Native Converter and related contracts + /// @dev This includes deploying the Custom Token and initializing both contracts + function deployNativeConverter() internal { + // Set underlying and custom token addresses + underlyingToken = new MockERC20Upgradeable(); + underlyingToken.initialize(underlyingTokenName, underlyingTokenSymbol); + underlyingTokenMetadata = abi.encode(underlyingTokenName, underlyingTokenSymbol, underlyingTokenDecimals); + + MockERC20Upgradeable existingCustomTokenImpl = new MockERC20Upgradeable(); + TransparentUpgradeableProxy existingCustomTokenProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingCustomTokenImpl), + proxyAdmin, + abi.encodeCall(MockERC20Upgradeable.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + TestHarnessCustomToken genericCustomTokenImpl = new TestHarnessCustomToken(); + + calculatedNativeConverter = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); + + bytes memory customTokenInitData = abi.encodeCall( + TestHarnessCustomToken.reinitialize2, + (proxyAdmin, customTokenDecimals, address(mockAgglayerBridge), calculatedNativeConverter) + ); + bytes memory customTokenUpgradeData = abi.encodeCall( + ITransparentUpgradeableProxy.upgradeToAndCall, (address(genericCustomTokenImpl), customTokenInitData) + ); + vm.prank(proxyAdmin); + (address(existingCustomTokenProxy).call(customTokenUpgradeData)); + + // assign variables for generic testing + customToken = MockERC20Upgradeable(address(existingCustomTokenProxy)); + + nativeConverterImpl = address(new TestHarnessNativeConverter()); + + stateBeforeInitialize = vm.snapshotState(); + + bytes memory initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + nativeConverter = TestHarnessNativeConverter(_proxify(nativeConverterImpl, proxyAdmin, initData)); + assertEq(address(nativeConverter), calculatedNativeConverter); + + vm.prank(address(nativeConverter)); + underlyingToken.approve(address(mockAgglayerBridge), type(uint256).max); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(customToken), "CustomToken"); + vm.label(address(nativeConverter), "NativeConverter"); + vm.label(address(underlyingToken), "UnderlyingToken"); + } + + /// @notice Helper to verify basic NativeConverter setup + function verifyNativeConverterSetup() internal view { + assertEq(address(nativeConverter.bridge()), address(mockAgglayerBridge)); + assertEq(address(nativeConverter.customToken()), address(customToken)); + assertEq(address(nativeConverter.migrationManager()), migrationManager); + assertEq(address(nativeConverter.underlyingToken()), address(underlyingToken)); + assertEq(nativeConverter.agglayerId(), NETWORK_ID_L2); + assertEq(nativeConverter.nonMigratableBackingPercentage(), maxNonMigratableBackingPercentage); + assertEq(nativeConverter.primaryChainAgglayerId(), primaryChainAgglayerId); + assertTrue(nativeConverter.hasRole(nativeConverter.DEFAULT_ADMIN_ROLE(), owner)); + } +} diff --git a/test/base/secondary-chain/SecondaryChainBase.sol b/test/base/secondary-chain/SecondaryChainBase.sol new file mode 100644 index 00000000..b3472aba --- /dev/null +++ b/test/base/secondary-chain/SecondaryChainBase.sol @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {TestConstants} from "test/base/TestConstants.sol"; + +// Core contracts +import {NativeConverter} from "src/secondary-chain/NativeConverter.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; + +// Mock contracts +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockERC20Upgradeable} from "test/utils/mocks/MockERC20Upgradeable.sol"; +import {MockGlobalExitRootManager} from "test/utils/mocks/MockGlobalExitRootManager.sol"; + +// OpenZeppelin +import {ERC20Upgradeable} from "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; + +/// @title Test Harness for CustomToken +/// @notice A test harness that extends CustomToken to expose reinitialization functions for testing +contract TestHarnessCustomToken is CustomToken { + constructor() { + _disableInitializers(); + } + + function reinitialize2( + address owner_, + uint8 originalUnderlyingTokenDecimals_, + address agglayerBridge_, + address nativeConverter_ + ) external whenNotPaused reinitializer(2) nonReentrant { + string memory name_ = ERC20Upgradeable.name(); + string memory symbol_ = ERC20Upgradeable.symbol(); + __CustomToken_init1(owner_, name_, symbol_, originalUnderlyingTokenDecimals_, agglayerBridge_, nativeConverter_); + } + + function reinitialize3() external whenNotPaused reinitializer(3) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + _incrementGlobalInitializationCounter(3); + + __CustomToken_init2(); + } + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_INIT_2_COMPATIBLE() internal pure override {} + + /// @inheritdoc CustomToken + function _CUSTOM_TOKEN_IS_MINTABLE_BURNABLE() internal override {} +} + +/// @title Test Harness for NativeConverter +/// @notice A test harness that extends NativeConverter to expose reinitialization functions for testing +contract TestHarnessNativeConverter is NativeConverter { + constructor() { + _disableInitializers(); + } + + function reinitialize1( + address owner_, + address customToken_, + address underlyingToken_, + address agglayerBridge_, + uint32 primaryChainAgglayerId_, + uint256 nonMigratableBackingPercentage_, + address migrationManager_ + ) external whenNotPaused reinitializer(1) nonReentrant { + __NativeConverter_init1( + owner_, + customToken_, + underlyingToken_, + agglayerBridge_, + primaryChainAgglayerId_, + nonMigratableBackingPercentage_, + migrationManager_ + ); + } + + function reinitialize2() external whenNotPaused reinitializer(2) nonReentrant { + _incrementGlobalInitializationCounter(1); + _incrementGlobalInitializationCounter(2); + + __NativeConverter_init2(); + } + + /// @inheritdoc NativeConverter + function _NATIVE_CONVERTER_INIT_2_COMPATIBLE() internal pure override {} + + function _mintCustomToken(address to, uint256 amount) internal override { + MockERC20Upgradeable(address(customToken())).mint(to, amount); + } + + function _burnCustomToken(address from, uint256 amount) internal override { + MockERC20Upgradeable(address(customToken())).burn(from, amount); + } +} + +/// @title Secondary Chain Base +/// @notice Base contract for setting up Secondary Chain infrastructure in tests +abstract contract SecondaryChainBase is TestConstants { + // ========= VAULT BRIDGE VERSION ========= + string internal constant NATIVE_CONVERTER_PROTOCOL = "1.0.0"; + + // ========= STATE VARIABLES ========= + uint256 internal stateBeforeInitialize; + + // ========= ADDRESSES ========= + address internal calculatedNativeConverter; + address internal dummyNativeConverter; + address internal migrationManager; + address internal originUnderlyingToken; + address internal owner; + address internal proxyAdmin; + address internal recipient; + address internal sender; + + // ========= CONTRACT METADATA ========= + bytes internal underlyingTokenMetadata; + string internal customTokenName; + string internal customTokenSymbol; + string internal underlyingTokenName; + string internal underlyingTokenSymbol; + string internal version; + uint256 internal maxNonMigratableBackingPercentage; + uint256 internal maxNonMigratableGasBackingPercentage; + uint32 internal primaryChainAgglayerId; + uint8 internal customTokenDecimals; + uint8 internal underlyingTokenDecimals; + bool internal wethFunctionalityEnabled; + + // ========= MOCK CONTRACTS ========= + MockAgglayerBridge internal mockAgglayerBridge; + MockERC20Upgradeable internal customToken; + MockERC20Upgradeable internal underlyingToken; + + /// @notice Configure Secondary Chain infrastructure + /// @dev This function sets up the basic infrastructure but doesn't deploy implementations + function deploySecondaryChainInfrastructure() internal virtual { + // Setup vault bridge version + version = NATIVE_CONVERTER_PROTOCOL; + + // Set standard test addresses + setupStandardTestAddresses(); + + // Set origin underlying token address + originUnderlyingToken = makeAddr("originUnderlyingToken"); + + // Deploy mock Agglayer Bridge for unit tests + mockAgglayerBridge = new MockAgglayerBridge(); + mockAgglayerBridge.setNetworkId(NETWORK_ID_L2); + + MockGlobalExitRootManager _globalExitRootManager = new MockGlobalExitRootManager(); + mockAgglayerBridge.setGlobalExitRootManager(address(_globalExitRootManager)); + + // Setup labels + setupCommonLabels(); + } + + /// @notice Setup standard test addresses + function setupStandardTestAddresses() internal { + dummyNativeConverter = makeAddr("dummyNativeConverter"); + migrationManager = makeAddr("migrationManager"); + owner = makeAddr("owner"); + proxyAdmin = makeAddr("proxyAdmin"); + recipient = makeAddr("recipient"); + sender = vm.addr(senderPrivateKey); + } + + /// @notice Setup debugging labels + function setupCommonLabels() internal { + vm.label(address(mockAgglayerBridge), "MockAgglayerBridge"); + vm.label(dummyNativeConverter, "DummyNativeConverter"); + vm.label(migrationManager, "MigrationManager"); + vm.label(owner, "Owner"); + vm.label(proxyAdmin, "ProxyAdmin"); + vm.label(recipient, "Recipient"); + vm.label(sender, "Sender"); + } + + /// @notice Helper function to get the proxy admin address + function _getProxyAdmin(address target) internal view returns (address) { + bytes32 ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 value = vm.load(target, ADMIN_SLOT); + return address(uint160(uint256(value))); + } +} diff --git a/test/base/secondary-chain/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase.sol b/test/base/secondary-chain/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase.sol new file mode 100644 index 00000000..844f971b --- /dev/null +++ b/test/base/secondary-chain/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {MockERC20Upgradeable, SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; +import {MockFiatTokenV2_2} from "test/utils/mocks/MockFiatTokenV2_2.sol"; + +// Core contracts +import {VbUsdcNativeConverterAgglayerBridgedUsdcStandard} from + "src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandard.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +/// @title Mock USDC Token with 6 decimals +/// @notice Mock implementation with USDC-like properties (6 decimals) + +contract MockUSDCToken is MockERC20Upgradeable { + /// @notice Get decimals (USDC has 6 decimals) + function decimals() public pure override returns (uint8) { + return 6; + } +} + +/// @title vbUSDC Native Converter Agglayer Bridged USDC Standard Test Base +/// @notice Base contract for testing VbUsdcNativeConverterAgglayerBridgedUsdcStandard as a standalone contract +abstract contract VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + VbUsdcNativeConverterAgglayerBridgedUsdcStandard internal nativeConverter; + address internal nativeConverterImpl; + MockFiatTokenV2_2 internal vbUsdcToken; + + /// @notice Deploy vbUSDC NativeConverter-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for vbUSDC NativeConverter testing + function deployVbUsdcNativeConverterAgglayerBridgedUsdcStandardInfrastructure() internal { + underlyingTokenName = "Bridged USDC"; + underlyingTokenSymbol = "USDC.e"; + underlyingTokenDecimals = 6; // USDC has 6 decimals + customTokenName = "vbUSDC"; + customTokenSymbol = "vbUSDC"; + customTokenDecimals = 6; // vbUSDC also has 6 decimals + maxNonMigratableBackingPercentage = 1e18; // 100% - migration not supported yet + primaryChainAgglayerId = NETWORK_ID_L1; + + deploySecondaryChainInfrastructure(); + deployVbUsdcNativeConverterAgglayerBridgedUsdcStandard(); + verifyVbUsdcNativeConverterAgglayerBridgedUsdcStandardSetup(); + setupLabels(); + } + + /// @notice Deploy vbUSDC Native Converter and related contracts + /// @dev This includes deploying the Custom Token (MockFiatTokenV2_2) and initializing both contracts + function deployVbUsdcNativeConverterAgglayerBridgedUsdcStandard() internal { + // Override the default decimals by creating a custom implementation that returns 6 + MockUSDCToken usdcToken = new MockUSDCToken(); + usdcToken.initialize(underlyingTokenName, underlyingTokenSymbol); + underlyingToken = MockERC20Upgradeable(address(usdcToken)); + + // Deploy vbUSDC token (FiatTokenV2_2 implementation) + MockFiatTokenV2_2 existingCustomTokenImpl = new MockFiatTokenV2_2(); + TransparentUpgradeableProxy existingCustomTokenProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingCustomTokenImpl), + proxyAdmin, + abi.encodeCall(MockFiatTokenV2_2.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + // Calculate the native converter address for proper initialization + calculatedNativeConverter = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); + + // For vbUSDC, we use the MockFiatTokenV2_2 directly as the custom token + // The bridge will mint this token directly + vbUsdcToken = MockFiatTokenV2_2(address(existingCustomTokenProxy)); + customToken = MockERC20Upgradeable(address(existingCustomTokenProxy)); + + nativeConverterImpl = address(new VbUsdcNativeConverterAgglayerBridgedUsdcStandard()); + + stateBeforeInitialize = vm.snapshotState(); + + bytes memory initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + nativeConverter = + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(_proxify(nativeConverterImpl, proxyAdmin, initData)); + assertEq(address(nativeConverter), calculatedNativeConverter); + + // Complete initialization + nativeConverter.reinitialize2(); + + vm.prank(address(nativeConverter)); + underlyingToken.approve(address(mockAgglayerBridge), type(uint256).max); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(vbUsdcToken), "vbUSDC"); + vm.label(address(nativeConverter), "VbUsdcNativeConverterAgglayerBridgedUsdcStandard"); + vm.label(address(underlyingToken), "BridgedUSDC"); + } + + /// @notice Helper to verify basic vbUSDC NativeConverter setup + function verifyVbUsdcNativeConverterAgglayerBridgedUsdcStandardSetup() internal view { + assertEq(address(nativeConverter.bridge()), address(mockAgglayerBridge)); + assertEq(address(nativeConverter.customToken()), address(customToken)); + assertEq(address(nativeConverter.migrationManager()), migrationManager); + assertEq(address(nativeConverter.underlyingToken()), address(underlyingToken)); + assertEq(nativeConverter.agglayerId(), NETWORK_ID_L2); + assertEq(nativeConverter.nonMigratableBackingPercentage(), maxNonMigratableBackingPercentage); + assertEq(nativeConverter.primaryChainAgglayerId(), primaryChainAgglayerId); + assertTrue(nativeConverter.hasRole(nativeConverter.DEFAULT_ADMIN_ROLE(), owner)); + + // Verify USDC-specific properties + assertEq(vbUsdcToken.decimals(), 6); + assertEq(vbUsdcToken.name(), customTokenName); + assertEq(vbUsdcToken.symbol(), customTokenSymbol); + assertEq(underlyingToken.decimals(), 6); // Mock USDC with 6 decimals + } +} diff --git a/test/base/secondary-chain/WethAgglayerTestBase.sol b/test/base/secondary-chain/WethAgglayerTestBase.sol new file mode 100644 index 00000000..ee43d5f0 --- /dev/null +++ b/test/base/secondary-chain/WethAgglayerTestBase.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {MockERC20Upgradeable, SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {WethAgglayer} from "src/secondary-chain/agglayer/vbETH/WethAgglayer.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title WETH Agglayer Test Base +/// @notice Base contract for testing WETH Agglayer as a standalone contract +abstract contract WethAgglayerTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + WethAgglayer internal wethAgglayer; + address internal wethAgglayerImpl; + + /// @notice Deploy WETH Agglayer-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for WETH Agglayer testing + function deployWethAgglayerInfrastructure() internal { + customTokenName = "WETH Custom Token"; + customTokenSymbol = "cWETH"; + customTokenDecimals = 18; + + deploySecondaryChainInfrastructure(); + deployWethAgglayer(true); + verifyWethAgglayerSetup(); + setupLabels(); + } + /// @notice Deploy WETH Agglayer and related contracts + /// @dev This includes deploying the Custom Token and initializing both contracts + + function deployWethAgglayer(bool wethFunctionalityEnabled_) internal { + MockERC20Upgradeable existingWethAgglayerImpl = new MockERC20Upgradeable(); + TransparentUpgradeableProxy existingWethAgglayerProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingWethAgglayerImpl), + address(this), + abi.encodeCall(MockERC20Upgradeable.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + address wethAgglayerImplAddr = address(new WethAgglayer()); + bytes memory initData = abi.encodeCall( + WethAgglayer.reinitialize2, (owner, customTokenDecimals, address(mockAgglayerBridge), dummyNativeConverter) + ); + bytes memory upgradeData = abi.encodeWithSelector( + ITransparentUpgradeableProxy.upgradeToAndCall.selector, wethAgglayerImplAddr, initData + ); + vm.prank(_getProxyAdmin(address(existingWethAgglayerProxy))); + (address(existingWethAgglayerProxy).call(upgradeData)); + WethAgglayer(payable(address(existingWethAgglayerProxy))).reinitialize3(wethFunctionalityEnabled_); + wethAgglayer = WethAgglayer(payable(address(existingWethAgglayerProxy))); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(wethAgglayer), "wethAgglayer"); + vm.label(address(wethAgglayerImpl), "wethAgglayerImpl"); + } + + /// @notice Helper to verify basic NativeConverter setup + function verifyWethAgglayerSetup() internal view { + assertEq(wethAgglayer.name(), customTokenName); + assertEq(wethAgglayer.symbol(), customTokenSymbol); + assertEq(wethAgglayer.decimals(), customTokenDecimals); + assertEq(wethAgglayer.bridge(), address(mockAgglayerBridge)); + assertEq(wethAgglayer.nativeConverter(), dummyNativeConverter); + } +} diff --git a/test/base/secondary-chain/WethNativeConverterAgglayerTestBase.sol b/test/base/secondary-chain/WethNativeConverterAgglayerTestBase.sol new file mode 100644 index 00000000..ef8ec4ec --- /dev/null +++ b/test/base/secondary-chain/WethNativeConverterAgglayerTestBase.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {MockERC20Upgradeable, SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {WethNativeConverterAgglayer} from "src/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayer.sol"; +import {WethAgglayer} from "src/secondary-chain/agglayer/vbETH/WethAgglayer.sol"; + +// OpenZeppelin +import { + ITransparentUpgradeableProxy, + TransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title WETH Native Converter Agglayer Test Base +/// @notice Base contract for testing WETH Native Converter Agglayer as a standalone contract +abstract contract WethNativeConverterAgglayerTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + WethNativeConverterAgglayer internal nativeConverter; + address internal nativeConverterImpl; + + /// @notice Deploy WETH NativeConverter-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for WETH NativeConverter testing + function deployWethNativeConverterAgglayerInfrastructure() internal { + underlyingTokenName = "Wrapped WETH"; + underlyingTokenSymbol = "uWETH"; + underlyingTokenDecimals = 18; + customTokenName = "WETH Custom Token"; + customTokenSymbol = "cWETH"; + customTokenDecimals = 18; + maxNonMigratableBackingPercentage = MAX_NON_MIGRATABLE_BACKING_PERCENTAGE; + maxNonMigratableGasBackingPercentage = MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE; + primaryChainAgglayerId = NETWORK_ID_L1; + + deploySecondaryChainInfrastructure(); + deployWethNativeConverterAgglayer(); + verifyWethNativeConverterAgglayerSetup(); + setupLabels(); + } + /// @notice Deploy WETH Native Converter Agglayer and related contracts + /// @dev This includes deploying the Custom Token and initializing both contracts + + function deployWethNativeConverterAgglayer() internal { + // Set underlying and custom token addresses + underlyingToken = new MockERC20Upgradeable(); + underlyingToken.initialize(underlyingTokenName, underlyingTokenSymbol); + underlyingTokenMetadata = abi.encode(underlyingTokenName, underlyingTokenSymbol, underlyingTokenDecimals); + + MockERC20Upgradeable existingCustomTokenImpl = new MockERC20Upgradeable(); + TransparentUpgradeableProxy existingCustomTokenProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(existingCustomTokenImpl), + proxyAdmin, + abi.encodeCall(MockERC20Upgradeable.initialize, (customTokenName, customTokenSymbol)) + ) + ) + ); + + WethAgglayer genericCustomTokenImpl = new WethAgglayer(); + + calculatedNativeConverter = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); + + bytes memory customTokenInitData = abi.encodeCall( + WethAgglayer.reinitialize2, + (proxyAdmin, customTokenDecimals, address(mockAgglayerBridge), calculatedNativeConverter) + ); + bytes memory customTokenUpgradeData = abi.encodeCall( + ITransparentUpgradeableProxy.upgradeToAndCall, (address(genericCustomTokenImpl), customTokenInitData) + ); + vm.prank(_getProxyAdmin(address(existingCustomTokenProxy))); + (bool success,) = address(existingCustomTokenProxy).call(customTokenUpgradeData); + require(success, "Failed to upgrade to WethAgglayer"); + + // Complete the initialization with reinitialize3 + WethAgglayer(payable(address(existingCustomTokenProxy))).reinitialize3(true); + + // assign variables for generic testing + customToken = MockERC20Upgradeable(address(existingCustomTokenProxy)); + + nativeConverterImpl = address(new WethNativeConverterAgglayer()); + + stateBeforeInitialize = vm.snapshotState(); + + bytes memory initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + nativeConverter = WethNativeConverterAgglayer(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + assertEq(address(nativeConverter), calculatedNativeConverter); + + vm.prank(address(nativeConverter)); + underlyingToken.approve(address(mockAgglayerBridge), type(uint256).max); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(customToken), "WETH"); + vm.label(address(nativeConverter), "WethNativeConverterAgglayer"); + vm.label(address(underlyingToken), "UnderlyingToken"); + } + + /// @notice Helper to verify basic NativeConverter setup + function verifyWethNativeConverterAgglayerSetup() internal view { + assertEq(address(nativeConverter.bridge()), address(mockAgglayerBridge)); + assertEq(address(nativeConverter.customToken()), address(customToken)); + assertEq(address(nativeConverter.migrationManager()), migrationManager); + assertEq(address(nativeConverter.underlyingToken()), address(underlyingToken)); + assertEq(nativeConverter.agglayerId(), NETWORK_ID_L2); + assertEq(nativeConverter.nonMigratableBackingPercentage(), maxNonMigratableBackingPercentage); + assertEq(nativeConverter.nonMigratableGasBackingPercentage(), maxNonMigratableGasBackingPercentage); + assertEq(nativeConverter.primaryChainAgglayerId(), primaryChainAgglayerId); + assertTrue(nativeConverter.hasRole(nativeConverter.DEFAULT_ADMIN_ROLE(), owner)); + assertEq(nativeConverter.nonMigratableGasBackingPercentage(), MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE); + } +} diff --git a/test/base/secondary-chain/WethWormholeTestBase.sol b/test/base/secondary-chain/WethWormholeTestBase.sol new file mode 100644 index 00000000..3f493b85 --- /dev/null +++ b/test/base/secondary-chain/WethWormholeTestBase.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test infrastructure +import {SecondaryChainBase} from "test/base/secondary-chain/SecondaryChainBase.sol"; + +// Core contracts +import {WethWormhole} from "src/secondary-chain/wormhole/vbETH/WethWormhole.sol"; + +// OpenZeppelin +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +/// @title WETH Wormhole Test Base +/// @notice Base contract for testing WethWormhole as a standalone contract +/// @dev This test base combines CustomTokenWormhole and CustomTokenWethExtension functionality +abstract contract WethWormholeTestBase is SecondaryChainBase { + // ========= MAIN CONTRACTS ========= + WethWormhole internal wethWormhole; + address internal wethWormholeImpl; + TransparentUpgradeableProxy existingWethWormholeProxy; + + // ========= VARIABLES ========= + address internal nttManager = makeAddr("NttManager"); + uint8 internal originalUnderlyingTokenDecimals = 18; + bool internal gasTokenIsEth = true; + + /// @notice Deploy WethWormhole-specific infrastructure + /// @dev Sets up tokens, bridge, and related contracts for WethWormhole testing + function deployWethWormholeInfrastructure() internal { + customTokenName = "WETH Wormhole"; + customTokenSymbol = "wWETH"; + customTokenDecimals = 18; + wethFunctionalityEnabled = true; + + deploySecondaryChainInfrastructure(); + deployWethWormhole(); + verifyWethWormholeSetup(); + setupLabels(); + } + + /// @notice Deploy WethWormhole and related contracts + /// @dev This includes deploying the WethWormhole implementation and initializing the proxy + function deployWethWormhole() internal { + wethWormholeImpl = address(new WethWormhole()); + + existingWethWormholeProxy = TransparentUpgradeableProxy( + payable( + _proxify( + wethWormholeImpl, + proxyAdmin, + abi.encodeCall( + WethWormhole.reinitialize1, + ( + owner, + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + nttManager, + gasTokenIsEth, + gasTokenIsEth + ) + ) + ) + ) + ); + + wethWormhole = WethWormhole(payable(address(existingWethWormholeProxy))); + } + + /// @notice Setup debugging labels + function setupLabels() internal { + vm.label(address(wethWormhole), "WethWormhole"); + vm.label(address(wethWormholeImpl), "WethWormholeImplementation"); + vm.label(nttManager, "MockNttManager"); + } + + /// @notice Helper to verify basic WethWormhole setup + function verifyWethWormholeSetup() internal view { + assertEq(wethWormhole.name(), customTokenName); + assertEq(wethWormhole.symbol(), customTokenSymbol); + assertEq(wethWormhole.decimals(), customTokenDecimals); + assertEq(wethWormhole.bridge(), nttManager); + assertTrue(wethWormhole.hasRole(wethWormhole.DEFAULT_ADMIN_ROLE(), owner)); + assertTrue(wethWormhole.hasRole(wethWormhole.PAUSER_ROLE(), owner)); + assertEq(wethWormhole.totalSupply(), 0); + assertFalse(wethWormhole.paused()); + assertTrue(wethWormhole.wethFunctionalityEnabled()); + } +} diff --git a/test/custom-tokens/WETH.t.sol b/test/custom-tokens/WETH.t.sol deleted file mode 100644 index 92cdf0e9..00000000 --- a/test/custom-tokens/WETH.t.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import "forge-std/Test.sol"; - -import {WETH} from "../../src/custom-tokens/WETH/WETH.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -contract LXLYBridgeMock { - address public gasTokenAddress; - uint32 public gasTokenNetwork; - - function setGasTokenAddress(address _gasTokenAddress) external { - gasTokenAddress = _gasTokenAddress; - } - - function setGasTokenNetwork(uint32 _gasTokenNetwork) external { - gasTokenNetwork = _gasTokenNetwork; - } -} - -contract WETHTest is Test { - WETH internal wETH; - LXLYBridgeMock internal lxlyBridgeMock; - uint256 internal zkevmFork; - address LXLY_BRIDGE = 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe; - address internal calculatedNativeConverterAddr = - vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2); - - function setUp() public { - zkevmFork = vm.createSelectFork("polygon_zkevm", 19164969); - - _deployWETH(LXLY_BRIDGE); - lxlyBridgeMock = new LXLYBridgeMock(); - - vm.label(address(wETH), "wETH"); - vm.label(address(lxlyBridgeMock), "lxlyBridgeMock"); - } - - function test_wETH_receive(uint256 amount) public { - assertEq(wETH.balanceOf(address(this)), 0); - deal(address(this), amount); - - (bool success,) = address(wETH).call{value: amount}(""); - require(success); - assertEq(wETH.balanceOf(address(this)), amount); - } - - function test_wETH_deposit(uint256 amount) public { - assertEq(wETH.balanceOf(address(this)), 0); - deal(address(this), amount); - - vm.expectEmit(); - emit WETH.Deposit(address(this), amount); - wETH.deposit{value: amount}(); - assertEq(wETH.balanceOf(address(this)), amount); - } - - function test_wETH_withdraw(uint256 amount) public { - assertEq(wETH.balanceOf(address(this)), 0); - deal(address(this), amount); - - wETH.deposit{value: amount}(); - assertEq(wETH.balanceOf(address(this)), amount); - assertEq(address(this).balance, 0); - - vm.expectEmit(); - emit WETH.Withdrawal(address(this), amount); - wETH.withdraw(amount); - assertEq(wETH.balanceOf(address(this)), 0); - assertEq(address(this).balance, amount); - } - - function test_wETH_version() public view { - assertEq(wETH.version(), "0.5.0"); - } - - function test_onlyIfGasTokenIsEth() public { - uint256 amount = 1 ether; - deal(address(this), amount); - - lxlyBridgeMock.setGasTokenAddress(address(this)); - lxlyBridgeMock.setGasTokenNetwork(0); - _deployWETH(address(lxlyBridgeMock)); - vm.expectRevert(WETH.FunctionNotSupportedOnThisNetwork.selector); - wETH.deposit{value: amount}(); - - lxlyBridgeMock.setGasTokenAddress(address(0)); - lxlyBridgeMock.setGasTokenNetwork(1); - _deployWETH(address(lxlyBridgeMock)); - vm.expectRevert(WETH.FunctionNotSupportedOnThisNetwork.selector); - wETH.deposit{value: amount}(); - - lxlyBridgeMock.setGasTokenAddress(address(0)); - lxlyBridgeMock.setGasTokenNetwork(0); - _deployWETH(address(lxlyBridgeMock)); - vm.expectEmit(); - emit WETH.Deposit(address(this), amount); - wETH.deposit{value: amount}(); - assertEq(wETH.balanceOf(address(this)), amount); - } - - function _deployWETH(address _lxlyBridge) internal { - wETH = new WETH(); - bytes memory initData = abi.encodeCall( - WETH.reinitialize, (address(this), "wETH", "wETH", 18, _lxlyBridge, calculatedNativeConverterAddr) - ); - wETH = WETH(payable(address(new TransparentUpgradeableProxy(address(wETH), address(this), initData)))); - } - - receive() external payable {} -} diff --git a/test/custom-tokens/WETHNativeConverter.t.sol b/test/custom-tokens/WETHNativeConverter.t.sol deleted file mode 100644 index 382b746b..00000000 --- a/test/custom-tokens/WETHNativeConverter.t.sol +++ /dev/null @@ -1,379 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import "forge-std/Test.sol"; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; - -import {MockERC20} from "forge-std/mocks/MockERC20.sol"; -import {MockERC20MintableBurnable} from "../GenericNativeConverter.t.sol"; -import {WETH} from "src/custom-tokens/WETH/WETH.sol"; - -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; - -import {GenericNativeConverterTest} from "../GenericNativeConverter.t.sol"; -import {WETHNativeConverter} from "../../src/custom-tokens/WETH/WETHNativeConverter.sol"; -import {GenericNativeConverter, NativeConverter} from "../../src/custom-tokens/GenericNativeConverter.sol"; -import {MigrationManager} from "../../src/MigrationManager.sol"; - -contract LXLYBridgeMock { - address public gasTokenAddress; - uint32 public gasTokenNetwork; - - function setGasTokenAddress(address _gasTokenAddress) external { - gasTokenAddress = _gasTokenAddress; - } - - function setGasTokenNetwork(uint32 _gasTokenNetwork) external { - gasTokenNetwork = _gasTokenNetwork; - } - - function networkID() external pure returns (uint32) { - return 1; - } - - function wrappedAddressIsNotMintable(address wrappedAddress) external pure returns (bool isNotMintable) { - (wrappedAddress); - return true; - } -} - -contract WETHNativeConverterTest is Test, GenericNativeConverterTest { - uint256 constant MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE = 1e17; - - MockERC20MintableBurnable internal wWETH; - WETH internal wETH; - LXLYBridgeMock internal lxlyBridgeMock; - address internal migrationManager_ = makeAddr("migrationManager"); - - WETHNativeConverter internal wETHConverter; - - function setUp() public override { - zkevmFork = vm.createSelectFork("polygon_zkevm", 19164969); - - // Setup tokens - wWETH = new MockERC20MintableBurnable(); - wWETH.initialize("Wrapped WETH", "wWETH", 18); - wETH = new WETH(); - address calculatedNativeConverterAddr = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2); - vm.etch(LXLY_BRIDGE, SOVEREIGN_BRIDGE_BYTECODE); - bytes memory initData = abi.encodeCall( - WETH.reinitialize, (address(this), "wETH", "wETH", 18, LXLY_BRIDGE, calculatedNativeConverterAddr) - ); - wETH = WETH(payable(address(new TransparentUpgradeableProxy(address(wETH), address(this), initData)))); - - // assign variables for generic testing - customToken = MockERC20MintableBurnable(address(wETH)); - underlyingToken = MockERC20MintableBurnable(address(wWETH)); - migrationManager = migrationManager_; - underlyingTokenMetadata = abi.encode("Wrapped WETH", "wWETH", 18); - - // Deploy and initialize converter - nativeConverter = GenericNativeConverter(address(new WETHNativeConverter())); - - /// important to assign customToken, underlyingToken, and nativeConverter - /// before the snapshot, so test_initialize will work - beforeInit = vm.snapshotState(); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - 18, // decimals - address(wETH), // custom token - address(wWETH), // wrapped underlying token - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - nativeConverter = GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - assertEq(address(nativeConverter), calculatedNativeConverterAddr); - - _mapCustomToken(originUnderlyingToken, address(wWETH), false); - - wETHConverter = WETHNativeConverter(payable(address(nativeConverter))); - - lxlyBridgeMock = new LXLYBridgeMock(); - - vm.label(address(wETH), "wETH"); - vm.label(address(this), "testerAddress"); - vm.label(LXLY_BRIDGE, "lxlyBridge"); - vm.label(migrationManager, "migrationManager"); - vm.label(owner, "owner"); - vm.label(recipient, "recipient"); - vm.label(sender, "sender"); - vm.label(address(nativeConverter), "WETHNativeConverter"); - vm.label(address(wWETH), "wWETH"); - } - - function test_initialize() public override { - vm.revertToState(beforeInit); - - bytes memory initData; - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - address(0), - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(NativeConverter.InvalidOwner.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(0), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(NativeConverter.InvalidCustomToken.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(0), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(NativeConverter.InvalidUnderlyingToken.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - address(0), - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(NativeConverter.InvalidLxLyBridge.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - MockERC20 dummyToken = new MockERC20(); - dummyToken.initialize("Dummy Token", "DT", 6); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(dummyToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(abi.encodeWithSelector(NativeConverter.NonMatchingCustomTokenDecimals.selector, 6, 18)); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - dummyToken = new MockERC20(); // have to deploy again because of revert - dummyToken.initialize("Dummy Token", "DT", 6); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(dummyToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(abi.encodeWithSelector(NativeConverter.NonMatchingUnderlyingTokenDecimals.selector, 6, 18)); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(dummyToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - 1e19, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(abi.encodeWithSelector(NativeConverter.InvalidNonMigratableBackingPercentage.selector)); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - address(0), - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - vm.expectRevert(NativeConverter.InvalidMigrationManager.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - - initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - LXLY_BRIDGE, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - 1e19 - ) - ); - vm.expectRevert(NativeConverter.InvalidNonMigratableBackingPercentage.selector); - GenericNativeConverter(_proxify(address(nativeConverter), address(this), initData)); - } - - function test_migrateGasBackingToLayerX() public { - uint256 amount = 100; - uint256 amountToMigrate = 50; - - vm.startPrank(owner); - - wETHConverter.pause(); - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - wETHConverter.migrateGasBackingToLayerX(amountToMigrate); - wETHConverter.unpause(); - - vm.expectRevert(NativeConverter.InvalidAssets.selector); - wETHConverter.migrateGasBackingToLayerX(0); // try with 0 backing - - // create backing on layer Y - uint256 backingOnLayerY = 0; - deal(address(underlyingToken), owner, amount); - underlyingToken.approve(address(nativeConverter), amount); - backingOnLayerY = wETHConverter.convert(amount, recipient); - - deal(address(wETH), amount); - - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_ASSET, NETWORK_ID_L1, address(0x00), NETWORK_ID_L1, migrationManager, amountToMigrate, "", 55413 - ); - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_MESSAGE, - NETWORK_ID_L2, - address(wETHConverter), - NETWORK_ID_L1, - migrationManager, - 0, - abi.encode( - MigrationManager.CrossNetworkInstruction.WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, - abi.encode(amountToMigrate, amountToMigrate) - ), - 55414 - ); - vm.expectEmit(); - emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); - wETHConverter.migrateGasBackingToLayerX(amountToMigrate); - assertEq(address(wETH).balance, amountToMigrate); - - uint256 currentBacking = address(wETH).balance; - uint256 nonMigratableGasBacking = Math.mulDiv(amount, MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE, 1e18); // since the non-migratable gas backing is calculated as the percentage of the total supply of the custom token we take the original amount - - vm.expectRevert( - abi.encodeWithSelector( - NativeConverter.AssetsTooLarge.selector, currentBacking - nonMigratableGasBacking, currentBacking + 1 - ) - ); - wETHConverter.migrateGasBackingToLayerX(currentBacking + 1); - - vm.stopPrank(); - } - - function test_onlyIfGasTokenIsEth() public { - uint256 amount = 100; - deal(address(this), amount); - - lxlyBridgeMock.setGasTokenAddress(address(this)); - lxlyBridgeMock.setGasTokenNetwork(0); - _deployWETHNativeConverter(address(lxlyBridgeMock)); - vm.expectRevert(WETHNativeConverter.FunctionNotSupportedOnThisNetwork.selector); - (address(wETHConverter).call{value: amount}("")); - - lxlyBridgeMock.setGasTokenAddress(address(0)); - lxlyBridgeMock.setGasTokenNetwork(1); - _deployWETHNativeConverter(address(lxlyBridgeMock)); - vm.expectRevert(WETHNativeConverter.FunctionNotSupportedOnThisNetwork.selector); - (address(wETHConverter).call{value: amount}("")); - - lxlyBridgeMock.setGasTokenAddress(address(0)); - lxlyBridgeMock.setGasTokenNetwork(0); - _deployWETHNativeConverter(address(lxlyBridgeMock)); - (address(wETHConverter).call{value: amount}("")); - assertEq(address(wETHConverter).balance, amount); - } - - function _deployWETHNativeConverter(address _lxlyBridge) internal { - wETHConverter = new WETHNativeConverter(); - bytes memory initData = abi.encodeCall( - WETHNativeConverter.initialize, - ( - owner, - ORIGINAL_UNDERLYING_TOKEN_DECIMALS, - address(customToken), - address(underlyingToken), - _lxlyBridge, - NETWORK_ID_L1, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - migrationManager, - MAX_NON_MIGRATABLE_GAS_BACKING_PERCENTAGE - ) - ); - wETHConverter = WETHNativeConverter(payable(_proxify(address(wETHConverter), address(this), initData))); - } -} diff --git a/test/etc/PendingLib.sol b/test/etc/PendingLib.sol deleted file mode 100644 index 63741cfd..00000000 --- a/test/etc/PendingLib.sol +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity ^0.8.0; - -struct MarketConfig { - /// @notice The maximum amount of assets that can be allocated to the market. - uint184 cap; - /// @notice Whether the market is in the withdraw queue. - bool enabled; - /// @notice The timestamp at which the market can be instantly removed from the withdraw queue. - uint64 removableAt; -} - -struct PendingUint192 { - /// @notice The pending value to set. - uint192 value; - /// @notice The timestamp at which the pending value becomes valid. - uint64 validAt; -} - -struct PendingAddress { - /// @notice The pending value to set. - address value; - /// @notice The timestamp at which the pending value becomes valid. - uint64 validAt; -} - -/// @title PendingLib -/// @author Morpho Labs -/// @custom:contact security@morpho.org -/// @notice Library to manage pending values and their validity timestamp. -library PendingLib { - /// @dev Updates `pending`'s value to `newValue` and its corresponding `validAt` timestamp. - /// @dev Assumes `timelock` <= `MAX_TIMELOCK`. - function update(PendingUint192 storage pending, uint184 newValue, uint256 timelock) internal { - pending.value = newValue; - // Safe "unchecked" cast because timelock <= MAX_TIMELOCK. - pending.validAt = uint64(block.timestamp + timelock); - } - - /// @dev Updates `pending`'s value to `newValue` and its corresponding `validAt` timestamp. - /// @dev Assumes `timelock` <= `MAX_TIMELOCK`. - function update(PendingAddress storage pending, address newValue, uint256 timelock) internal { - pending.value = newValue; - // Safe "unchecked" cast because timelock <= MAX_TIMELOCK. - pending.validAt = uint64(block.timestamp + timelock); - } -} diff --git a/test/fuzz/GenericVaultBridgeTokenFuzz.t.sol b/test/fuzz/GenericVaultBridgeTokenFuzz.t.sol index c5d9c366..b46a5456 100644 --- a/test/fuzz/GenericVaultBridgeTokenFuzz.t.sol +++ b/test/fuzz/GenericVaultBridgeTokenFuzz.t.sol @@ -1,22 +1,17 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; -import "forge-std/Test.sol"; - -import {GenericVaultBridgeToken} from "src/vault-bridge-tokens/GenericVaultBridgeToken.sol"; -import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {VaultBridgeToken, PausableUpgradeable, Initializable} from "src/VaultBridgeToken.sol"; -import {VaultBridgeTokenPart2} from "src/VaultBridgeTokenPart2.sol"; -import {VaultBridgeTokenInitializer} from "src/VaultBridgeTokenInitializer.sol"; +// Test base +import "test/base/primary-chain/VaultBridgeTokenTestBase.sol"; +// OpenZeppelin import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {TestVault} from "test/etc/TestVault.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -contract GenericVaultBridgeTokenHarness is GenericVaultBridgeToken { - constructor() GenericVaultBridgeToken() {} +// Core contracts +import {VaultBridgeToken} from "src/primary-chain/VaultBridgeToken.sol"; +contract VaultBridgeTokenHarness is TestHarnessVaultBridgeToken { function internal_withdrawFromYieldVault( uint256 assets, bool exact, @@ -30,86 +25,167 @@ contract GenericVaultBridgeTokenHarness is GenericVaultBridgeToken { ); } - function internal_depositIntoYieldVault(uint256 assets, bool exact) internal returns (uint256 nonDepositedAssets) { + function internal_depositIntoYieldVault(uint256 assets, bool exact) public returns (uint256 nonDepositedAssets) { nonDepositedAssets = _depositIntoYieldVault(assets, exact); } } -contract GenericVaultBridgeTokenFuzzTest is Test { +contract GenericVaultBridgeTokenFuzzTest is VaultBridgeTokenTestBase { using SafeERC20 for IERC20; - using SafeERC20 for GenericVaultBridgeTokenHarness; - - // constants - address constant LXLY_BRIDGE = 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe; - address internal constant TEST_TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - uint256 internal constant MAX_DEPOSIT = 10e18; - uint256 internal constant MAX_WITHDRAW = 10e18; - uint256 internal constant MINIMUM_YIELD_VAULT_DEPOSIT = 1e12; - uint256 internal constant YIELD_VAULT_ALLOWED_SLIPPAGE = 1e16; // 1% - - address asset; - address vbTokenImplementation; - GenericVaultBridgeTokenHarness vbToken; - VaultBridgeTokenPart2 vbTokenPart2; - TestVault vbTokenVault; - uint256 mainnetFork; - - address migrationManager = makeAddr("migrationManager"); - address owner = makeAddr("owner"); - address sender = vm.addr(0xBEEF); - address yieldRecipient = makeAddr("yieldRecipient"); + using SafeERC20 for VaultBridgeTokenHarness; - function setUp() public virtual { - mainnetFork = vm.createSelectFork("mainnet"); + // Override the vbToken with our harness version for internal function testing + VaultBridgeTokenHarness internal vbTokenHarness; - asset = TEST_TOKEN; - vbTokenVault = new TestVault(asset); - vbTokenVault.setMaxDeposit(MAX_DEPOSIT); - vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); - - vbToken = new GenericVaultBridgeTokenHarness(); - vbTokenImplementation = address(vbToken); + function setUp() public virtual { + deployVaultBridgeTokenInfrastructure(); - vbTokenPart2 = new VaultBridgeTokenPart2(); + // Deploy harness version for testing internal functions + address harnessImplementation = address(new VaultBridgeTokenHarness()); VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ owner: owner, - name: "Vault Bridge USDC", - symbol: "vbUSDC", - underlyingToken: asset, - minimumReservePercentage: 1e17, - yieldVault: address(vbTokenVault), + name: tokenName, + symbol: tokenSymbol, + underlyingToken: underlyingToken, + minimumReservePercentage: minimumReservePercentage, + yieldVault: address(yieldVault), yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, + agglayerBridge: agglayerBridge, minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, + migrationManager: migrationManagerAddr, yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) + vaultBridgeTokenPart2: address(vbTokenPart2Implementation) }); - bytes memory initData = - abi.encodeCall(vbToken.initialize, (address(new VaultBridgeTokenInitializer()), initParams)); - vbToken = - GenericVaultBridgeTokenHarness(payable(_proxify(address(vbTokenImplementation), address(this), initData))); - vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); - - deal(asset, migrationManager, 10000000 ether); - vm.prank(migrationManager); - IERC20(asset).forceApprove(address(vbToken), 10000000 ether); - - vm.label(address(vbTokenVault), "vbToken Vault"); - vm.label(address(vbToken), "vbToken"); - vm.label(address(vbTokenImplementation), "vbToken Implementation"); - vm.label(asset, "Underlying Asset"); - vm.label(migrationManager, "Migration Manager"); - vm.label(owner, "Owner"); - vm.label(sender, "Sender"); - vm.label(yieldRecipient, "Yield Recipient"); - vm.label(LXLY_BRIDGE, "Lxly Bridge"); - vm.label(address(vbTokenPart2), "vbToken Part 2"); + + bytes memory initData = abi.encodeCall( + VaultBridgeTokenHarness(harnessImplementation).reinitialize1, (address(initializer), initParams) + ); + + address harnessProxy = _proxify(harnessImplementation, address(this), initData); + vbTokenHarness = VaultBridgeTokenHarness(payable(harnessProxy)); + vbTokenHarness.reinitialize2(); + + vm.label(address(vbTokenHarness), "VaultBridgeToken Harness"); + } + + function testFuzz_depositIntoYieldVault_minimumDepositNotMet_revert(uint256 assets) public { + assets = bound(assets, 1, MINIMUM_YIELD_VAULT_DEPOSIT - 1); + + vm.expectRevert( + abi.encodeWithSelector( + VaultBridgeToken.MinimumYieldVaultDepositNotMet.selector, assets, MINIMUM_YIELD_VAULT_DEPOSIT + ) + ); + vbTokenHarness.internal_depositIntoYieldVault(assets, true); + } + + function testFuzz_depositIntoYieldVault_minimumDepositNotMet_nonExact(uint256 assets) public { + assets = bound(assets, 1, MINIMUM_YIELD_VAULT_DEPOSIT - 1); + + uint256 nonDepositedAssets = vbTokenHarness.internal_depositIntoYieldVault(assets, false); + assertEq(nonDepositedAssets, assets); + } + + function testFuzz_depositIntoYieldVault_exceedsMaxDeposit_revert(uint256 assets) public { + assets = bound(assets, MAX_DEPOSIT + 1, type(uint128).max); + + vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.YieldVaultDepositFailed.selector, assets, MAX_DEPOSIT)); + vbTokenHarness.internal_depositIntoYieldVault(assets, true); + } + + function testFuzz_depositIntoYieldVault_exceedsMaxDeposit_nonExact(uint256 assets) public { + assets = bound(assets, MAX_DEPOSIT + 1, type(uint128).max); + + // Provide the contract with enough tokens to handle the deposit + deal(underlyingToken, address(vbTokenHarness), MAX_DEPOSIT); + + uint256 nonDepositedAssets = vbTokenHarness.internal_depositIntoYieldVault(assets, false); + assertEq(nonDepositedAssets, assets - MAX_DEPOSIT); + } + + function testFuzz_depositIntoYieldVault_slippageFailure_revert(uint256 assets, uint256 slippageAmount) public { + assets = bound(assets, MINIMUM_YIELD_VAULT_DEPOSIT, MAX_DEPOSIT); + + // Calculate minimum expected shares for 1% slippage tolerance + uint256 minimumExpectedShares = Math.mulDiv(assets, 1e18 - YIELD_VAULT_ALLOWED_SLIPPAGE, 1e18); + + // Bound slippage to be large enough to cause solvency failure + // The actual shares after slippage will be (assets - slippageAmount) + // We need this to be less than minimumExpectedShares + uint256 maxAllowedSlippage = assets - minimumExpectedShares; + slippageAmount = bound(slippageAmount, maxAllowedSlippage + 1, assets - 1); + + // Setup vault to have slippage that exceeds the allowed threshold + deal(underlyingToken, address(vbTokenHarness), assets); + yieldVault.setSlippage(true, slippageAmount); + + // Calculate the actual shares minted after slippage + uint256 actualMintedShares = assets - slippageAmount; + + vm.expectRevert( + abi.encodeWithSelector( + VaultBridgeToken.InsufficientYieldVaultSharesMinted.selector, assets, actualMintedShares + ) + ); + vbTokenHarness.internal_depositIntoYieldVault(assets, true); + } + + function testFuzz_depositIntoYieldVault_slippageFailure_nonExact(uint256 assets, uint256 slippageAmount) public { + assets = bound(assets, MINIMUM_YIELD_VAULT_DEPOSIT, MAX_DEPOSIT); + + // Calculate minimum expected shares for 1% slippage tolerance + uint256 minimumExpectedShares = Math.mulDiv(assets, 1e18 - YIELD_VAULT_ALLOWED_SLIPPAGE, 1e18); + + // Bound slippage to be large enough to cause solvency failure + // The actual shares after slippage will be (assets - slippageAmount) + // We need this to be less than minimumExpectedShares + uint256 maxAllowedSlippage = assets - minimumExpectedShares; + slippageAmount = bound(slippageAmount, maxAllowedSlippage + 1, assets - 1); + + // Setup vault to have slippage that exceeds the allowed threshold + deal(underlyingToken, address(vbTokenHarness), assets); + yieldVault.setSlippage(true, slippageAmount); + + uint256 nonDepositedAssets = vbTokenHarness.internal_depositIntoYieldVault(assets, false); + assertEq(nonDepositedAssets, assets); + } + + function testFuzz_depositIntoYieldVault_success(uint256 assets, uint256 slippageAmount) public { + assets = bound(assets, MINIMUM_YIELD_VAULT_DEPOSIT, MAX_DEPOSIT); + + // Calculate minimum expected shares for 1% slippage tolerance + uint256 minimumExpectedShares = Math.mulDiv(assets, 1e18 - YIELD_VAULT_ALLOWED_SLIPPAGE, 1e18); + + // Bound slippage to be within allowed threshold + // The actual shares after slippage will be (assets - slippageAmount) + // We need this to be >= minimumExpectedShares + uint256 maxAllowedSlippage = assets - minimumExpectedShares; + slippageAmount = bound(slippageAmount, 0, maxAllowedSlippage); + + // Calculate expected minted shares after slippage + uint256 expectedMintedShares = assets - slippageAmount; + + // Setup vault with acceptable slippage + deal(underlyingToken, address(vbTokenHarness), assets); + yieldVault.setSlippage(true, slippageAmount); + + uint256 nonDepositedAssets = vbTokenHarness.internal_depositIntoYieldVault(assets, false); + assertEq(nonDepositedAssets, 0); + assertEq(yieldVault.balanceOf(address(vbTokenHarness)), expectedMintedShares); } - // @todo add fuzz tests for the following functions: - // - _depositIntoYieldVault + function testFuzz_depositIntoYieldVault_successNoSlippage(uint256 assets) public { + assets = bound(assets, MINIMUM_YIELD_VAULT_DEPOSIT, MAX_DEPOSIT); + + // Setup vault without slippage + deal(underlyingToken, address(vbTokenHarness), assets); + yieldVault.setSlippage(false, 0); + + uint256 nonDepositedAssets = vbTokenHarness.internal_depositIntoYieldVault(assets, true); + assertEq(nonDepositedAssets, 0); + assertEq(yieldVault.balanceOf(address(vbTokenHarness)), assets); + } function testFuzz_withdrawFromYieldVault_revert(uint256 assets, uint256 originalTotalSupply, uint256 slippageAmount) public @@ -118,16 +194,16 @@ contract GenericVaultBridgeTokenFuzzTest is Test { vm.assume(originalTotalSupply >= assets); vm.assume(slippageAmount > Math.mulDiv(assets, 0.01e18, 1e18) && slippageAmount < assets); - deal(TEST_TOKEN, address(vbTokenVault), assets); - vbTokenVault.setBalance(address(vbToken), assets); - vbTokenVault.setSlippage(true, slippageAmount); + deal(underlyingToken, address(yieldVault), assets); + yieldVault.setBalance(address(vbTokenHarness), assets); + yieldVault.setSlippage(true, slippageAmount); vm.expectRevert( abi.encodeWithSelector( VaultBridgeToken.ExcessiveYieldVaultSharesBurned.selector, assets + slippageAmount, assets ) ); - vbToken.internal_withdrawFromYieldVault( + vbTokenHarness.internal_withdrawFromYieldVault( assets, false, sender, originalTotalSupply, 0, originalTotalSupply - assets ); } @@ -139,14 +215,14 @@ contract GenericVaultBridgeTokenFuzzTest is Test { vm.assume(originalTotalSupply >= assets); vm.assume(slippageAmount <= Math.mulDiv(assets, 0.01e18, 1e18) && slippageAmount < assets); - deal(TEST_TOKEN, address(vbTokenVault), assets); - vbTokenVault.setBalance(address(vbToken), assets); - vbTokenVault.setSlippage(true, slippageAmount); + deal(underlyingToken, address(yieldVault), assets); + yieldVault.setBalance(address(vbTokenHarness), assets); + yieldVault.setSlippage(true, slippageAmount); - vbToken.internal_withdrawFromYieldVault( + vbTokenHarness.internal_withdrawFromYieldVault( assets, false, sender, originalTotalSupply, 0, originalTotalSupply - assets ); - assertEq(IERC20(asset).balanceOf(sender), assets); + assertEq(IERC20(underlyingToken).balanceOf(sender), assets); } function testFuzz_setMinimumReservePercentage(uint256 percentage) public { @@ -155,8 +231,4 @@ contract GenericVaultBridgeTokenFuzzTest is Test { vbTokenPart2.setMinimumReservePercentage(percentage); assertEq(vbToken.minimumReservePercentage(), percentage); } - - function _proxify(address logic, address admin, bytes memory initData) internal returns (address proxy) { - proxy = address(new TransparentUpgradeableProxy(logic, admin, initData)); - } } diff --git a/test/integration/AgglayerIntegrationTest.t.sol b/test/integration/AgglayerIntegrationTest.t.sol new file mode 100644 index 00000000..26f2014e --- /dev/null +++ b/test/integration/AgglayerIntegrationTest.t.sol @@ -0,0 +1,921 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test base +import "forge-std/Test.sol"; +import {TestConstants} from "test/base/TestConstants.sol"; +import {ZkEVMCommon} from "test/utils/ZkEVMCommon.sol"; + +// Core contracts +import "src/primary-chain/VaultBridgeToken.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; +import {NativeConverter} from "src/secondary-chain/NativeConverter.sol"; +import {VaultBridgeTokenInitializer} from "src/primary-chain/VaultBridgeTokenInitializer.sol"; +import {GenericVaultBridgeToken} from "src/primary-chain/ethereum/GenericVaultBridgeToken.sol"; +import {VaultBridgeTokenPart2} from "src/primary-chain/VaultBridgeTokenPart2.sol"; +import {GenericNativeConverterAgglayer as GenericNativeConverter} from + "src/secondary-chain/agglayer/GenericNativeConverterAgglayer.sol"; +import {GenericCustomTokenAgglayer as GenericCustomToken} from + "src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol"; + +// OpenZeppelin +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// Mocks +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockVault} from "test/utils/mocks/MockVault.sol"; +import {MockWETH} from "test/utils/mocks/MockWETH.sol"; +import {MockERC20Upgradeable} from "test/utils/mocks/MockERC20Upgradeable.sol"; +import {MockERC20} from "test/utils/mocks/MockERC20.sol"; +import {MockLxlyBridgeWrappedToken} from "test/utils/mocks/MockLxlyBridgeWrappedToken.sol"; + +// Interfaces +import {IBridgeL2SovereignChain} from "test/interfaces/IBridgeL2SovereignChain.sol"; +import {IAgglayerBridge as _IAgglayerBridge} from "test/interfaces/IAgglayerBridge.sol"; +import {IPolygonZkEVMGlobalExitRoot} from "test/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; + +contract AgglayerIntegrationTest is TestConstants, ZkEVMCommon { + // ===== STRUCTS ===== + struct ClaimPayload { + bytes32[32] proofPrimaryChain; + bytes32[32] proofSecondaryChain; + uint256 globalIndex; + bytes32 exitRootPrimaryChain; + bytes32 exitRootSecondaryChain; + uint32 originNetwork; + address originAddress; + uint32 destinationNetwork; + address destinationAddress; + uint256 amount; + bytes metadata; + } + + struct LeafPayload { + uint8 leafType; + uint32 originNetwork; + address originAddress; + uint32 destinationNetwork; + address destinationAddress; + uint256 amount; + bytes metadata; + } + + // ===== FORK IDs ===== + uint256 forkIdPrimaryChain; + uint256 forkIdSecondaryChain; + + // ===== TEST ADDRESSES ===== + address recipient = makeAddr("recipient"); + address owner = makeAddr("owner"); + address yieldRecipient = makeAddr("yieldRecipient"); + address sender = vm.addr(senderPrivateKey); + + // ===== CORE CONTRACTS ===== + GenericVaultBridgeToken vbToken; + VaultBridgeTokenPart2 vbTokenPart2; + GenericCustomToken customToken; + GenericNativeConverter nativeConverter; + MigrationManager migrationManager; + + // ===== EXTERNAL CONTRACTS ===== + MockVault vbTokenVault; + MockWETH wrappedGasToken; + + // ===== TOKEN CONTRACTS ===== + MockERC20 underlyingAsset; + MockERC20 bwUnderlyingAsset; + MockLxlyBridgeWrappedToken bwVbToken; + + // ===== METADATA ===== + bytes vbTokenMetaData = abi.encode(VBTOKEN_NAME, VBTOKEN_SYMBOL, VBTOKEN_DECIMALS); + bytes bwVbTokenMetaData = abi.encode("", "", 18); + + function setUp() public virtual { + ////////////////////////////////////////////////////////////// + // Primary Chain + ////////////////////////////////////////////////////////////// + forkIdPrimaryChain = vm.createSelectFork("sepolia"); + + // deploy underlying asset + underlyingAsset = new MockERC20(UNDERLYING_ASSET_NAME, UNDERLYING_ASSET_SYMBOL, UNDERLYING_ASSET_DECIMALS); + + // deploy vault + vbTokenVault = new MockVault(address(underlyingAsset)); + vbTokenVault.setMaxDeposit(MAX_DEPOSIT); + vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); + + // calculate native converter address + uint256 nativeConverterNonce = vm.getNonce(address(this)) + 11; + address nativeConverterAddr = vm.computeCreateAddress(address(this), nativeConverterNonce); + + address initializer = address(new VaultBridgeTokenInitializer()); + + // calculate migration manager address + uint256 migrationManagerNonce = vm.getNonce(address(this)) + 5; + address migrationManagerAddr = vm.computeCreateAddress(address(this), migrationManagerNonce); + + // deploy vbToken part 2 + vbTokenPart2 = new VaultBridgeTokenPart2(); + + // deploy vbToken + vbToken = new GenericVaultBridgeToken(); + VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ + owner: owner, + name: VBTOKEN_NAME, + symbol: VBTOKEN_SYMBOL, + underlyingToken: address(underlyingAsset), + minimumReservePercentage: MINIMUM_RESERVE_PERCENTAGE, + yieldVault: address(vbTokenVault), + yieldRecipient: yieldRecipient, + agglayerBridge: LXLY_BRIDGE_X, + minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT_INTEGRATION, + migrationManager: migrationManagerAddr, + yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, + vaultBridgeTokenPart2: address(vbTokenPart2) + }); + bytes memory vbTokenInitData = abi.encodeCall(vbToken.reinitialize1, (initializer, initParams)); + vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbToken), address(this), vbTokenInitData))); + vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); + + uint32[] memory secondaryChainAgglayerIds = new uint32[](1); + secondaryChainAgglayerIds[0] = NETWORK_ID_Y; + address[] memory nativeConverters = new address[](1); + nativeConverters[0] = nativeConverterAddr; + + // deploy migration manager + wrappedGasToken = new MockWETH(); + + MigrationManager migrationManagerImpl = new MigrationManager(); + bytes memory migrationManagerInitData = abi.encodeCall(MigrationManager.reinitialize1, (owner, LXLY_BRIDGE_X)); + migrationManager = + MigrationManager(payable(_proxify(address(migrationManagerImpl), address(this), migrationManagerInitData))); + migrationManager.reinitialize2(address(wrappedGasToken)); + + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + assertEq(migrationManagerAddr, address(migrationManager)); + + ////////////////////////////////////////////////////////////// + // Switch to Secondary Chain + ////////////////////////////////////////////////////////////// + forkIdSecondaryChain = vm.createSelectFork("bokuto"); + + // deploy custom token + MockERC20Upgradeable customTokenBridgeImpl = new MockERC20Upgradeable(); + TransparentUpgradeableProxy customTokenProxy = TransparentUpgradeableProxy( + payable( + _proxify( + address(customTokenBridgeImpl), + address(this), + abi.encodeCall(MockERC20Upgradeable.initialize, (CUSTOM_TOKEN_NAME, CUSTOM_TOKEN_SYMBOL)) + ) + ) + ); + + GenericCustomToken genericCustomTokenImpl = new GenericCustomToken(); + bytes memory initData = abi.encodeCall( + GenericCustomToken.reinitialize2, (owner, CUSTOM_TOKEN_DECIMALS, LXLY_BRIDGE_Y, nativeConverterAddr) + ); + bytes memory upgradeData = + abi.encodeCall(ITransparentUpgradeableProxy.upgradeToAndCall, (address(genericCustomTokenImpl), initData)); + vm.prank(_getAdmin(address(customTokenProxy))); + (address(customTokenProxy).call(upgradeData)); + customToken = GenericCustomToken(address(customTokenProxy)); + + // calculate bridge wrapped vbToken address + bwVbToken = MockLxlyBridgeWrappedToken( + _IAgglayerBridge(LXLY_BRIDGE_Y).computeTokenProxyAddress(NETWORK_ID_X, address(vbToken)) + ); + + // deploy underlying token (note: normally we don't have to do this manually and this should be done automatically by bridging vbToken on Primary Chain) + vm.prank(LXLY_BRIDGE_Y); + ERC20 tempBwVbToken = new MockLxlyBridgeWrappedToken(BW_VBTOKEN_NAME, BW_VBTOKEN_SYMBOL, BW_VBTOKEN_DECIMALS); + vm.etch(address(bwVbToken), address(tempBwVbToken).code); + + // calculate bridge wrapped underlying asset address + bwUnderlyingAsset = + MockERC20(_IAgglayerBridge(LXLY_BRIDGE_Y).computeTokenProxyAddress(NETWORK_ID_X, address(underlyingAsset))); + + // deploy the bridge wrapped underlying asset (note: normally we don't have to do this manually and this should be done automatically by bridging underlying asset on Primary Chain) + vm.prank(LXLY_BRIDGE_Y); + ERC20 tempBwUnderlyingAsset = new MockLxlyBridgeWrappedToken( + BW_UNDERLYING_ASSET_NAME, BW_UNDERLYING_ASSET_SYMBOL, BW_UNDERLYING_ASSET_DECIMALS + ); + vm.etch(address(bwUnderlyingAsset), address(tempBwUnderlyingAsset).code); + + // deploy native converter + nativeConverter = new GenericNativeConverter(); + bytes memory nativeConverterInitData = abi.encodeCall( + GenericNativeConverter(nativeConverter).reinitialize1, + ( + owner, + address(customToken), + address(bwVbToken), + LXLY_BRIDGE_Y, + NETWORK_ID_X, + MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, + address(migrationManager) + ) + ); + nativeConverter = + GenericNativeConverter(_proxify(address(nativeConverter), address(this), nativeConverterInitData)); + assertEq(nativeConverterAddr, address(nativeConverter)); + + ////////////////////////////////////////////////////////////// + // Primary Chain + ////////////////////////////////////////////////////////////// + vm.selectFork(forkIdPrimaryChain); + + vm.label(BRIDGE_MANAGER, "Bridge Manager"); + vm.label(address(customToken), "Custom Token"); + vm.label(address(this), "Default Address"); + vm.label(GER_X, "GlobalExitRoot Primary Chain"); + vm.label(GER_Y, "GlobalExitRoot Secondary Chain"); + vm.label(LXLY_BRIDGE_X, "Agglayer Bridge X"); + vm.label(LXLY_BRIDGE_Y, "Agglayer Bridge Y"); + vm.label(address(nativeConverter), "Native Converter"); + vm.label(address(owner), "Owner"); + vm.label(address(recipient), "Recipient"); + vm.label(address(sender), "Sender"); + vm.label(address(underlyingAsset), "Underlying Asset"); + vm.label(address(bwUnderlyingAsset), "Bridge Wrapped Underlying Asset"); + vm.label(address(bwVbToken), "Underlying Wrapped Asset"); + vm.label(address(vbToken), "vbToken"); + vm.label(address(vbTokenVault), "vbToken Vault"); + vm.label(address(yieldRecipient), "Yield Recipient"); + vm.label(address(migrationManager), "Migration Manager"); + } + + function test_depositAndBridge_bridgeWrappedMapping() public { + uint256 depositAmount = 100; + + LeafPayload memory depositLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_Y, + destinationAddress: recipient, + amount: depositAmount, + metadata: vbTokenMetaData + }); + + vm.selectFork(forkIdPrimaryChain); + + deal(address(underlyingAsset), sender, depositAmount); // fund sender + _depositAndBridgePrimaryChain(sender, depositAmount, depositLeaf); + + bytes32 lastPrimaryChainExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); + ClaimPayload memory claimPayload = _getClaimPayloadPrimaryChain(depositLeaf, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdSecondaryChain); + + // map the bridge wrapped vbToken to the vbToken (simulating the natural bridging process, no need in real life) + _mapTokenSecondaryChainToPrimaryChain(address(vbToken), address(bwVbToken), false); + _claimAndVerifyAssetSecondaryChain(bwVbToken, claimPayload); + } + + function test_depositAndBridge_underlyingTokenMapping() public { + uint256 depositAmount = 100; + + LeafPayload memory leaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_Y, + destinationAddress: recipient, + amount: depositAmount, + metadata: vbTokenMetaData + }); + + vm.selectFork(forkIdPrimaryChain); + + deal(address(underlyingAsset), sender, depositAmount); // fund sender + _depositAndBridgePrimaryChain(sender, depositAmount, leaf); + + bytes32 lastPrimaryChainExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); + ClaimPayload memory claimPayload = _getClaimPayloadPrimaryChain(leaf, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdSecondaryChain); + + // map the underlying token to the vbToken and claim it + _mapTokenSecondaryChainToPrimaryChain(address(vbToken), address(bwUnderlyingAsset), false); + _claimAndVerifyAssetSecondaryChain(bwUnderlyingAsset, claimPayload); + } + + // Add test for not being able to withdraw the needed amount from external vault + // Add another test where vault maxWithdraw works + function test_claimAndRedeem_customTokenMapping() public { + uint256 depositAmount = 1000; + + vm.selectFork(forkIdPrimaryChain); + + // create backing on the bridge on Primary Chain + deal(address(underlyingAsset), sender, depositAmount); + LeafPayload memory depositLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_Y, + destinationAddress: recipient, + amount: depositAmount, + metadata: vbTokenMetaData + }); + _depositAndBridgePrimaryChain(sender, depositAmount, depositLeaf); + bytes32 lastPrimaryChainExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); + ClaimPayload memory depositClaimPayload = _getClaimPayloadPrimaryChain(depositLeaf, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdSecondaryChain); + uint256 withdrawAmount = 100; + _mapTokenSecondaryChainToPrimaryChain(address(vbToken), address(bwVbToken), false); + deal(address(bwVbToken), sender, withdrawAmount); + + _claimAndVerifyAssetSecondaryChain(bwVbToken, depositClaimPayload); + + vm.startPrank(sender); + bwVbToken.approve(LXLY_BRIDGE_Y, withdrawAmount); + + // make the withdrawal leaf + LeafPayload memory withdrawLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_X, + destinationAddress: address(vbToken), + amount: withdrawAmount, + metadata: bwVbTokenMetaData + }); + + // bridge the custom token + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + withdrawLeaf.leafType, + withdrawLeaf.originNetwork, + withdrawLeaf.originAddress, + withdrawLeaf.destinationNetwork, + withdrawLeaf.destinationAddress, + withdrawLeaf.amount, + withdrawLeaf.metadata, + _IAgglayerBridge(LXLY_BRIDGE_Y).depositCount() + ); + IAgglayerBridge(LXLY_BRIDGE_Y).bridgeAsset( + NETWORK_ID_X, address(vbToken), withdrawAmount, address(bwVbToken), true, "" + ); + assertEq(bwVbToken.balanceOf(LXLY_BRIDGE_Y), 0); + vm.stopPrank(); + + LeafPayload[] memory leafPayloads = new LeafPayload[](1); + leafPayloads[0] = withdrawLeaf; + ClaimPayload[] memory withdrawClaimPayload = + _getClaimPayloadsSecondaryChain(leafPayloads, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdPrimaryChain); + + _claimAndRedeemPrimaryChainAndVerify(withdrawClaimPayload[0]); + } + + function test_deconvertAndBridge_bridgeWrappedMapping() public { + uint256 amount = 100; + + vm.selectFork(forkIdPrimaryChain); + + // create liquidity on the bridge on Primary Chain + deal(address(underlyingAsset), sender, amount); + LeafPayload memory depositLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_Y, + destinationAddress: recipient, + amount: amount, + metadata: vbTokenMetaData + }); + _depositAndBridgePrimaryChain(sender, amount, depositLeaf); + bytes32 lastPrimaryChainExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); + ClaimPayload memory depositClaimPayload = _getClaimPayloadPrimaryChain(depositLeaf, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdSecondaryChain); + + uint256 convertAmount = 100; + + _mapTokenSecondaryChainToPrimaryChain(address(vbToken), address(bwVbToken), false); + _claimAndVerifyAssetSecondaryChain(bwVbToken, depositClaimPayload); // claim the bridge wrapped vbToken + + // create backing on the bridge on Secondary Chain: necessary for deconversion + uint256 backingOnSecondaryChain = 0; + deal(address(bwVbToken), owner, convertAmount); + vm.startPrank(owner); + bwVbToken.approve(address(nativeConverter), convertAmount); + backingOnSecondaryChain = nativeConverter.convert(convertAmount, recipient); + vm.stopPrank(); + + deal(address(customToken), sender, convertAmount); + + LeafPayload memory withdrawLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_X, + destinationAddress: recipient, + amount: convertAmount, + metadata: bwVbTokenMetaData // deconversion would give us back the bwVbToken so we'll bridge it back to Primary Chain + }); + _deconvertAndBridgeSecondaryChain(sender, convertAmount, withdrawLeaf); + + LeafPayload[] memory leafPayloads = new LeafPayload[](1); + leafPayloads[0] = withdrawLeaf; + ClaimPayload[] memory withdrawClaimPayload = + _getClaimPayloadsSecondaryChain(leafPayloads, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdPrimaryChain); + + _claimAndVerifyAssetPrimaryChain(vbToken, withdrawClaimPayload[0]); + } + + function test_migrateBackingToPrimaryChain() public { + uint256 amount = 100; + + uint256 vbTokenTotalSupplyBefore = vbToken.totalSupply(); + + vm.selectFork(forkIdPrimaryChain); + + // create liquidity on the bridge on Primary Chain + deal(address(underlyingAsset), sender, amount); + LeafPayload memory depositLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_Y, + destinationAddress: recipient, + amount: amount, + metadata: vbTokenMetaData + }); + _depositAndBridgePrimaryChain(sender, amount, depositLeaf); + bytes32 lastPrimaryChainExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); + ClaimPayload memory depositClaimPayload = _getClaimPayloadPrimaryChain(depositLeaf, lastPrimaryChainExitRoot); + + vm.selectFork(forkIdSecondaryChain); + + uint256 convertAmount = 100; + + _mapTokenSecondaryChainToPrimaryChain(address(vbToken), address(bwVbToken), false); + _claimAndVerifyAssetSecondaryChain(bwVbToken, depositClaimPayload); // claim the bridge wrapped vbToken + + // create backing on the native converter on Secondary Chain + uint256 backingOnSecondaryChain = 0; + deal(address(bwVbToken), owner, convertAmount); + vm.startPrank(owner); + bwVbToken.approve(address(nativeConverter), convertAmount); + backingOnSecondaryChain = nativeConverter.convert(convertAmount, recipient); + vm.stopPrank(); + + uint256 maxNonMigratableBacking = backingOnSecondaryChain * MAX_NON_MIGRATABLE_BACKING_PERCENTAGE / 1e18; + uint256 amountToMigrate = backingOnSecondaryChain - maxNonMigratableBacking; + + // make the migration leaves + LeafPayload memory assetLeaf = LeafPayload({ + leafType: LEAF_TYPE_ASSET, + originNetwork: NETWORK_ID_X, + originAddress: address(vbToken), + destinationNetwork: NETWORK_ID_X, + destinationAddress: address(migrationManager), + amount: amountToMigrate, + metadata: bwVbTokenMetaData + }); + + LeafPayload memory messageLeaf = LeafPayload({ + leafType: LEAF_TYPE_MESSAGE, + originNetwork: NETWORK_ID_Y, + originAddress: address(nativeConverter), + destinationNetwork: NETWORK_ID_X, + destinationAddress: address(migrationManager), + amount: 0, + metadata: abi.encode( + MigrationManager.CrossChainInstruction._0_COMPLETE_MIGRATION, abi.encode(amountToMigrate, amountToMigrate) + ) + }); + + // migrate backing to Primary Chain + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + assetLeaf.leafType, + assetLeaf.originNetwork, + assetLeaf.originAddress, + assetLeaf.destinationNetwork, + assetLeaf.destinationAddress, + assetLeaf.amount, + assetLeaf.metadata, + _IAgglayerBridge(LXLY_BRIDGE_Y).depositCount() + ); + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + messageLeaf.leafType, + messageLeaf.originNetwork, + messageLeaf.originAddress, + messageLeaf.destinationNetwork, + messageLeaf.destinationAddress, + messageLeaf.amount, + messageLeaf.metadata, + _IAgglayerBridge(LXLY_BRIDGE_Y).depositCount() + 1 + ); + vm.expectEmit(); + emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); + vm.prank(owner); + nativeConverter.migrateBackingToPrimaryChain(amountToMigrate); + + LeafPayload[] memory leafPayloads = new LeafPayload[](2); + leafPayloads[0] = assetLeaf; + leafPayloads[1] = messageLeaf; + ClaimPayload[] memory claimPayloads = _getClaimPayloadsSecondaryChain(leafPayloads, lastPrimaryChainExitRoot); + + // switch to Primary Chain + vm.selectFork(forkIdPrimaryChain); + + // Fund the Migration Manager with the underlying asset + deal(address(underlyingAsset), address(migrationManager), amountToMigrate); + + // claim and withdraw on Primary Chain + _claimAndVerifyAssetPrimaryChain(vbToken, claimPayloads[0]); + _claimMessagePrimaryChain(claimPayloads[1]); + + uint256 vbTokenTotalSupplyAfter = vbToken.totalSupply(); + assertGt(vbTokenTotalSupplyAfter, vbTokenTotalSupplyBefore); + } + + function _depositAndBridgePrimaryChain(address _sender, uint256 _amount, LeafPayload memory _leaf) internal { + // make sure we are on Primary Chain + assertEq(vm.activeFork(), forkIdPrimaryChain); + + vm.startPrank(_sender); + + // approve underlying asset + vbToken.underlyingToken().approve(address(vbToken), _amount); + + // deposit and bridge + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + _leaf.leafType, + _leaf.originNetwork, + _leaf.originAddress, + _leaf.destinationNetwork, + _leaf.destinationAddress, + _leaf.amount, + _leaf.metadata, + _IAgglayerBridge(LXLY_BRIDGE_X).depositCount() + ); + vbToken.depositAndBridge(_amount, _leaf.destinationAddress, _leaf.destinationNetwork, true); + + vm.stopPrank(); + + // assert balances + vm.assertEq(vbToken.underlyingToken().balanceOf(_sender), 0); + vm.assertEq(vbToken.balanceOf(LXLY_BRIDGE_X), _amount); // shares locked in the bridge + } + + function _mapTokenSecondaryChainToPrimaryChain( + address _originTokenAddress, + address _sovereignTokenAddress, + bool _isNotMintable + ) internal { + // make sure we are on Secondary Chain + assertEq(vm.activeFork(), forkIdSecondaryChain); + + uint32[] memory originNetworks = new uint32[](1); + originNetworks[0] = NETWORK_ID_X; + address[] memory originTokenAddresses = new address[](1); + originTokenAddresses[0] = _originTokenAddress; + address[] memory sovereignTokenAddresses = new address[](1); + sovereignTokenAddresses[0] = _sovereignTokenAddress; + bool[] memory isNotMintable = new bool[](1); + isNotMintable[0] = _isNotMintable; + + vm.prank(BRIDGE_MANAGER); + IBridgeL2SovereignChain(LXLY_BRIDGE_Y).setMultipleSovereignTokenAddress( + originNetworks, originTokenAddresses, sovereignTokenAddresses, isNotMintable + ); + } + + function _getClaimPayloadPrimaryChain(LeafPayload memory _leaf, bytes32 lastMainnetExitRoot) + internal + returns (ClaimPayload memory) + { + // make sure we are on Primary Chain + assertEq(vm.activeFork(), forkIdPrimaryChain); + + // simulate the Merkle trees on Primary Chain + bytes32[] memory merkleTreePrimaryChain = new bytes32[](1); + merkleTreePrimaryChain[0] = _IAgglayerBridge(LXLY_BRIDGE_X).getLeafValue( + _leaf.leafType, + _leaf.originNetwork, + _leaf.originAddress, + _leaf.destinationNetwork, + _leaf.destinationAddress, + _leaf.amount, + keccak256(abi.encodePacked(_leaf.metadata)) + ); + + // Primary Chain leaf index + uint256 leafIndexPrimaryChain = 0; + + // Primary Chain Merkle tree root + bytes32 merkleTreeRootPrimaryChain = _getMerkleTreeRoot(_encodeLeaves(merkleTreePrimaryChain)); + + // Primary Chain proof + bytes32[32] memory proofPrimaryChain = + _getProofByIndex(_encodeLeaves(merkleTreePrimaryChain), vm.toString(leafIndexPrimaryChain)); + + // simulate the Merkle tree on Secondary Chain + bytes32[] memory merkleTreeSecondaryChain = new bytes32[](1); + merkleTreeSecondaryChain[0] = merkleTreeRootPrimaryChain; + + // Secondary Chain leaf index + uint256 leafIndexSecondaryChain = 0; + + // Secondary Chain Merkle tree root + bytes32 merkleTreeRootSecondaryChain = _getMerkleTreeRoot(_encodeLeaves(merkleTreeSecondaryChain)); + + // Secondary Chain proof + bytes32[32] memory proofSecondaryChain = + _getProofByIndex(_encodeLeaves(merkleTreeSecondaryChain), vm.toString(leafIndexSecondaryChain)); + + return ClaimPayload({ + proofPrimaryChain: proofPrimaryChain, + proofSecondaryChain: proofSecondaryChain, + globalIndex: _computeGlobalIndex(leafIndexPrimaryChain, leafIndexSecondaryChain, false), + exitRootPrimaryChain: lastMainnetExitRoot, + exitRootSecondaryChain: merkleTreeRootSecondaryChain, + originNetwork: _leaf.originNetwork, + originAddress: _leaf.originAddress, + destinationNetwork: _leaf.destinationNetwork, + destinationAddress: _leaf.destinationAddress, + amount: _leaf.amount, + metadata: _leaf.metadata + }); + } + + function _getClaimPayloadsSecondaryChain(LeafPayload[] memory _leaves, bytes32 lastMainnetExitRoot) + internal + returns (ClaimPayload[] memory) + { + // make sure we are on Secondary Chain + assertEq(vm.activeFork(), forkIdSecondaryChain); + + // simulate the Merkle trees on Secondary Chain + bytes32[] memory merkleTreePrimaryChain = new bytes32[](_leaves.length); + for (uint256 i = 0; i < _leaves.length; i++) { + merkleTreePrimaryChain[i] = _IAgglayerBridge(LXLY_BRIDGE_Y).getLeafValue( + _leaves[i].leafType, + _leaves[i].originNetwork, + _leaves[i].originAddress, + _leaves[i].destinationNetwork, + _leaves[i].destinationAddress, + _leaves[i].amount, + keccak256(abi.encodePacked(_leaves[i].metadata)) + ); + } + + // Primary Chain Merkle tree root + bytes32 merkleTreeRootPrimaryChain = _getMerkleTreeRoot(_encodeLeaves(merkleTreePrimaryChain)); + + bytes32[] memory merkleTreeSecondaryChain = new bytes32[](2); + merkleTreeSecondaryChain[0] = merkleTreeRootPrimaryChain; + merkleTreeSecondaryChain[1] = merkleTreeRootPrimaryChain; + + // Secondary Chain Merkle tree root + bytes32 merkleExitRootSecondaryChain = _getMerkleTreeRoot(_encodeLeaves(merkleTreeSecondaryChain)); + + ClaimPayload[] memory claimPayloads = new ClaimPayload[](_leaves.length); + for (uint256 i = 0; i < _leaves.length; i++) { + LeafPayload memory leaf = _leaves[i]; + + // Primary Chain leaf index + uint256 leafIndexPrimaryChain = i; + + // proof for Primary Chain + bytes32[32] memory proofPrimaryChain = + _getProofByIndex(_encodeLeaves(merkleTreePrimaryChain), vm.toString(leafIndexPrimaryChain)); + + // Secondary Chain leaf index + uint256 leafIndexSecondaryChain = i; + + // proof for Secondary Chain + bytes32[32] memory proofSecondaryChain = + _getProofByIndex(_encodeLeaves(merkleTreeSecondaryChain), vm.toString(leafIndexSecondaryChain)); + + claimPayloads[i] = ClaimPayload({ + proofPrimaryChain: proofPrimaryChain, + proofSecondaryChain: proofSecondaryChain, + globalIndex: _computeGlobalIndex(leafIndexPrimaryChain, leafIndexSecondaryChain, false), + exitRootPrimaryChain: lastMainnetExitRoot, + exitRootSecondaryChain: merkleExitRootSecondaryChain, + originNetwork: leaf.originNetwork, + originAddress: leaf.originAddress, + destinationNetwork: leaf.destinationNetwork, + destinationAddress: leaf.destinationAddress, + amount: leaf.amount, + metadata: leaf.metadata + }); + } + + return claimPayloads; + } + + function _claimAndVerifyAssetPrimaryChain(IERC20 _token, ClaimPayload memory _claimPayload) internal { + // make sure we are on Primary Chain + assertEq(vm.activeFork(), forkIdPrimaryChain); + + // update Primary Chain exit root + vm.prank(address(ROLLUP_MANAGER)); + IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootSecondaryChain); + + // claim asset on Primary Chain + vm.expectEmit(); + emit MockAgglayerBridge.ClaimEvent( + _claimPayload.globalIndex, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationAddress, + _claimPayload.amount + ); + IAgglayerBridge(LXLY_BRIDGE_X).claimAsset( + _claimPayload.proofPrimaryChain, + _claimPayload.proofSecondaryChain, + _claimPayload.globalIndex, + _claimPayload.exitRootPrimaryChain, + _claimPayload.exitRootSecondaryChain, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationNetwork, + _claimPayload.destinationAddress, + _claimPayload.amount, + _claimPayload.metadata + ); + + // assert balances + assertEq(_token.balanceOf(_claimPayload.destinationAddress), _claimPayload.amount); + } + + function _claimMessagePrimaryChain(ClaimPayload memory _claimPayload) internal { + // make sure we are on Primary Chain + assertEq(vm.activeFork(), forkIdPrimaryChain); + + // update Primary Chain exit root + vm.prank(address(ROLLUP_MANAGER)); + IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootSecondaryChain); + + // claim asset on Primary Chain + vm.expectEmit(); + emit MockAgglayerBridge.ClaimEvent( + _claimPayload.globalIndex, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationAddress, + _claimPayload.amount + ); + IAgglayerBridge(LXLY_BRIDGE_X).claimMessage( + _claimPayload.proofPrimaryChain, + _claimPayload.proofSecondaryChain, + _claimPayload.globalIndex, + _claimPayload.exitRootPrimaryChain, + _claimPayload.exitRootSecondaryChain, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationNetwork, + _claimPayload.destinationAddress, + _claimPayload.amount, + _claimPayload.metadata + ); + } + + function _claimAndVerifyAssetSecondaryChain(IERC20 _token, ClaimPayload memory _claimPayload) internal { + // make sure we are on Secondary Chain + assertEq(vm.activeFork(), forkIdSecondaryChain); + + // update Secondary Chain exit root + vm.prank(address(LXLY_BRIDGE_Y)); + IPolygonZkEVMGlobalExitRoot(GER_Y).updateExitRoot(_claimPayload.exitRootSecondaryChain); + + // insert Secondary Chain global exit root + vm.prank(GER_Y_UPDATER); + IPolygonZkEVMGlobalExitRoot(GER_Y).insertGlobalExitRoot( + _calculateGlobalExitRoot(_claimPayload.exitRootPrimaryChain, _claimPayload.exitRootSecondaryChain) + ); + + // claim asset on Secondary Chain + vm.expectEmit(); + emit MockAgglayerBridge.ClaimEvent( + _claimPayload.globalIndex, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationAddress, + _claimPayload.amount + ); + IAgglayerBridge(LXLY_BRIDGE_Y).claimAsset( + _claimPayload.proofPrimaryChain, + _claimPayload.proofSecondaryChain, + _claimPayload.globalIndex, + _claimPayload.exitRootPrimaryChain, + _claimPayload.exitRootSecondaryChain, + _claimPayload.originNetwork, + _claimPayload.originAddress, + _claimPayload.destinationNetwork, + _claimPayload.destinationAddress, + _claimPayload.amount, + _claimPayload.metadata + ); + + // assert balances + assertEq(_token.balanceOf(_claimPayload.destinationAddress), _claimPayload.amount); + } + + function _claimAndRedeemPrimaryChainAndVerify(ClaimPayload memory _claimPayload) internal { + // make sure we are on Primary Chain + assertEq(vm.activeFork(), forkIdPrimaryChain); + + // update Primary Chain exit root + vm.prank(address(ROLLUP_MANAGER)); + IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootSecondaryChain); + + // claim and withdraw on Primary Chain + vm.prank(address(vbToken)); + vbToken.approve(recipient, _claimPayload.amount); + + vm.prank(recipient); + vbToken.claimAndRedeem( + _claimPayload.proofPrimaryChain, + _claimPayload.proofSecondaryChain, + _claimPayload.globalIndex, + _claimPayload.exitRootPrimaryChain, + _claimPayload.exitRootSecondaryChain, + _claimPayload.destinationAddress, + _claimPayload.amount, + recipient, + _claimPayload.metadata + ); + + assertEq(vbToken.underlyingToken().balanceOf(recipient), _claimPayload.amount); + } + + function _deconvertAndBridgeSecondaryChain(address _sender, uint256 _amount, LeafPayload memory _leaf) internal { + // make sure we are on Secondary Chain + assertEq(vm.activeFork(), forkIdSecondaryChain); + + vm.startPrank(_sender); + + // approve the custom token + nativeConverter.customToken().approve(address(nativeConverter), _amount); + + // deconvert and bridge + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + _leaf.leafType, + _leaf.originNetwork, + _leaf.originAddress, + _leaf.destinationNetwork, + _leaf.destinationAddress, + _leaf.amount, + _leaf.metadata, + _IAgglayerBridge(LXLY_BRIDGE_Y).depositCount() + ); + + nativeConverter.deconvertAndBridge(_amount, _leaf.destinationAddress, _leaf.destinationNetwork, true); + + vm.stopPrank(); + + // assert balances + vm.assertEq(nativeConverter.customToken().balanceOf(_sender), 0); + vm.assertEq(nativeConverter.underlyingToken().balanceOf(LXLY_BRIDGE_Y), 0); + } + + function _computeGlobalIndex(uint256 indexPrimaryChain, uint256 indexSecondaryChain, bool isPrimaryChain) + internal + pure + returns (uint256) + { + if (isPrimaryChain) { + return indexPrimaryChain + 2 ** 64; + } else { + return indexPrimaryChain + indexSecondaryChain * 2 ** 32; + } + } + + function _calculateGlobalExitRoot(bytes32 exitRootPrimaryChain, bytes32 exitRootSecondaryChain) + internal + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(exitRootPrimaryChain, exitRootSecondaryChain)); + } + + function _getAdmin(address target) internal view returns (address) { + bytes32 ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 value = vm.load(target, ADMIN_SLOT); + return address(uint160(uint256(value))); + } +} diff --git a/test/integration/Integration.t.sol b/test/integration/Integration.t.sol deleted file mode 100644 index 7806a6f8..00000000 --- a/test/integration/Integration.t.sol +++ /dev/null @@ -1,1012 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import "forge-std/Test.sol"; -import "src/VaultBridgeToken.sol"; - -import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import {CustomToken} from "src/CustomToken.sol"; -import {MigrationManager} from "src/MigrationManager.sol"; -import {NativeConverter} from "src/NativeConverter.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {TestVault} from "test/etc/TestVault.sol"; -import {ZkEVMCommon} from "test/etc/ZkEVMCommon.sol"; -import {VaultBridgeTokenInitializer} from "src/VaultBridgeTokenInitializer.sol"; -import {GenericVaultBridgeToken} from "src/vault-bridge-tokens/GenericVaultBridgeToken.sol"; -import {VaultBridgeTokenPart2} from "src/VaultBridgeTokenPart2.sol"; -import {GenericNativeConverter} from "src/custom-tokens/GenericNativeConverter.sol"; -import {GenericCustomToken} from "src/custom-tokens/GenericCustomToken.sol"; - -import {IBridgeL2SovereignChain} from "test/interfaces/IBridgeL2SovereignChain.sol"; -import {ILxLyBridge as _ILxLyBridge} from "test/interfaces/ILxLyBridge.sol"; -import {IPolygonZkEVMGlobalExitRoot} from "test/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; - -contract UnderlyingAsset is ERC20 { - constructor(string memory name, string memory symbol) ERC20(name, symbol) {} -} - -contract TokenWrapped is ERC20 { - // Domain typehash - bytes32 public constant DOMAIN_TYPEHASH = - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); - // Permit typehash - bytes32 public constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - - // Version - string public constant VERSION = "1"; - - // Chain id on deployment - uint256 public immutable deploymentChainId; - - // Domain separator calculated on deployment - bytes32 private immutable _DEPLOYMENT_DOMAIN_SEPARATOR; - - // PolygonZkEVM Bridge address - address public immutable bridgeAddress; - - // Decimals - uint8 private immutable _decimals; - - // Permit nonces - mapping(address => uint256) public nonces; - - modifier onlyBridge() { - require(msg.sender == bridgeAddress, "TokenWrapped::onlyBridge: Not PolygonZkEVMBridge"); - _; - } - - constructor(string memory name, string memory symbol, uint8 __decimals) ERC20(name, symbol) { - bridgeAddress = msg.sender; - _decimals = __decimals; - deploymentChainId = block.chainid; - _DEPLOYMENT_DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid); - } - - function mint(address to, uint256 value) external onlyBridge { - _mint(to, value); - } - - // Notice that is not require to approve wrapped tokens to use the bridge - function burn(address account, uint256 value) external onlyBridge { - _burn(account, value); - } - - function decimals() public view virtual override returns (uint8) { - return _decimals; - } - - // Permit relative functions - function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) - external - { - require(block.timestamp <= deadline, "TokenWrapped::permit: Expired permit"); - - bytes32 hashStruct = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)); - - bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), hashStruct)); - - address signer = ecrecover(digest, v, r, s); - require(signer != address(0) && signer == owner, "TokenWrapped::permit: Invalid signature"); - - _approve(owner, spender, value); - } - - /** - * @notice Calculate domain separator, given a chainID. - * @param chainId Current chainID - */ - function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { - return keccak256( - abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name())), keccak256(bytes(VERSION)), chainId, address(this)) - ); - } - - /// @dev Return the DOMAIN_SEPARATOR. - function DOMAIN_SEPARATOR() public view returns (bytes32) { - return - block.chainid == deploymentChainId ? _DEPLOYMENT_DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid); - } -} - -contract IntegrationTest is Test, ZkEVMCommon { - struct ClaimPayload { - bytes32[32] proofLayerX; - bytes32[32] proofLayerY; - uint256 globalIndex; - bytes32 exitRootLayerX; - bytes32 exitRootLayerY; - uint32 originNetwork; - address originAddress; - uint32 destinationNetwork; - address destinationAddress; - uint256 amount; - bytes metadata; - } - - struct LeafPayload { - uint8 leafType; - uint32 originNetwork; - address originAddress; - uint32 destinationNetwork; - address destinationAddress; - uint256 amount; - bytes metadata; - } - - address internal constant BRIDGE_MANAGER = 0x165BD6204Df6A4C47875D62582dc7C1Ed6477c17; - address constant LXLY_BRIDGE_X = 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582; - address constant LXLY_BRIDGE_Y = 0x528e26b25a34a4A5d0dbDa1d57D318153d2ED582; - address constant GER_X = 0xAd1490c248c5d3CbAE399Fd529b79B42984277DF; - address constant GER_Y = 0xa40D5f56745a118D0906a34E69aeC8C0Db1cB8fA; - address constant GER_Y_UPDATER = 0x7d8EB43E982b1aAb2b0cd1084EeF80345D3f92d8; - address constant ROLLUP_MANAGER = 0x32d33D5137a7cFFb54c5Bf8371172bcEc5f310ff; - uint8 constant LEAF_TYPE_ASSET = 0; - uint8 constant LEAF_TYPE_MESSAGE = 1; - uint32 constant NETWORK_ID_X = 0; // mainnet/sepolia - uint32 constant NETWORK_ID_Y = 29; // katana-apex - bytes32 constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes4 constant PERMIT_SIGNATURE = 0xd505accf; - uint256 constant MAX_NON_MIGRATABLE_BACKING_PERCENTAGE = 1e17; - uint256 internal constant MAX_DEPOSIT = 10e18; - uint256 internal constant MAX_WITHDRAW = 10e18; - uint256 internal constant YIELD_VAULT_ALLOWED_SLIPPAGE = 1e16; // 1% - - // extra contracts - TestVault vbTokenVault; - GenericNativeConverter nativeConverter; - MigrationManager migrationManager; - - // dummy addresses - address recipient = makeAddr("recipient"); - address owner = makeAddr("owner"); - address yieldRecipient = makeAddr("yieldRecipient"); - uint256 senderPrivateKey = 0xBEEF; - address sender = vm.addr(senderPrivateKey); - - // underlying asset - UnderlyingAsset underlyingAsset; - string internal constant UNDERLYING_ASSET_NAME = "Underlying Asset"; - string internal constant UNDERLYING_ASSET_SYMBOL = "UAT"; - uint8 internal constant UNDERLYING_ASSET_DECIMALS = 18; - bytes underlyingAssetMetaData = - abi.encode(UNDERLYING_ASSET_NAME, UNDERLYING_ASSET_SYMBOL, UNDERLYING_ASSET_DECIMALS); - - // bridge wrapped underlying asset - UnderlyingAsset bwUnderlyingAsset; - string internal constant BW_UNDERLYING_ASSET_NAME = "Bridge Wrapped Underlying Asset"; - string internal constant BW_UNDERLYING_ASSET_SYMBOL = "BWUAT"; - uint8 internal constant BW_UNDERLYING_ASSET_DECIMALS = 18; - bytes bwUnderlyingAssetMetaData = abi.encode("", "", 18); - - // vbToken - GenericVaultBridgeToken vbToken; - VaultBridgeTokenPart2 vbTokenPart2; - uint256 internal constant MINIMUM_RESERVE_PERCENTAGE = 1e17; - string internal constant VBTOKEN_NAME = "Vault Bridge Token"; - string internal constant VBTOKEN_SYMBOL = "VBTK"; - uint8 internal constant VBTOKEN_DECIMALS = 18; - uint256 internal constant MINIMUM_YIELD_VAULT_DEPOSIT = 1e18; - bytes vbTokenMetaData = abi.encode(VBTOKEN_NAME, VBTOKEN_SYMBOL, VBTOKEN_DECIMALS); - - // custom token - GenericCustomToken customToken; - string internal constant CUSTOM_TOKEN_NAME = "Custom Token"; - string internal constant CUSTOM_TOKEN_SYMBOL = "CT"; - uint8 internal constant CUSTOM_TOKEN_DECIMALS = 18; - bytes customTokenMetaData = abi.encode(CUSTOM_TOKEN_NAME, CUSTOM_TOKEN_SYMBOL, CUSTOM_TOKEN_DECIMALS); - - // bridge wrapped vbToken - TokenWrapped bwVbToken; - string internal constant BW_VBTOKEN_NAME = "Bridge Wrapped VbToken"; - string internal constant BW_VBTOKEN_SYMBOL = "BWVBTK"; - uint8 internal constant BW_VBTOKEN_DECIMALS = 18; - bytes bwVbTokenMetaData = abi.encode("", "", 18); - - uint256 forkIdLayerX; - uint256 forkIdLayerY; - - // error messages - error EnforcedPause(); - - // events - event BridgeEvent( - uint8 leafType, - uint32 originNetwork, - address originAddress, - uint32 destinationNetwork, - address destinationAddress, - uint256 amount, - bytes metadata, - uint32 depositCount - ); - event ClaimEvent( - uint256 globalIndex, uint32 originNetwork, address originAddress, address destinationAddress, uint256 amount - ); - event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); - event ReserveRebalanced(uint256 reservedAssets); - event YieldCollected(address indexed yieldRecipient, uint256 vbTokenAmount); - event YieldRecipientChanged(address indexed yieldRecipient); - event MinimumReservePercentageChanged(uint8 minimumReservePercentage); - event MigrationCompleted( - uint32 indexed destinationNetworkId, - uint256 indexed shares, - uint256 assetsBeforeTransferFee, - uint256 assets, - uint256 usedYield - ); - - function setUp() public virtual { - ////////////////////////////////////////////////////////////// - // Layer X - ////////////////////////////////////////////////////////////// - forkIdLayerX = vm.createSelectFork("sepolia"); - - // deploy underlying asset - underlyingAsset = new UnderlyingAsset(UNDERLYING_ASSET_NAME, UNDERLYING_ASSET_SYMBOL); - - // deploy vault - vbTokenVault = new TestVault(address(underlyingAsset)); - vbTokenVault.setMaxDeposit(MAX_DEPOSIT); - vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); - - // calculate native converter address - uint256 nativeConverterNonce = vm.getNonce(address(this)) + 9; - address nativeConverterAddr = vm.computeCreateAddress(address(this), nativeConverterNonce); - - address initializer = address(new VaultBridgeTokenInitializer()); - - // calculate migration manager address - uint256 migrationManagerNonce = vm.getNonce(address(this)) + 4; - address migrationManagerAddr = vm.computeCreateAddress(address(this), migrationManagerNonce); - - // deploy vbToken part 2 - vbTokenPart2 = new VaultBridgeTokenPart2(); - - // deploy vbToken - vbToken = new GenericVaultBridgeToken(); - VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ - owner: owner, - name: VBTOKEN_NAME, - symbol: VBTOKEN_SYMBOL, - underlyingToken: address(underlyingAsset), - minimumReservePercentage: MINIMUM_RESERVE_PERCENTAGE, - yieldVault: address(vbTokenVault), - yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE_X, - minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManagerAddr, - yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) - }); - bytes memory vbTokenInitData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbToken), address(this), vbTokenInitData))); - vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); - - uint32[] memory layerYLxlyIds = new uint32[](1); - layerYLxlyIds[0] = NETWORK_ID_Y; - address[] memory nativeConverters = new address[](1); - nativeConverters[0] = nativeConverterAddr; - - // deploy migration manager - MigrationManager migrationManagerImpl = new MigrationManager(); - bytes memory migrationManagerInitData = abi.encodeCall(MigrationManager.initialize, (owner, LXLY_BRIDGE_X)); - migrationManager = - MigrationManager(payable(_proxify(address(migrationManagerImpl), address(this), migrationManagerInitData))); - vm.prank(owner); - migrationManager.configureNativeConverters(layerYLxlyIds, nativeConverters, payable(address(vbToken))); - assertEq(migrationManagerAddr, address(migrationManager)); - - ////////////////////////////////////////////////////////////// - // Switch to Layer Y - ////////////////////////////////////////////////////////////// - forkIdLayerY = vm.createSelectFork("tatara"); - - // deploy custom token - customToken = new GenericCustomToken(); - bytes memory customTokenInitData = abi.encodeCall( - GenericCustomToken.reinitialize, - (owner, CUSTOM_TOKEN_NAME, CUSTOM_TOKEN_SYMBOL, CUSTOM_TOKEN_DECIMALS, LXLY_BRIDGE_Y, nativeConverterAddr) - ); - customToken = GenericCustomToken(_proxify(address(customToken), address(this), customTokenInitData)); - - // calculate bridge wrapped vbToken address - bwVbToken = TokenWrapped( - _ILxLyBridge(LXLY_BRIDGE_Y).precalculatedWrapperAddress( - NETWORK_ID_X, address(vbToken), VBTOKEN_NAME, VBTOKEN_SYMBOL, VBTOKEN_DECIMALS - ) - ); - - // deploy underlying token (note: normally we don't have to do this manually and this should be done automatically by bridging vbToken on Layer X) - vm.prank(LXLY_BRIDGE_Y); - ERC20 tempBwVbToken = new TokenWrapped(BW_VBTOKEN_NAME, BW_VBTOKEN_SYMBOL, BW_VBTOKEN_DECIMALS); - vm.etch(address(bwVbToken), address(tempBwVbToken).code); - - // calculate bridge wrapped underlying asset address - bwUnderlyingAsset = UnderlyingAsset( - _ILxLyBridge(LXLY_BRIDGE_Y).precalculatedWrapperAddress( - NETWORK_ID_X, - address(underlyingAsset), - UNDERLYING_ASSET_NAME, - UNDERLYING_ASSET_SYMBOL, - UNDERLYING_ASSET_DECIMALS - ) - ); - - // deploy the bridge wrapped underlying asset (note: normally we don't have to do this manually and this should be done automatically by bridging underlying asset on Layer X) - vm.prank(LXLY_BRIDGE_Y); - ERC20 tempBwUnderlyingAsset = - new TokenWrapped(BW_UNDERLYING_ASSET_NAME, BW_UNDERLYING_ASSET_SYMBOL, BW_UNDERLYING_ASSET_DECIMALS); - vm.etch(address(bwUnderlyingAsset), address(tempBwUnderlyingAsset).code); - - // deploy native converter - nativeConverter = new GenericNativeConverter(); - bytes memory nativeConverterInitData = abi.encodeCall( - GenericNativeConverter(nativeConverter).initialize, - ( - owner, - VBTOKEN_DECIMALS, - address(customToken), - address(bwUnderlyingAsset), - LXLY_BRIDGE_Y, - NETWORK_ID_X, - MAX_NON_MIGRATABLE_BACKING_PERCENTAGE, - address(migrationManager) - ) - ); - nativeConverter = - GenericNativeConverter(_proxify(address(nativeConverter), address(this), nativeConverterInitData)); - assertEq(nativeConverterAddr, address(nativeConverter)); - - ////////////////////////////////////////////////////////////// - // Layer X - ////////////////////////////////////////////////////////////// - vm.selectFork(forkIdLayerX); - - vm.label(BRIDGE_MANAGER, "Bridge Manager"); - vm.label(address(customToken), "Custom Token"); - vm.label(address(this), "Default Address"); - vm.label(GER_X, "GlobalExitRoot Layer X"); - vm.label(GER_Y, "GlobalExitRoot Layer Y"); - vm.label(LXLY_BRIDGE_X, "Lxly Bridge X"); - vm.label(LXLY_BRIDGE_Y, "Lxly Bridge Y"); - vm.label(address(nativeConverter), "Native Converter"); - vm.label(address(owner), "Owner"); - vm.label(address(recipient), "Recipient"); - vm.label(address(sender), "Sender"); - vm.label(address(underlyingAsset), "Underlying Asset"); - vm.label(address(bwUnderlyingAsset), "Bridge Wrapped Underlying Asset"); - vm.label(address(bwVbToken), "Underlying Wrapped Asset"); - vm.label(address(vbToken), "vbToken"); - vm.label(address(vbTokenVault), "vbToken Vault"); - vm.label(address(yieldRecipient), "Yield Recipient"); - vm.label(address(migrationManager), "Migration Manager"); - } - - function test_depositAndBridge_bridgeWrappedMapping() public { - uint256 depositAmount = 100; - - LeafPayload memory depositLeaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(vbToken), - destinationNetwork: NETWORK_ID_Y, - destinationAddress: recipient, - amount: depositAmount, - metadata: vbTokenMetaData - }); - - vm.selectFork(forkIdLayerX); - - deal(address(underlyingAsset), sender, depositAmount); // fund sender - _depositAndBridgeLayerX(sender, depositAmount, depositLeaf); - - bytes32 lastLayerXExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); - ClaimPayload memory claimPayload = _getClaimPayloadLayerX(depositLeaf, lastLayerXExitRoot); - - vm.selectFork(forkIdLayerY); - - // map the bridge wrapped vbToken to the vbToken (simulating the natural bridging process, no need in real life) - _mapTokenLayerYToLayerX(address(vbToken), address(bwVbToken), false); - _claimAndVerifyAssetLayerY(bwVbToken, claimPayload); - } - - function test_depositAndBridge_customTokenMapping() public { - uint256 depositAmount = 100; - - LeafPayload memory leaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(vbToken), - destinationNetwork: NETWORK_ID_Y, - destinationAddress: recipient, - amount: depositAmount, - metadata: vbTokenMetaData - }); - - vm.selectFork(forkIdLayerX); - - deal(address(underlyingAsset), sender, depositAmount); // fund sender - _depositAndBridgeLayerX(sender, depositAmount, leaf); - - bytes32 lastLayerXExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); - ClaimPayload memory claimPayload = _getClaimPayloadLayerX(leaf, lastLayerXExitRoot); - - vm.selectFork(forkIdLayerY); - - // map the custom token to the vbToken - _mapTokenLayerYToLayerX(address(vbToken), address(customToken), false); - _claimAndVerifyAssetLayerY(customToken, claimPayload); - } - - // Add test for not being able to withdraw the needed amount from external vault - // Add another test where vault maxWithdraw works - function test_claimAndRedeem_customTokenMapping() public { - uint256 depositAmount = 1000; - - vm.selectFork(forkIdLayerX); - - // create backing on the bridge on layer X - deal(address(underlyingAsset), sender, depositAmount); - LeafPayload memory depositLeaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(vbToken), - destinationNetwork: NETWORK_ID_Y, - destinationAddress: recipient, - amount: depositAmount, - metadata: vbTokenMetaData - }); - _depositAndBridgeLayerX(sender, depositAmount, depositLeaf); - bytes32 lastLayerXExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); - - vm.selectFork(forkIdLayerY); - - uint256 withdrawAmount = 100; - - _mapTokenLayerYToLayerX(address(vbToken), address(customToken), false); - deal(address(customToken), sender, withdrawAmount); - - vm.startPrank(sender); - customToken.approve(LXLY_BRIDGE_Y, withdrawAmount); - - // make the withdrawal leaf - LeafPayload memory withdrawLeaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(vbToken), - destinationNetwork: NETWORK_ID_X, - destinationAddress: address(vbToken), - amount: withdrawAmount, - metadata: customTokenMetaData - }); - - // bridge the custom token - vm.expectEmit(); - emit BridgeEvent( - withdrawLeaf.leafType, - withdrawLeaf.originNetwork, - withdrawLeaf.originAddress, - withdrawLeaf.destinationNetwork, - withdrawLeaf.destinationAddress, - withdrawLeaf.amount, - withdrawLeaf.metadata, - _ILxLyBridge(LXLY_BRIDGE_Y).depositCount() - ); - ILxLyBridge(LXLY_BRIDGE_Y).bridgeAsset( - NETWORK_ID_X, address(vbToken), withdrawAmount, address(customToken), true, "" - ); - assertEq(customToken.balanceOf(LXLY_BRIDGE_Y), 0); // custom token is burned as it is custom mapped to vbToken - vm.stopPrank(); - - LeafPayload[] memory leafPayloads = new LeafPayload[](1); - leafPayloads[0] = withdrawLeaf; - ClaimPayload[] memory withdrawClaimPayload = _getClaimPayloadsLayerY(leafPayloads, lastLayerXExitRoot); - - vm.selectFork(forkIdLayerX); - - _claimAndRedeemLayerXAndVerify(withdrawClaimPayload[0]); - } - - function test_deconvertAndBridge_bridgeWrappedMapping() public { - uint256 amount = 100; - - vm.selectFork(forkIdLayerX); - - // create liquidity on the bridge on layer X - deal(address(underlyingAsset), LXLY_BRIDGE_X, amount); - bytes32 lastLayerXExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); - - vm.selectFork(forkIdLayerY); - - // create backing on layer Y - uint256 backingOnLayerY = 0; - uint256 convertAmount = 100; - deal(address(bwUnderlyingAsset), owner, convertAmount); - vm.startPrank(owner); - bwUnderlyingAsset.approve(address(nativeConverter), convertAmount); - backingOnLayerY = nativeConverter.convert(convertAmount, recipient); - vm.stopPrank(); - - _mapTokenLayerYToLayerX(address(vbToken), address(customToken), false); - _mapTokenLayerYToLayerX(address(underlyingAsset), address(bwUnderlyingAsset), false); - - deal(address(customToken), sender, convertAmount); - - LeafPayload memory withdrawLeaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(underlyingAsset), - destinationNetwork: NETWORK_ID_X, - destinationAddress: recipient, - amount: convertAmount, - metadata: bwUnderlyingAssetMetaData // since we deconvert the custom token to the underlying token we'll bridge the underlying token - }); - _deconvertAndBridgeLayerY(sender, convertAmount, withdrawLeaf); - - LeafPayload[] memory leafPayloads = new LeafPayload[](1); - leafPayloads[0] = withdrawLeaf; - ClaimPayload[] memory withdrawClaimPayload = _getClaimPayloadsLayerY(leafPayloads, lastLayerXExitRoot); - - vm.selectFork(forkIdLayerX); - _claimAndVerifyAssetLayerX(underlyingAsset, withdrawClaimPayload[0]); - } - - function test_migrateBackingToLayerX() public { - uint256 amount = 100; - - uint256 vbTokenTotalSupplyBefore = vbToken.totalSupply(); - - // switch to Layer X - vm.selectFork(forkIdLayerX); - - // create liquidity on the bridge on layer X - deal(address(underlyingAsset), LXLY_BRIDGE_X, amount); - bytes32 lastLayerXExitRoot = IPolygonZkEVMGlobalExitRoot(GER_X).lastMainnetExitRoot(); - - vm.selectFork(forkIdLayerY); - - // create backing on layer Y - uint256 backingOnLayerY = 0; - uint256 convertAmount = 100; - deal(address(bwUnderlyingAsset), owner, convertAmount); - vm.startPrank(owner); - bwUnderlyingAsset.approve(address(nativeConverter), convertAmount); - backingOnLayerY = nativeConverter.convert(convertAmount, recipient); - vm.stopPrank(); - - _mapTokenLayerYToLayerX(address(vbToken), address(customToken), false); - _mapTokenLayerYToLayerX(address(underlyingAsset), address(bwUnderlyingAsset), false); - - uint256 maxNonMigratableBacking = backingOnLayerY * MAX_NON_MIGRATABLE_BACKING_PERCENTAGE / 1e18; - uint256 amountToMigrate = backingOnLayerY - maxNonMigratableBacking; - - // make the migration leaves - LeafPayload memory assetLeaf = LeafPayload({ - leafType: LEAF_TYPE_ASSET, - originNetwork: NETWORK_ID_X, - originAddress: address(underlyingAsset), - destinationNetwork: NETWORK_ID_X, - destinationAddress: address(migrationManager), - amount: amountToMigrate, - metadata: bwVbTokenMetaData - }); - - LeafPayload memory messageLeaf = LeafPayload({ - leafType: LEAF_TYPE_MESSAGE, - originNetwork: NETWORK_ID_Y, - originAddress: address(nativeConverter), - destinationNetwork: NETWORK_ID_X, - destinationAddress: address(migrationManager), - amount: 0, - metadata: abi.encode( - MigrationManager.CrossNetworkInstruction.COMPLETE_MIGRATION, abi.encode(amountToMigrate, amountToMigrate) - ) - }); - - // migrate backing to Layer X - vm.expectEmit(); - emit BridgeEvent( - assetLeaf.leafType, - assetLeaf.originNetwork, - assetLeaf.originAddress, - assetLeaf.destinationNetwork, - assetLeaf.destinationAddress, - assetLeaf.amount, - assetLeaf.metadata, - _ILxLyBridge(LXLY_BRIDGE_Y).depositCount() - ); - vm.expectEmit(); - emit BridgeEvent( - messageLeaf.leafType, - messageLeaf.originNetwork, - messageLeaf.originAddress, - messageLeaf.destinationNetwork, - messageLeaf.destinationAddress, - messageLeaf.amount, - messageLeaf.metadata, - _ILxLyBridge(LXLY_BRIDGE_Y).depositCount() + 1 - ); - vm.expectEmit(); - emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); - vm.prank(owner); - nativeConverter.migrateBackingToLayerX(amountToMigrate); - - LeafPayload[] memory leafPayloads = new LeafPayload[](2); - leafPayloads[0] = assetLeaf; - leafPayloads[1] = messageLeaf; - ClaimPayload[] memory claimPayloads = _getClaimPayloadsLayerY(leafPayloads, lastLayerXExitRoot); - - // switch to Layer X - vm.selectFork(forkIdLayerX); - - // claim and withdraw on Layer X - _claimAndVerifyAssetLayerX(underlyingAsset, claimPayloads[0]); - _claimMessageLayerX(claimPayloads[1]); - - uint256 vbTokenTotalSupplyAfter = vbToken.totalSupply(); - assertGt(vbTokenTotalSupplyAfter, vbTokenTotalSupplyBefore); - } - - function _depositAndBridgeLayerX(address _sender, uint256 _amount, LeafPayload memory _leaf) internal { - // make sure we are on Layer X - assertEq(vm.activeFork(), forkIdLayerX); - - vm.startPrank(_sender); - - // approve underlying asset - vbToken.underlyingToken().approve(address(vbToken), _amount); - - // deposit and bridge - vm.expectEmit(); - emit BridgeEvent( - _leaf.leafType, - _leaf.originNetwork, - _leaf.originAddress, - _leaf.destinationNetwork, - _leaf.destinationAddress, - _leaf.amount, - _leaf.metadata, - _ILxLyBridge(LXLY_BRIDGE_X).depositCount() - ); - vbToken.depositAndBridge(_amount, _leaf.destinationAddress, _leaf.destinationNetwork, true); - - vm.stopPrank(); - - // assert balances - vm.assertEq(vbToken.underlyingToken().balanceOf(_sender), 0); - vm.assertEq(vbToken.balanceOf(LXLY_BRIDGE_X), _amount); // shares locked in the bridge - } - - function _mapTokenLayerYToLayerX(address _originTokenAddress, address _sovereignTokenAddress, bool _isNotMintable) - internal - { - // make sure we are on Layer Y - assertEq(vm.activeFork(), forkIdLayerY); - - uint32[] memory originNetworks = new uint32[](1); - originNetworks[0] = NETWORK_ID_X; - address[] memory originTokenAddresses = new address[](1); - originTokenAddresses[0] = _originTokenAddress; - address[] memory sovereignTokenAddresses = new address[](1); - sovereignTokenAddresses[0] = _sovereignTokenAddress; - bool[] memory isNotMintable = new bool[](1); - isNotMintable[0] = _isNotMintable; - - vm.prank(BRIDGE_MANAGER); - IBridgeL2SovereignChain(LXLY_BRIDGE_Y).setMultipleSovereignTokenAddress( - originNetworks, originTokenAddresses, sovereignTokenAddresses, isNotMintable - ); - } - - function _getClaimPayloadLayerX(LeafPayload memory _leaf, bytes32 lastMainnetExitRoot) - internal - returns (ClaimPayload memory) - { - // make sure we are on Layer X - assertEq(vm.activeFork(), forkIdLayerX); - - // simulate the Merkle trees on Layer X - bytes32[] memory merkleTreeLayerX = new bytes32[](1); - merkleTreeLayerX[0] = _ILxLyBridge(LXLY_BRIDGE_X).getLeafValue( - _leaf.leafType, - _leaf.originNetwork, - _leaf.originAddress, - _leaf.destinationNetwork, - _leaf.destinationAddress, - _leaf.amount, - keccak256(abi.encodePacked(_leaf.metadata)) - ); - - // layer X leaf index - uint256 leafIndexLayerX = 0; - - // layer X Merkle tree root - bytes32 merkleTreeRootLayerX = _getMerkleTreeRoot(_encodeLeaves(merkleTreeLayerX)); - - // layer X proof - bytes32[32] memory proofLayerX = _getProofByIndex(_encodeLeaves(merkleTreeLayerX), vm.toString(leafIndexLayerX)); - - // simulate the Merkle tree on Layer Y - bytes32[] memory merkleTreeLayerY = new bytes32[](1); - merkleTreeLayerY[0] = merkleTreeRootLayerX; - - // layer Y leaf index - uint256 leafIndexLayerY = 0; - - // layer Y Merkle tree root - bytes32 merkleTreeRootLayerY = _getMerkleTreeRoot(_encodeLeaves(merkleTreeLayerY)); - - // layer Y proof - bytes32[32] memory proofLayerY = _getProofByIndex(_encodeLeaves(merkleTreeLayerY), vm.toString(leafIndexLayerY)); - - return ClaimPayload({ - proofLayerX: proofLayerX, - proofLayerY: proofLayerY, - globalIndex: _computeGlobalIndex(leafIndexLayerX, leafIndexLayerY, false), - exitRootLayerX: lastMainnetExitRoot, - exitRootLayerY: merkleTreeRootLayerY, - originNetwork: _leaf.originNetwork, - originAddress: _leaf.originAddress, - destinationNetwork: _leaf.destinationNetwork, - destinationAddress: _leaf.destinationAddress, - amount: _leaf.amount, - metadata: _leaf.metadata - }); - } - - function _getClaimPayloadsLayerY(LeafPayload[] memory _leaves, bytes32 lastMainnetExitRoot) - internal - returns (ClaimPayload[] memory) - { - // make sure we are on Layer Y - assertEq(vm.activeFork(), forkIdLayerY); - - // simulate the Merkle trees on Layer Y - bytes32[] memory merkleTreeLayerX = new bytes32[](_leaves.length); - for (uint256 i = 0; i < _leaves.length; i++) { - merkleTreeLayerX[i] = _ILxLyBridge(LXLY_BRIDGE_Y).getLeafValue( - _leaves[i].leafType, - _leaves[i].originNetwork, - _leaves[i].originAddress, - _leaves[i].destinationNetwork, - _leaves[i].destinationAddress, - _leaves[i].amount, - keccak256(abi.encodePacked(_leaves[i].metadata)) - ); - } - - // layer X Merkle tree root - bytes32 merkleTreeRootLayerX = _getMerkleTreeRoot(_encodeLeaves(merkleTreeLayerX)); - - bytes32[] memory merkleTreeLayerY = new bytes32[](2); - merkleTreeLayerY[0] = merkleTreeRootLayerX; - merkleTreeLayerY[1] = merkleTreeRootLayerX; - - // layer Y Merkle tree root - bytes32 merkleExitRootLayerY = _getMerkleTreeRoot(_encodeLeaves(merkleTreeLayerY)); - - ClaimPayload[] memory claimPayloads = new ClaimPayload[](_leaves.length); - for (uint256 i = 0; i < _leaves.length; i++) { - LeafPayload memory leaf = _leaves[i]; - - // layer X leaf index - uint256 leafIndexLayerX = i; - - // proof for Layer X - bytes32[32] memory proofLayerX = - _getProofByIndex(_encodeLeaves(merkleTreeLayerX), vm.toString(leafIndexLayerX)); - - // layer Y leaf index - uint256 leafIndexLayerY = i; - - // proof for Layer Y - bytes32[32] memory proofLayerY = - _getProofByIndex(_encodeLeaves(merkleTreeLayerY), vm.toString(leafIndexLayerY)); - - claimPayloads[i] = ClaimPayload({ - proofLayerX: proofLayerX, - proofLayerY: proofLayerY, - globalIndex: _computeGlobalIndex(leafIndexLayerX, leafIndexLayerY, false), - exitRootLayerX: lastMainnetExitRoot, - exitRootLayerY: merkleExitRootLayerY, - originNetwork: leaf.originNetwork, - originAddress: leaf.originAddress, - destinationNetwork: leaf.destinationNetwork, - destinationAddress: leaf.destinationAddress, - amount: leaf.amount, - metadata: leaf.metadata - }); - } - - return claimPayloads; - } - - function _claimAndVerifyAssetLayerX(IERC20 _token, ClaimPayload memory _claimPayload) internal { - // make sure we are on Layer X - assertEq(vm.activeFork(), forkIdLayerX); - - // update Layer X exit root - vm.prank(address(ROLLUP_MANAGER)); - IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootLayerY); - - // claim asset on Layer X - vm.expectEmit(); - emit ClaimEvent( - _claimPayload.globalIndex, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationAddress, - _claimPayload.amount - ); - ILxLyBridge(LXLY_BRIDGE_X).claimAsset( - _claimPayload.proofLayerX, - _claimPayload.proofLayerY, - _claimPayload.globalIndex, - _claimPayload.exitRootLayerX, - _claimPayload.exitRootLayerY, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationNetwork, - _claimPayload.destinationAddress, - _claimPayload.amount, - _claimPayload.metadata - ); - - // assert balances - assertEq(_token.balanceOf(_claimPayload.destinationAddress), _claimPayload.amount); - } - - function _claimMessageLayerX(ClaimPayload memory _claimPayload) internal { - // make sure we are on Layer X - assertEq(vm.activeFork(), forkIdLayerX); - - // update Layer X exit root - vm.prank(address(ROLLUP_MANAGER)); - IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootLayerY); - - // claim asset on Layer X - vm.expectEmit(); - emit ClaimEvent( - _claimPayload.globalIndex, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationAddress, - _claimPayload.amount - ); - ILxLyBridge(LXLY_BRIDGE_X).claimMessage( - _claimPayload.proofLayerX, - _claimPayload.proofLayerY, - _claimPayload.globalIndex, - _claimPayload.exitRootLayerX, - _claimPayload.exitRootLayerY, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationNetwork, - _claimPayload.destinationAddress, - _claimPayload.amount, - _claimPayload.metadata - ); - } - - function _claimAndVerifyAssetLayerY(IERC20 _token, ClaimPayload memory _claimPayload) internal { - // make sure we are on Layer Y - assertEq(vm.activeFork(), forkIdLayerY); - - // update Layer Y exit root - vm.prank(address(LXLY_BRIDGE_Y)); - IPolygonZkEVMGlobalExitRoot(GER_Y).updateExitRoot(_claimPayload.exitRootLayerY); - - // insert Layer Y global exit root - vm.prank(GER_Y_UPDATER); - IPolygonZkEVMGlobalExitRoot(GER_Y).insertGlobalExitRoot( - _calculateGlobalExitRoot(_claimPayload.exitRootLayerX, _claimPayload.exitRootLayerY) - ); - - // claim asset on Layer Y - vm.expectEmit(); - emit ClaimEvent( - _claimPayload.globalIndex, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationAddress, - _claimPayload.amount - ); - ILxLyBridge(LXLY_BRIDGE_Y).claimAsset( - _claimPayload.proofLayerX, - _claimPayload.proofLayerY, - _claimPayload.globalIndex, - _claimPayload.exitRootLayerX, - _claimPayload.exitRootLayerY, - _claimPayload.originNetwork, - _claimPayload.originAddress, - _claimPayload.destinationNetwork, - _claimPayload.destinationAddress, - _claimPayload.amount, - _claimPayload.metadata - ); - - // assert balances - assertEq(_token.balanceOf(_claimPayload.destinationAddress), _claimPayload.amount); - } - - function _claimAndRedeemLayerXAndVerify(ClaimPayload memory _claimPayload) internal { - // make sure we are on Layer X - assertEq(vm.activeFork(), forkIdLayerX); - - // update Layer X exit root - vm.prank(address(ROLLUP_MANAGER)); - IPolygonZkEVMGlobalExitRoot(GER_X).updateExitRoot(_claimPayload.exitRootLayerY); - - // claim and withdraw on Layer X - vm.prank(address(vbToken)); - vbToken.approve(recipient, _claimPayload.amount); - - vm.prank(recipient); - vbToken.claimAndRedeem( - _claimPayload.proofLayerX, - _claimPayload.proofLayerY, - _claimPayload.globalIndex, - _claimPayload.exitRootLayerX, - _claimPayload.exitRootLayerY, - _claimPayload.destinationAddress, - _claimPayload.amount, - recipient, - _claimPayload.metadata - ); - - assertEq(vbToken.underlyingToken().balanceOf(recipient), _claimPayload.amount); - } - - function _deconvertAndBridgeLayerY(address _sender, uint256 _amount, LeafPayload memory _leaf) internal { - // make sure we are on Layer Y - assertEq(vm.activeFork(), forkIdLayerY); - - vm.startPrank(_sender); - - // approve the custom token - nativeConverter.customToken().approve(address(nativeConverter), _amount); - - // deconvert and bridge - vm.expectEmit(); - emit BridgeEvent( - _leaf.leafType, - _leaf.originNetwork, - _leaf.originAddress, - _leaf.destinationNetwork, - _leaf.destinationAddress, - _leaf.amount, - _leaf.metadata, - _ILxLyBridge(LXLY_BRIDGE_Y).depositCount() - ); - - nativeConverter.deconvertAndBridge(_amount, _leaf.destinationAddress, _leaf.destinationNetwork, true); - - vm.stopPrank(); - - // assert balances - vm.assertEq(nativeConverter.customToken().balanceOf(_sender), 0); - vm.assertEq(nativeConverter.underlyingToken().balanceOf(LXLY_BRIDGE_Y), 0); - } - - function _proxify(address logic, address admin, bytes memory initData) internal returns (address proxy) { - proxy = address(new TransparentUpgradeableProxy(logic, admin, initData)); - } - - function _computeGlobalIndex(uint256 indexLayerX, uint256 indexLayerY, bool isLayerX) - internal - pure - returns (uint256) - { - if (isLayerX) { - return indexLayerX + 2 ** 64; - } else { - return indexLayerX + indexLayerY * 2 ** 32; - } - } - - function _calculateGlobalExitRoot(bytes32 exitRootLayerX, bytes32 exitRootLayerY) internal pure returns (bytes32) { - return keccak256(abi.encodePacked(exitRootLayerX, exitRootLayerY)); - } -} diff --git a/test/integration/PolygonIntegrationTest.t.sol b/test/integration/PolygonIntegrationTest.t.sol new file mode 100644 index 00000000..40e98448 --- /dev/null +++ b/test/integration/PolygonIntegrationTest.t.sol @@ -0,0 +1,316 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test base +import {TestConstants} from "test/base/TestConstants.sol"; + +// Core contracts +import {GenericCustomTokenPolygon} from "src/secondary-chain/polygon/GenericCustomTokenPolygon.sol"; +import {GenericVaultBridgeToken} from "src/primary-chain/ethereum/GenericVaultBridgeToken.sol"; +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; +import {VaultBridgeToken} from "src/primary-chain/VaultBridgeToken.sol"; +import {VaultBridgeTokenInitializer} from "src/primary-chain/VaultBridgeTokenInitializer.sol"; +import {VaultBridgeTokenPart2} from "src/primary-chain/VaultBridgeTokenPart2.sol"; + +// OpenZeppelin +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +// Mocks +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockChildChainManager} from "test/utils/mocks/MockChildChainManager.sol"; +import {MockERC20} from "test/utils/mocks/MockERC20.sol"; +import {MockRootChainManager} from "test/utils/mocks/MockRootChainManager.sol"; +import {MockVault} from "test/utils/mocks/MockVault.sol"; + +/** + * @title PolygonIntegrationTest + * @notice Integration test for Polygon PoS bridge functionality with VaultBridgeToken + * @dev This test simulates the complete flow for bridging vault bridge tokens to Polygon via PoS bridge: + * 1. User deposits underlying asset into VaultBridgeToken to get vbToken on primary chain + * 2. User manually calls depositFor on Polygon PoS RootChainManager to lock vbToken + * 3. State sync mechanism mints wrapped vbToken (WvbToken) on Polygon chain + * 4. User withdraws WvbToken on Polygon chain, burning the tokens + * 5. User calls exit on Polygon PoS RootChainManager to unlock vbToken on primary chain + */ +contract PolygonIntegrationTest is TestConstants { + // ===== TEST ADDRESSES ===== + address owner = makeAddr("owner"); + address sender = vm.addr(senderPrivateKey); + + // ===== CORE CONTRACTS ===== + MigrationManager migrationManager; + MockAgglayerBridge mockAgglayerBridge; + MockChildChainManager mockChildChainManager; + MockRootChainManager mockRootChainManager; + GenericVaultBridgeToken vbToken; + VaultBridgeTokenPart2 vbTokenPart2; + GenericCustomTokenPolygon customToken; + + // ===== EXTERNAL CONTRACTS ===== + MockVault vbTokenVault; + MockERC20 underlyingAsset; + + function setUp() public virtual { + _deployInfrastructure(); + _deployVaultBridgeToken(); + _deployCustomToken(); + _configureTokenMappings(); + _addLabels(); + } + + /// @notice Deploy core infrastructure contracts + function _deployInfrastructure() internal { + // Deploy mock underlying asset + underlyingAsset = new MockERC20(UNDERLYING_ASSET_NAME, UNDERLYING_ASSET_SYMBOL, UNDERLYING_ASSET_DECIMALS); + + // Deploy vault for vbToken + vbTokenVault = new MockVault(address(underlyingAsset)); + vbTokenVault.setMaxDeposit(MAX_DEPOSIT); + vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); + + // Deploy mock bridge managers + mockRootChainManager = new MockRootChainManager(); + mockChildChainManager = new MockChildChainManager(); + + // Deploy mock agglayer bridge + mockAgglayerBridge = new MockAgglayerBridge(); + mockAgglayerBridge.setNetworkId(1); // Ethereum mainnet + + // Deploy migration manager + MigrationManager migrationManagerImpl = new MigrationManager(); + bytes memory migrationManagerInitData = abi.encodeCall( + MigrationManager.reinitialize1, + ( + owner, // owner + address(mockAgglayerBridge) // agglayerBridge + ) + ); + migrationManager = + MigrationManager(payable(_proxify(address(migrationManagerImpl), address(this), migrationManagerInitData))); + } + + /// @notice Deploy VaultBridgeToken on primary chain + function _deployVaultBridgeToken() internal { + // Deploy vbToken part 2 + vbTokenPart2 = new VaultBridgeTokenPart2(); + + // Deploy vbToken implementation + GenericVaultBridgeToken vbTokenImpl = new GenericVaultBridgeToken(); + + // Deploy initializer + address initializer = address(new VaultBridgeTokenInitializer()); + + // Prepare initialization parameters + VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ + owner: owner, + name: VBTOKEN_NAME, + symbol: VBTOKEN_SYMBOL, + underlyingToken: address(underlyingAsset), + minimumReservePercentage: MINIMUM_RESERVE_PERCENTAGE, + yieldVault: address(vbTokenVault), + yieldRecipient: owner, + agglayerBridge: address(mockAgglayerBridge), + minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT_INTEGRATION, + migrationManager: address(migrationManager), + yieldVaultMaximumSlippagePercentage: 0.01e18, // 1% slippage + vaultBridgeTokenPart2: address(vbTokenPart2) + }); + + // Deploy vbToken proxy + bytes memory vbTokenInitData = abi.encodeCall(vbTokenImpl.reinitialize1, (initializer, initParams)); + vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbTokenImpl), address(this), vbTokenInitData))); + vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); + } + + /// @notice Deploy CustomToken on secondary chain + function _deployCustomToken() internal { + // Deploy custom token implementation + GenericCustomTokenPolygon customTokenImpl = new GenericCustomTokenPolygon(); + + // Prepare initialization data for the custom token + bytes memory customTokenInitData = abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, + ( + owner, // owner_ + "Wrapped Vault Bridge Underlying Asset", // name_ + "WvbUAT", // symbol_ + UNDERLYING_ASSET_DECIMALS, // originalUnderlyingTokenDecimals_ + address(mockChildChainManager) // childChainManager_ + ) + ); + + // Deploy custom token proxy + customToken = GenericCustomTokenPolygon(_proxify(address(customTokenImpl), address(this), customTokenInitData)); + } + + /// @notice Configure token mappings between chains + function _configureTokenMappings() internal { + // Map root token (vbToken) to child token (customToken) on root chain manager + mockRootChainManager.mapToken( + address(vbToken), // rootToken + address(customToken), // childToken + keccak256("ERC20") // tokenType + ); + + // Map child token (customToken) to root token (vbToken) on child chain manager + mockChildChainManager.mapToken( + address(vbToken), // rootToken + address(customToken) // childToken + ); + } + + /// @notice Add labels for debugging + function _addLabels() internal { + vm.label(address(customToken), "Custom Token"); + vm.label(address(migrationManager), "Migration Manager"); + vm.label(address(mockAgglayerBridge), "Mock Agglayer Bridge"); + vm.label(address(mockChildChainManager), "Mock Child Chain Manager"); + vm.label(address(mockRootChainManager), "Mock Root Chain Manager"); + vm.label(address(owner), "Owner"); + vm.label(address(sender), "Sender"); + vm.label(address(underlyingAsset), "Mock Underlying Asset"); + vm.label(address(vbToken), "VB Token"); + vm.label(address(vbTokenVault), "VB Token Vault"); + } + + // Basic test to verify setup + function test_setupVerification() public view { + // Verify vbToken setup + assertEq(address(vbToken.underlyingToken()), address(underlyingAsset)); + assertEq(vbToken.name(), VBTOKEN_NAME); + assertEq(vbToken.symbol(), VBTOKEN_SYMBOL); + assertEq(vbToken.decimals(), VBTOKEN_DECIMALS); + + // Verify root chain mapping + address mappedChildToken = mockRootChainManager.rootToChildToken(address(vbToken)); + assertEq(mappedChildToken, address(customToken), "Root to child mapping incorrect"); + + // Verify secondary chain setup + assertEq(customToken.name(), "Wrapped Vault Bridge Underlying Asset"); + assertEq(customToken.symbol(), "WvbUAT"); + assertEq(customToken.decimals(), UNDERLYING_ASSET_DECIMALS); + assertEq(customToken.bridge(), address(mockChildChainManager)); // bridge is child chain manager + assertTrue(customToken.hasRole(customToken.DEFAULT_ADMIN_ROLE(), owner)); + + // Verify child chain mapping + address mappedRootToken = mockChildChainManager.rootToChildToken(address(vbToken)); + assertEq(mappedRootToken, address(customToken), "Child to root mapping incorrect"); + } + + // ===== BRIDGING TESTS ===== + + function test_depositAndBridge() public { + uint256 depositAmount = 1000 * 10 ** UNDERLYING_ASSET_DECIMALS; + + // Step 1: User deposits underlying asset into vbToken + + // Fund sender with underlying asset + underlyingAsset.mint(sender, depositAmount); + + // Approve vbToken to spend underlying asset + vm.prank(sender); + underlyingAsset.approve(address(vbToken), depositAmount); + + // Deposit underlying asset to get vbToken + vm.prank(sender); + uint256 vbTokenAmount = vbToken.deposit(depositAmount, sender); + + assertEq(vbToken.balanceOf(sender), vbTokenAmount); + assertGt(vbTokenAmount, 0, "Should receive vbTokens"); + + // Step 2: User bridges vbToken via Polygon PoS (manual deposit + depositFor) + // Note: This uses the Polygon PoS bridge directly, not the agglayer bridge + _bridgeToSecondaryChain(sender, vbTokenAmount); + } + + function test_withdrawAndExit() public { + uint256 vbTokenAmount = 1000 * 10 ** UNDERLYING_ASSET_DECIMALS; + + // Step 1: Setup - Bridge tokens to secondary chain first + _setupTokensOnSecondaryChain(sender, vbTokenAmount); + + // Verify initial state + assertEq(customToken.balanceOf(sender), vbTokenAmount, "Should have WvbUAT on secondary chain"); + assertEq( + vbToken.balanceOf(address(mockRootChainManager)), vbTokenAmount, "Root chain manager should hold vbTokens" + ); + + // Step 2: User withdraws WvbUAT from secondary chain and exits on primary chain + _bridgeToPrimaryChain(sender, vbTokenAmount); + + // Verify final state + assertEq(customToken.balanceOf(sender), 0, "Should have no WvbUAT left"); + assertEq(vbToken.balanceOf(sender), vbTokenAmount, "Should have vbTokens back on primary chain"); + } + + // ===== HELPER FUNCTIONS ===== + + /// @notice Simulates bridging from primary to secondary chain via Polygon PoS bridge + /// @dev This is the manual process: user must first deposit to get vbTokens, + /// then manually call depositFor on the root chain manager + /// @param user The user bridging tokens + /// @param amount The amount of vbTokens to bridge + function _bridgeToSecondaryChain(address user, uint256 amount) internal { + // Step 1: User calls depositFor on MockRootChainManager + + vm.prank(user); + vbToken.approve(address(mockRootChainManager), amount); + + // Encode the deposit data + bytes memory depositData = abi.encode(amount); + + // Call depositFor on MockRootChainManager + vm.prank(user); + mockRootChainManager.depositFor(user, address(vbToken), depositData); + + // Step 2: Simulate state sync - ChildChainManager calls deposit on secondary chain + _simulateDepositFromPrimaryChain(user, address(vbToken), amount); + } + + /// @notice Simulates the state sync mechanism for deposits + /// @param user The user receiving tokens + /// @param amount The amount to mint + function _simulateDepositFromPrimaryChain(address user, address rootToken, uint256 amount) internal { + // Simulate ChildChainManager calling deposit on the custom token + bytes memory depositData = abi.encode(user, rootToken, amount); + mockChildChainManager.onStateReceive(0, depositData); + + assertEq(customToken.balanceOf(user), amount, "WvbUAT should be minted"); + } + + /// @notice Setup tokens on secondary chain for testing reverse flow + /// @param user The user to setup tokens for + /// @param amount The amount of underlying tokens to bridge + function _setupTokensOnSecondaryChain(address user, uint256 amount) internal { + // Fund user with underlying asset + underlyingAsset.mint(user, amount); + + // Approve vbToken to spend underlying asset + vm.prank(user); + underlyingAsset.approve(address(vbToken), amount); + + // Deposit underlying asset to get vbToken + vm.prank(user); + uint256 vbTokenAmount = vbToken.deposit(amount, user); + + // Bridge vbToken to secondary chain + _bridgeToSecondaryChain(user, vbTokenAmount); + } + + /// @notice Simulates bridging from secondary to primary chain via Polygon PoS bridge + /// @dev This is the reverse process: user withdraws WvbUAT, then exits on primary chain + /// @param user The user bridging tokens + /// @param amount The amount of WvbUAT to bridge back + function _bridgeToPrimaryChain(address user, uint256 amount) internal { + // Step 1: User withdraws WvbUAT on secondary chain (burns tokens) + vm.prank(user); + customToken.withdraw(amount); + + // Verify tokens were burned + assertEq(customToken.balanceOf(user), 0, "WvbUAT should be burned"); + + // Step 2: Simulate exit on primary chain (normally requires merkle proof) + // In real scenario, user would submit proof and call exit on RootChainManager + mockRootChainManager.exit(user, address(vbToken), amount); + } +} diff --git a/test/interfaces/ILxLyBridge.sol b/test/interfaces/IAgglayerBridge.sol similarity index 53% rename from test/interfaces/ILxLyBridge.sol rename to test/interfaces/IAgglayerBridge.sol index ea3eabe8..e07585b1 100644 --- a/test/interfaces/ILxLyBridge.sol +++ b/test/interfaces/IAgglayerBridge.sol @@ -1,17 +1,14 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity 0.8.29; -import {ILxLyBridge as _ILxLyBridge} from "../../src/etc/ILxLyBridge.sol"; +import {IAgglayerBridge as _IAgglayerBridge} from "src/etc/IAgglayerBridge.sol"; -interface ILxLyBridge is _ILxLyBridge { +interface IAgglayerBridge is _IAgglayerBridge { function depositCount() external view returns (uint32); - function precalculatedWrapperAddress( - uint32 originNetwork, - address originTokenAddress, - string calldata name, - string calldata symbol, - uint8 decimals - ) external view returns (address); + function computeTokenProxyAddress(uint32 originNetwork, address originTokenAddress) + external + view + returns (address); function getLeafValue( uint8 leafType, uint32 originNetwork, diff --git a/test/interfaces/IBridgeL2SovereignChain.sol b/test/interfaces/IBridgeL2SovereignChain.sol index 86bf7f81..97e00b7b 100644 --- a/test/interfaces/IBridgeL2SovereignChain.sol +++ b/test/interfaces/IBridgeL2SovereignChain.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity 0.8.29; interface IBridgeL2SovereignChain { diff --git a/test/interfaces/IPolygonZkEVMGlobalExitRoot.sol b/test/interfaces/IPolygonZkEVMGlobalExitRoot.sol index 11cd0c4c..491db666 100644 --- a/test/interfaces/IPolygonZkEVMGlobalExitRoot.sol +++ b/test/interfaces/IPolygonZkEVMGlobalExitRoot.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity 0.8.29; interface IPolygonZkEVMGlobalExitRoot { diff --git a/test/unit/InitializationCounterUpgradeableTest.t.sol b/test/unit/InitializationCounterUpgradeableTest.t.sol new file mode 100644 index 00000000..176a1cb4 --- /dev/null +++ b/test/unit/InitializationCounterUpgradeableTest.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity 0.8.29; + +// Test base +import "test/base/InitializationCounterTestBase.sol"; + +// Core contract +import {InitializationCounterUpgradeable} from "src/etc/InitializationCounterUpgradeable.sol"; + +/// @dev Tests for InitializationCounterUpgradeable +contract InitializationCounterUpgradeableTest is InitializationCounterTestBase { + function setUp() public virtual { + deployInitializationCounterInfrastructure(); + } + + // ========= INITIAL STATE TESTS ========= + + function test_initialState() public view { + assertCounterValues(INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + } + + function test_globalInitializationCounter_view() public view { + assertEq(initCounter.globalInitializationCounter(), INITIAL_COUNTER_VALUE); + } + + function test_storageSlot_correct() public view { + // Verify the storage slot matches the expected ERC-7201 calculation + bytes32 expectedSlot = keccak256( + abi.encode(uint256(keccak256("agglayer.vault-bridge.InitializationCounterUpgradeable.storage")) - 1) + ) & ~bytes32(uint256(0xff)); + + assertEq(initCounter.getStorageSlot(), expectedSlot); + } + + // ========= LOCAL INITIALIZATION COUNTER TESTS ========= + + function test_incrementLocalInitializationCounter_success() public { + // First increment: 0 -> 1 + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + assertCounterValues(INITIAL_COUNTER_VALUE, FIRST_INCREMENT, INITIAL_COUNTER_VALUE); + + // Second increment: 1 -> 2 + incrementLocalCounterSuccessfully(SECOND_INCREMENT); + assertCounterValues(INITIAL_COUNTER_VALUE, SECOND_INCREMENT, INITIAL_COUNTER_VALUE); + + // Third increment: 2 -> 3 + incrementLocalCounterSuccessfully(THIRD_INCREMENT); + assertCounterValues(INITIAL_COUNTER_VALUE, THIRD_INCREMENT, INITIAL_COUNTER_VALUE); + } + + function test_incrementLocalInitializationCounter_wrongExpectedValue_reverts() public { + // Try to increment from 0 to 2 (should expect 1) + expectLocalCounterIncrementFailure(SECOND_INCREMENT); + + // Counter should remain unchanged + assertCounterValues(INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + } + + function test_incrementLocalInitializationCounter_modifier_works() public { + // Verify the modifier correctly increments the counter + initCounter.incrementLocalInitializationCounterWithModifier(FIRST_INCREMENT); + + // Verify state changed + assertEq(initCounter.localInitializationCounter(), FIRST_INCREMENT); + } + + // ========= GLOBAL INITIALIZATION COUNTER TESTS ========= + + function test_incrementGlobalInitializationCounter_success() public { + // First increment: 0 -> 1 + incrementGlobalCounterSuccessfully(FIRST_INCREMENT); + assertCounterValues(FIRST_INCREMENT, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + + // Second increment: 1 -> 2 + incrementGlobalCounterSuccessfully(SECOND_INCREMENT); + assertCounterValues(SECOND_INCREMENT, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + + // Third increment: 2 -> 3 + incrementGlobalCounterSuccessfully(THIRD_INCREMENT); + assertCounterValues(THIRD_INCREMENT, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + } + + function test_incrementGlobalInitializationCounter_wrongExpectedValue_reverts() public { + // Try to increment from 0 to 2 (should expect 1) + expectGlobalCounterIncrementFailure(SECOND_INCREMENT); + + // Counter should remain unchanged + assertCounterValues(INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + } + + function test_incrementGlobalInitializationCounter_returnsCorrectValue() public { + uint64 returnedValue = initCounter.incrementGlobalInitializationCounter(FIRST_INCREMENT); + assertEq(returnedValue, FIRST_INCREMENT); + } + + function test_incrementGlobalInitializationCounter_errorMessage_correct() public { + uint64 wrongValue = 5; + uint64 expectedCorrectValue = FIRST_INCREMENT; + + vm.expectRevert( + abi.encodeWithSelector( + InitializationCounterUpgradeable.IncorrectInitializationOrder.selector, wrongValue, expectedCorrectValue + ) + ); + + initCounter.incrementGlobalInitializationCounter(wrongValue); + } + + // ========= EXTENSION INITIALIZATION COUNTER TESTS ========= + + function test_incrementExtensionInitializationCounter_success() public { + // First, increment local counter to meet requirements + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + + // Now increment extension counter: 0 -> 1 + incrementExtensionCounterSuccessfully(FIRST_INCREMENT, FIRST_INCREMENT); + assertCounterValues(INITIAL_COUNTER_VALUE, FIRST_INCREMENT, FIRST_INCREMENT); + + // Increment local again + incrementLocalCounterSuccessfully(SECOND_INCREMENT); + + // Increment extension counter again: 1 -> 2 + incrementExtensionCounterSuccessfully(SECOND_INCREMENT, SECOND_INCREMENT); + assertCounterValues(INITIAL_COUNTER_VALUE, SECOND_INCREMENT, SECOND_INCREMENT); + } + + function test_incrementExtensionInitializationCounter_wrongLocalRequirement_reverts() public { + // Try to increment extension without meeting local counter requirement + expectExtensionCounterIncrementFailure(FIRST_INCREMENT, FIRST_INCREMENT); + + // Counter should remain unchanged + assertCounterValues(INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE, INITIAL_COUNTER_VALUE); + } + + function test_incrementExtensionInitializationCounter_wrongExpectedValue_reverts() public { + // Set up local counter properly + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + + // Try to increment extension with wrong expected value + expectExtensionCounterIncrementFailureWithWrongExtension(FIRST_INCREMENT, SECOND_INCREMENT); + + // Counters should remain unchanged (except local which was incremented above) + assertCounterValues(INITIAL_COUNTER_VALUE, FIRST_INCREMENT, INITIAL_COUNTER_VALUE); + } + + function test_incrementExtensionInitializationCounter_modifier_works() public { + // Set up prerequisites + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + + // Test the modifier + initCounter.incrementExtensionInitializationCounterWithModifier(FIRST_INCREMENT, FIRST_INCREMENT); + + // Verify state changed + assertEq(initCounter.extensionInitializationCounter(), FIRST_INCREMENT); + } + + // ========= INTEGRATION TESTS ========= + + function test_multipleCounterTypes_independent() public { + // Increment all three types independently + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + incrementGlobalCounterSuccessfully(FIRST_INCREMENT); + incrementExtensionCounterSuccessfully(FIRST_INCREMENT, FIRST_INCREMENT); + + // Verify all are at expected values + assertCounterValues(FIRST_INCREMENT, FIRST_INCREMENT, FIRST_INCREMENT); + + // Increment local and extension again + incrementLocalCounterSuccessfully(SECOND_INCREMENT); + incrementExtensionCounterSuccessfully(SECOND_INCREMENT, SECOND_INCREMENT); + + // Global should remain unchanged, others incremented + assertCounterValues(FIRST_INCREMENT, SECOND_INCREMENT, SECOND_INCREMENT); + } + + function test_sequentialOperations() public { + // Complex sequence testing various combinations + incrementLocalCounterSuccessfully(FIRST_INCREMENT); + incrementGlobalCounterSuccessfully(FIRST_INCREMENT); + incrementExtensionCounterSuccessfully(FIRST_INCREMENT, FIRST_INCREMENT); + + incrementLocalCounterSuccessfully(SECOND_INCREMENT); + incrementGlobalCounterSuccessfully(SECOND_INCREMENT); + incrementExtensionCounterSuccessfully(SECOND_INCREMENT, SECOND_INCREMENT); + + incrementLocalCounterSuccessfully(THIRD_INCREMENT); + incrementGlobalCounterSuccessfully(THIRD_INCREMENT); + incrementExtensionCounterSuccessfully(THIRD_INCREMENT, THIRD_INCREMENT); + + assertCounterValues(THIRD_INCREMENT, THIRD_INCREMENT, THIRD_INCREMENT); + } + + // ========= EDGE CASES ========= + + function test_largeCounterValues() public { + // Test with larger counter values + uint64 largeValue = 1000; + + // Increment local counter many times to reach large value + for (uint64 i = 1; i <= largeValue; i++) { + incrementLocalCounterSuccessfully(i); + } + + assertEq(initCounter.localInitializationCounter(), largeValue); + } + + function test_maxUint64Values() public { + // Test behavior near uint64 max (we can't test overflow due to gas limits, + // but we can test large values) + uint64 nearMaxValue = type(uint64).max - 10; + + // This would normally be done through proper initialization sequence, + // but for testing purposes we'll just verify the function handles large values + vm.expectRevert(); + initCounter.incrementLocalInitializationCounterWithModifier(nearMaxValue); + } +} diff --git a/test/unit/primary-chain/MigrationManagerTest.t.sol b/test/unit/primary-chain/MigrationManagerTest.t.sol new file mode 100644 index 00000000..2c8dbeca --- /dev/null +++ b/test/unit/primary-chain/MigrationManagerTest.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test base +import "test/base/primary-chain/MigrationManagerTestBase.sol"; + +// Core contracts +import {MigrationManager, PausableUpgradeable} from "src/primary-chain/MigrationManager.sol"; + +// OpenZeppelin +import {IAccessControl} from "@openzeppelin-contracts/access/IAccessControl.sol"; +import {IERC20} from "@openzeppelin-contracts/token/ERC20/IERC20.sol"; + +/// @dev Tests for MigrationManager +contract MigrationManagerTest is MigrationManagerTestBase { + function setUp() public virtual { + deployMigrationManagerInfrastructure(); + } + + function test_reinitialize1() public { + vm.revertToState(stateBeforeInitialize); + + // Test reinitialize1 with invalid owner + bytes memory migrationManagerInitData = + abi.encodeCall(MigrationManager.reinitialize1, (address(0), address(agglayerBridge))); + vm.expectRevert(MigrationManager.InvalidOwner.selector); + _proxify(migrationManagerImpl, address(this), migrationManagerInitData); + + // Test reinitialize1 with invalid agglayer bridge + migrationManagerInitData = abi.encodeCall(MigrationManager.reinitialize1, (owner, address(0))); + vm.expectRevert(MigrationManager.InvalidAgglayerBridge.selector); + _proxify(migrationManagerImpl, address(this), migrationManagerInitData); + + // Test reinitialize2 with invalid wrapped gas token + migrationManagerInitData = abi.encodeCall(MigrationManager.reinitialize1, (owner, address(agglayerBridge))); + MigrationManager testManager = + MigrationManager(payable(_proxify(migrationManagerImpl, address(this), migrationManagerInitData))); + vm.expectRevert(MigrationManager.InvalidWrappedGasToken.selector); + testManager.reinitialize2(address(0)); + } + + function test_configureNativeConverters_reverts() public { + uint32[] memory secondaryChainAgglayerIds = new uint32[](1); + secondaryChainAgglayerIds[0] = NETWORK_ID_L2; + address[] memory nativeConverters = new address[](1); + nativeConverters[0] = nativeConverter; + + // test pause and unpause + bytes memory callData = abi.encodeCall( + migrationManager.configureNativeConverters, + (secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken))) + ); + _testPauseUnpause(owner, address(migrationManager), callData); + + // test only callable by the default admin + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(this), + migrationManager.DEFAULT_ADMIN_ROLE() + ) + ); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + vm.startPrank(owner); + + // test mismatched inputs: secondaryChainAgglayerIds + vm.expectRevert(MigrationManager.NonMatchingInputLengths.selector); + migrationManager.configureNativeConverters(new uint32[](2), nativeConverters, payable(address(vbToken))); + + // test mismatched inputs: nativeConverters + vm.expectRevert(MigrationManager.NonMatchingInputLengths.selector); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, new address[](2), payable(address(vbToken)) + ); + + // test invalid secondaryChainAgglayerId + secondaryChainAgglayerIds[0] = NETWORK_ID_L1; + vm.expectRevert(MigrationManager.InvalidSecondaryChainAgglayerId.selector); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + // test invalid native converter + secondaryChainAgglayerIds[0] = NETWORK_ID_L2; + nativeConverters[0] = address(0); + vm.expectRevert(MigrationManager.InvalidNativeConverter.selector); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + // test invalid underlying token + nativeConverters[0] = nativeConverter; + vbToken.setUnderlyingToken(address(0)); + vm.expectRevert(MigrationManager.InvalidUnderlyingToken.selector); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + vbToken.setUnderlyingToken(address(underlyingToken)); + + vm.stopPrank(); + } + + function test_configureNativeConverters() public { + uint32[] memory secondaryChainAgglayerIds = new uint32[](1); + secondaryChainAgglayerIds[0] = NETWORK_ID_L2; + address[] memory nativeConverters = new address[](1); + nativeConverters[0] = nativeConverter; + + // configure native converter + vm.expectEmit(); + emit MigrationManager.NativeConverterConfigured(NETWORK_ID_L2, nativeConverter, (address(vbToken))); + vm.startPrank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + MigrationManager.TokenPair memory tokenPair = + migrationManager.nativeConvertersConfiguration(NETWORK_ID_L2, nativeConverter); + + assertEq(address(tokenPair.vbToken), address(vbToken)); + assertEq(address(tokenPair.underlyingToken), address(underlyingToken)); + assertEq(IERC20(underlyingToken).allowance(address(migrationManager), address(vbToken)), type(uint256).max); + + // change vbToken + MockERC20 newUnderlyingToken = new MockERC20("New Underlying Token", "NUT", 18); + MockVbToken newVbToken = new MockVbToken(); + newVbToken.setUnderlyingToken(address(newUnderlyingToken)); + + vm.expectEmit(); + emit MigrationManager.NativeConverterConfigured(NETWORK_ID_L2, nativeConverter, payable(address(newVbToken))); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(newVbToken)) + ); + + tokenPair = migrationManager.nativeConvertersConfiguration(NETWORK_ID_L2, nativeConverter); + + assertEq(address(tokenPair.vbToken), address(newVbToken)); + assertEq(address(tokenPair.underlyingToken), address(newUnderlyingToken)); + assertEq(IERC20(underlyingToken).allowance(address(migrationManager), payable(address(vbToken))), 0); + assertEq( + newUnderlyingToken.allowance(address(migrationManager), payable(address(newVbToken))), type(uint256).max + ); + + // unset vbToken + vm.expectEmit(); + emit MigrationManager.NativeConverterConfigured(NETWORK_ID_L2, nativeConverter, address(0)); + migrationManager.configureNativeConverters(secondaryChainAgglayerIds, nativeConverters, payable(address(0))); + + tokenPair = migrationManager.nativeConvertersConfiguration(NETWORK_ID_L2, nativeConverter); + assertEq(address(tokenPair.vbToken), address(0)); + assertEq(address(tokenPair.underlyingToken), address(0)); + assertEq(newUnderlyingToken.allowance(address(migrationManager), address(newVbToken)), 0); + + vm.stopPrank(); + } + + function test_onMessageReceived_reverts() public { + uint32[] memory secondaryChainAgglayerIds = new uint32[](1); + secondaryChainAgglayerIds[0] = NETWORK_ID_L2; + address[] memory nativeConverters = new address[](1); + nativeConverters[0] = nativeConverter; + + // test pause and unpause + bytes memory callData = + abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_L2, bytes(""))); + _testPauseUnpause(owner, address(migrationManager), callData); + + // test only callable by the agglayer bridge + vm.expectRevert(MigrationManager.Unauthorized.selector); + migrationManager.onMessageReceived(nativeConverter, NETWORK_ID_L2, bytes("")); + + bytes memory data = abi.encode( + MigrationManager.CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, abi.encode(100, 100) + ); + + // test unset vbToken + vm.expectRevert(MigrationManager.Unauthorized.selector); + vm.prank(address(agglayerBridge)); + migrationManager.onMessageReceived(nativeConverter, NETWORK_ID_L2, data); + + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + // test wrapped native token with insufficient balance (balance does not match after receiving native token) + MockWETH mockERC20WithDeposit = new MockWETH(); + vbToken.setUnderlyingToken(address(mockERC20WithDeposit)); + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + deal(address(agglayerBridge), 100); + + bytes memory onMessageReceivedCallData = + abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_L2, data)); + vm.expectRevert( + abi.encodeWithSelector(MigrationManager.InsufficientUnderlyingTokenBalanceAfterWrapping.selector, 0, 100) + ); + vm.prank(address(agglayerBridge)); + (bool _ignored,) = address(migrationManager).call{value: 100}(onMessageReceivedCallData); + _ignored = _ignored; // silence unused variable warning + } + + function test_onMessageReceived() public { + uint32[] memory secondaryChainAgglayerIds = new uint32[](1); + secondaryChainAgglayerIds[0] = NETWORK_ID_L2; + address[] memory nativeConverters = new address[](1); + nativeConverters[0] = nativeConverter; + + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + MockWETH mockERC20WithDeposit = new MockWETH(); + vbToken.setUnderlyingToken(address(mockERC20WithDeposit)); + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(vbToken)) + ); + + deal(address(agglayerBridge), 100); + + // test regular migration + bytes memory data = + abi.encode(MigrationManager.CrossChainInstruction._0_COMPLETE_MIGRATION, abi.encode(100, 100)); + + vm.prank(address(agglayerBridge)); + (bool success,) = address(migrationManager).call( + abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_L2, data)) + ); + assertTrue(success); + + // test migration with wrapped gas token + MockVbToken wrappedGasTokenVbToken = new MockVbToken(); + wrappedGasTokenVbToken.setUnderlyingToken(address(wrappedGasToken)); + + vm.prank(owner); + migrationManager.configureNativeConverters( + secondaryChainAgglayerIds, nativeConverters, payable(address(wrappedGasTokenVbToken)) + ); + + data = abi.encode( + MigrationManager.CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, abi.encode(100, 100) + ); + + vm.prank(address(agglayerBridge)); + (success,) = address(migrationManager).call{value: 100}( + abi.encodeCall(migrationManager.onMessageReceived, (nativeConverter, NETWORK_ID_L2, data)) + ); + assertTrue(success); + } +} diff --git a/test/GenericVaultBridgeToken.t.sol b/test/unit/primary-chain/VaultBridgeTokenTest.t.sol similarity index 59% rename from test/GenericVaultBridgeToken.t.sol rename to test/unit/primary-chain/VaultBridgeTokenTest.t.sol index 95af8314..78ce229b 100644 --- a/test/GenericVaultBridgeToken.t.sol +++ b/test/unit/primary-chain/VaultBridgeTokenTest.t.sol @@ -1,171 +1,48 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; -import "forge-std/Test.sol"; +// Test base +import "test/base/primary-chain/VaultBridgeTokenTestBase.sol"; -import {GenericVaultBridgeToken} from "src/vault-bridge-tokens/GenericVaultBridgeToken.sol"; +// OpenZeppelin +import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {Initializable} from "@openzeppelin-contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {VaultBridgeToken, PausableUpgradeable, Initializable} from "src/VaultBridgeToken.sol"; -import {VaultBridgeTokenInitializer} from "src/VaultBridgeTokenInitializer.sol"; -import {VaultBridgeTokenPart2} from "src/VaultBridgeTokenPart2.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import {TestVault} from "test/etc/TestVault.sol"; -import {ILxLyBridge as _ILxLyBridge} from "test/interfaces/ILxLyBridge.sol"; +// Mocks +import {IAgglayerBridge as _IAgglayerBridge} from "test/interfaces/IAgglayerBridge.sol"; +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockVault} from "test/utils/mocks/MockVault.sol"; -contract GenericVaultBridgeTokenTest is Test { +/// @dev Tests for VaultBridgeToken and VaultBridgeTokenPart2 +contract VaultBridgeTokenTest is VaultBridgeTokenTestBase { using SafeERC20 for IERC20; - using SafeERC20 for GenericVaultBridgeToken; - - // constants - address constant LXLY_BRIDGE = 0x2a3DD3EB832aF982ec71669E178424b10Dca2EDe; - uint32 constant NETWORK_ID_L1 = 0; - uint32 constant NETWORK_ID_L2 = 1; - uint8 constant LEAF_TYPE_ASSET = 0; - bytes32 constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - bytes4 constant PERMIT_SIGNATURE = 0xd505accf; - address internal constant TEST_TOKEN = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - uint256 internal constant MAX_RESERVE_PERCENTAGE = 1e18; - uint256 internal constant MAX_DEPOSIT = 10e18; - uint256 internal constant MAX_WITHDRAW = 10e18; - uint256 internal constant MINIMUM_YIELD_VAULT_DEPOSIT = 1e12; - uint256 internal constant YIELD_VAULT_ALLOWED_SLIPPAGE = 1e16; // 1% - bytes32 internal constant RESERVE_ASSET_STORAGE = - hex"f082fbc4cfb4d172ba00d34227e208a31ceb0982bc189440d519185302e44702"; - - uint256 stateBeforeInitialize; - uint256 mainnetFork; - TestVault vbTokenVault; - GenericVaultBridgeToken vbToken; - VaultBridgeTokenPart2 vbTokenPart2; - address vbTokenImplementation; - address asset; - - address migrationManager = makeAddr("migrationManager"); - address owner = makeAddr("owner"); - address recipient = makeAddr("recipient"); - uint256 senderPrivateKey = 0xBEEF; - address sender = vm.addr(senderPrivateKey); - address yieldRecipient = makeAddr("yieldRecipient"); - - // events - event BridgeEvent( - uint8 leafType, - uint32 originNetwork, - address originAddress, - uint32 destinationNetwork, - address destinationAddress, - uint256 amount, - bytes metadata, - uint32 depositCount - ); - - // vbToken metadata - string version; - string name; - string symbol; - uint256 decimals; - uint256 minimumReservePercentage; - bytes vbTokenMetaData; - address initializer; + using SafeERC20 for TestHarnessVaultBridgeToken; function setUp() public virtual { - mainnetFork = vm.createSelectFork("mainnet"); - - asset = TEST_TOKEN; - vbTokenVault = new TestVault(asset); - version = "0.5.0"; - name = "Vault Bridge USDC"; - symbol = "vbUSDC"; - decimals = 6; - vbTokenMetaData = abi.encode(name, symbol, decimals); - minimumReservePercentage = 1e17; - initializer = address(new VaultBridgeTokenInitializer()); - - vbTokenVault.setMaxDeposit(MAX_DEPOSIT); - vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); - - // deploy the vbToken part 2 - vbTokenPart2 = new VaultBridgeTokenPart2(); - - vbToken = new GenericVaultBridgeToken(); - vbTokenImplementation = address(vbToken); - stateBeforeInitialize = vm.snapshotState(); - VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ - owner: owner, - name: name, - symbol: symbol, - underlyingToken: asset, - minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), - yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, - minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, - yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) - }); - bytes memory initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbTokenImplementation), address(this), initData))); - vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); - - // fund the migration manager manually since the test is not using the actual migration manager - deal(asset, migrationManager, 10000000 ether); - vm.prank(migrationManager); - IERC20(asset).forceApprove(address(vbToken), 10000000 ether); - - vm.label(address(vbTokenVault), "vbToken Vault"); - vm.label(address(vbToken), "vbToken"); - vm.label(address(vbTokenImplementation), "vbToken Implementation"); - vm.label(address(this), "Default Address"); - vm.label(asset, "Underlying Asset"); - vm.label(migrationManager, "Migration Manager"); - vm.label(owner, "Owner"); - vm.label(recipient, "Recipient"); - vm.label(sender, "Sender"); - vm.label(yieldRecipient, "Yield Recipient"); - vm.label(LXLY_BRIDGE, "Lxly Bridge"); - vm.label(initializer, "Initializer"); - vm.label(address(vbTokenPart2), "vbToken Part 2"); - } - - function test_setup() public view { - assert(vbToken.hasRole(vbToken.DEFAULT_ADMIN_ROLE(), owner)); - assertEq(vbToken.name(), name); - assertEq(vbToken.symbol(), symbol); - assertEq(vbToken.decimals(), decimals); - assertEq(vbToken.asset(), asset); - assertEq(vbToken.minimumReservePercentage(), minimumReservePercentage); - assertEq(address(vbToken.yieldVault()), address(vbTokenVault)); - assertEq(vbToken.yieldRecipient(), yieldRecipient); - assertEq(address(vbToken.lxlyBridge()), LXLY_BRIDGE); - assertEq(vbToken.migrationManager(), migrationManager); - assertEq(vbToken.allowance(address(vbToken), LXLY_BRIDGE), type(uint256).max); - assertEq(IERC20(asset).allowance(address(vbToken), address(vbToken.yieldVault())), type(uint256).max); + deployVaultBridgeTokenInfrastructure(); } function test_initialize_twice() public { vm.expectRevert(Initializable.InvalidInitialization.selector); VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ owner: owner, - name: name, - symbol: symbol, - underlyingToken: asset, + name: tokenName, + symbol: tokenSymbol, + underlyingToken: underlyingToken, minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), + yieldVault: address(yieldVault), yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, + agglayerBridge: agglayerBridge, minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, + migrationManager: migrationManagerAddr, yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, vaultBridgeTokenPart2: address(vbTokenPart2) }); - vbToken.initialize(initializer, initParams); + vbToken.reinitialize1(address(initializer), initParams); } function test_initialize() public virtual { @@ -174,88 +51,88 @@ contract GenericVaultBridgeTokenTest is Test { bytes memory initData; VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ owner: address(0), - name: name, - symbol: symbol, - underlyingToken: asset, + name: tokenName, + symbol: tokenSymbol, + underlyingToken: underlyingToken, minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), + yieldVault: address(yieldVault), yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, + agglayerBridge: agglayerBridge, minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, + migrationManager: migrationManagerAddr, yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, vaultBridgeTokenPart2: address(vbTokenPart2) }); - initData = abi.encodeCall(vbToken.initialize, (address(0), initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(0), initParams)); vm.expectRevert(VaultBridgeToken.InvalidInitializer.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidOwner.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); vm.revertToState(stateBeforeInitialize); initParams.owner = owner; initParams.name = ""; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidName.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.name = name; + initParams.name = tokenName; initParams.symbol = ""; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidSymbol.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.symbol = symbol; + initParams.symbol = tokenSymbol; initParams.underlyingToken = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); /// forge-config: default.allow_internal_expect_revert = true vm.expectRevert(VaultBridgeToken.InvalidUnderlyingToken.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.underlyingToken = asset; + initParams.underlyingToken = underlyingToken; initParams.minimumReservePercentage = 1e19; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidMinimumReservePercentage.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); initParams.minimumReservePercentage = minimumReservePercentage; initParams.yieldVault = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidYieldVault.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.yieldVault = address(vbTokenVault); + initParams.yieldVault = address(yieldVault); initParams.yieldRecipient = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidYieldRecipient.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); initParams.yieldRecipient = yieldRecipient; - initParams.lxlyBridge = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidLxLyBridge.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + initParams.agglayerBridge = address(0); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); + vm.expectRevert(VaultBridgeToken.InvalidAgglayerBridge.selector); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.lxlyBridge = LXLY_BRIDGE; + initParams.agglayerBridge = agglayerBridge; initParams.migrationManager = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidMigrationManager.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - initParams.migrationManager = migrationManager; + initParams.migrationManager = migrationManagerAddr; initParams.yieldVaultMaximumSlippagePercentage = 1e19; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidYieldVaultMaximumSlippagePercentage.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); initParams.yieldVaultMaximumSlippagePercentage = YIELD_VAULT_ALLOWED_SLIPPAGE; initParams.vaultBridgeTokenPart2 = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); + initData = abi.encodeCall(vbToken.reinitialize1, (address(initializer), initParams)); vm.expectRevert(VaultBridgeToken.InvalidVaultBridgeTokenPart2.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); + vbToken = TestHarnessVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); } function test_deposit_revert() public { @@ -264,7 +141,7 @@ contract GenericVaultBridgeTokenTest is Test { bytes memory callData = abi.encodeCall(vbToken.deposit, (amount, recipient)); _testPauseUnpause(owner, address(vbToken), callData); - deal(asset, sender, amount); + deal(underlyingToken, sender, amount); vm.startPrank(sender); @@ -281,16 +158,15 @@ contract GenericVaultBridgeTokenTest is Test { } function test_deposit_amount_gt_max_deposit() public { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 + 1; // account for the minimum reserve percentage and add to make the amount greater than the max deposit limit assertGt(amount, MINIMUM_YIELD_VAULT_DEPOSIT, "Amount should be greater than the minimum deposit."); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); uint256 sharesToBeMinted = vbToken.previewDeposit(amount); - IERC20(asset).forceApprove(address(vbToken), amount); vm.expectEmit(); emit IERC4626.Deposit(sender, recipient, amount, sharesToBeMinted); vbToken.deposit(amount, recipient); @@ -298,10 +174,10 @@ contract GenericVaultBridgeTokenTest is Test { vm.stopPrank(); // since max deposit is reached, the reserve amount should be calculated based on the max deposit limit - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased - assertGt(vbTokenVault.balanceOf(address(vbToken)), 0); // shares locked in the vault + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased + assertGt(yieldVault.balanceOf(address(vbToken)), 0); // shares locked in the vault assertEq(vbToken.balanceOf(recipient), sharesToBeMinted); // shares minted to the recipient } @@ -309,7 +185,7 @@ contract GenericVaultBridgeTokenTest is Test { uint256 amount = 100 ether; // use a large amount to ensure reserve exceeds the threshold assertGt(amount, MAX_DEPOSIT, "Amount should be greater than the max deposit."); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, MAX_DEPOSIT); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, MAX_DEPOSIT); uint256 reserveThreshold = 3 * minimumReservePercentage; // the threshold is set to 3x the minimum reserve percentage according to the spec uint256 maxDepositPercentage = Math.mulDiv(reserveAssetsAfterDeposit, 1e18, amount); @@ -319,12 +195,11 @@ contract GenericVaultBridgeTokenTest is Test { "Max deposit percentage should be greater than the reserve threshold." ); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); uint256 sharesToBeMinted = vbToken.previewDeposit(amount); - IERC20(asset).forceApprove(address(vbToken), amount); vm.expectEmit(); emit IERC4626.Deposit(sender, recipient, amount, sharesToBeMinted); vbToken.deposit(amount, recipient); @@ -333,24 +208,23 @@ contract GenericVaultBridgeTokenTest is Test { // since the reserve percentage is above the threshold, the reserve amount should be calculated based on the rebalanced amount uint256 newAmount = reserveAssetsAfterDeposit; - uint256 finalReserveAssets = _calculateReserveAssets(newAmount, MAX_DEPOSIT); + uint256 finalReserveAssets = calculateReserveAssets(newAmount, MAX_DEPOSIT); - assertEq(IERC20(asset).balanceOf(address(vbToken)), finalReserveAssets); // reserve assets increased - assertGt(vbTokenVault.balanceOf(address(vbToken)), 0); // shares locked in the vault + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), finalReserveAssets); // reserve assets increased + assertGt(yieldVault.balanceOf(address(vbToken)), 0); // shares locked in the vault assertEq(vbToken.balanceOf(recipient), sharesToBeMinted); // shares minted to the recipient } function test_deposit_amount_lt_max_deposit() public { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 - 1; // account for the minimum reserve percentage and subtract to make the amount less than the max deposit limit assertGt(amount, MINIMUM_YIELD_VAULT_DEPOSIT, "Amount should be greater than the minimum deposit."); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); uint256 sharesToBeMinted = vbToken.previewDeposit(amount); - IERC20(asset).forceApprove(address(vbToken), amount); vm.expectEmit(); emit IERC4626.Deposit(sender, recipient, amount, sharesToBeMinted); vbToken.deposit(amount, recipient); @@ -360,35 +234,34 @@ contract GenericVaultBridgeTokenTest is Test { // since max deposit is not reached, the reserve amount should be calculated based on the deposit amount uint256 reserveAssets = (amount * minimumReservePercentage) / MAX_RESERVE_PERCENTAGE; - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssets); // reserve assets increased - assertGt(vbTokenVault.balanceOf(address(vbToken)), 0); // shares locked in the vault + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssets); // reserve assets increased + assertGt(yieldVault.balanceOf(address(vbToken)), 0); // shares locked in the vault assertEq(vbToken.balanceOf(recipient), sharesToBeMinted); // shares minted to the recipient } function test_deposit_amount_lt_minimum_deposit() public { uint256 amount = MINIMUM_YIELD_VAULT_DEPOSIT - 1; - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); uint256 sharesToBeMinted = vbToken.previewDeposit(amount); - IERC20(asset).forceApprove(address(vbToken), amount); vm.expectEmit(); emit IERC4626.Deposit(sender, recipient, amount, sharesToBeMinted); vbToken.deposit(amount, recipient); vm.stopPrank(); - assertEq(IERC20(asset).balanceOf(address(vbToken)), amount); // All assets are reserved and non are deposited in the vault - assertEq(vbTokenVault.balanceOf(address(vbToken)), 0); // No assets deposited in the vault + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), amount); // All assets are reserved and non are deposited in the vault + assertEq(yieldVault.balanceOf(address(vbToken)), 0); // No assets deposited in the vault assertEq(vbToken.balanceOf(recipient), sharesToBeMinted); // shares minted to the recipient } function test_depositWithPermit_revert() public { uint256 amount = 100 ether; - deal(asset, sender, amount); + deal(underlyingToken, sender, amount); vm.startPrank(sender); vm.expectRevert(VaultBridgeToken.InvalidPermitData.selector); @@ -399,11 +272,11 @@ contract GenericVaultBridgeTokenTest is Test { function test_depositWithPermit() public virtual { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); - deal(asset, sender, amount); + deal(underlyingToken, sender, amount); - bytes32 domainSeparator = IERC20Permit(asset).DOMAIN_SEPARATOR(); + bytes32 domainSeparator = IERC20Permit(underlyingToken).DOMAIN_SEPARATOR(); (uint8 v, bytes32 r, bytes32 s) = vm.sign( senderPrivateKey, @@ -430,9 +303,9 @@ contract GenericVaultBridgeTokenTest is Test { vbToken.depositWithPermit(amount, recipient, permitData); vm.stopPrank(); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased assertEq(vbToken.balanceOf(recipient), sharesToBeMinted); // shares minted to the recipient } @@ -445,42 +318,41 @@ contract GenericVaultBridgeTokenTest is Test { function test_depositAndBridge() public { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); bytes memory callData = abi.encodeCall(vbToken.depositAndBridge, (amount, recipient, NETWORK_ID_L2, true)); _testPauseUnpause(owner, address(vbToken), callData); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vm.expectEmit(); - emit BridgeEvent( + emit MockAgglayerBridge.BridgeEvent( LEAF_TYPE_ASSET, NETWORK_ID_L1, address(vbToken), NETWORK_ID_L2, recipient, amount, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() + tokenMetadata, + _IAgglayerBridge(agglayerBridge).depositCount() ); vbToken.depositAndBridge(amount, recipient, NETWORK_ID_L2, true); vm.stopPrank(); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased - assertEq(vbToken.balanceOf(LXLY_BRIDGE), amount); // shares locked on bridge + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); // reserve assets increased + assertEq(vbToken.balanceOf(agglayerBridge), amount); // shares locked on bridge } function test_depositAndBridgePermit() public virtual { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); - deal(asset, sender, amount); + deal(underlyingToken, sender, amount); - bytes32 domainSeparator = IERC20Permit(asset).DOMAIN_SEPARATOR(); + bytes32 domainSeparator = IERC20Permit(underlyingToken).DOMAIN_SEPARATOR(); (uint8 v, bytes32 r, bytes32 s) = vm.sign( senderPrivateKey, @@ -501,43 +373,42 @@ contract GenericVaultBridgeTokenTest is Test { vm.startPrank(sender); vm.expectEmit(); - emit BridgeEvent( + emit MockAgglayerBridge.BridgeEvent( LEAF_TYPE_ASSET, NETWORK_ID_L1, address(vbToken), NETWORK_ID_L2, recipient, amount, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() + tokenMetadata, + _IAgglayerBridge(agglayerBridge).depositCount() ); vbToken.depositWithPermitAndBridge(amount, recipient, NETWORK_ID_L2, true, permitData); vm.stopPrank(); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); - assertEq(vbToken.balanceOf(LXLY_BRIDGE), amount); // shares locked on bridge + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); + assertEq(vbToken.balanceOf(agglayerBridge), amount); // shares locked on bridge } function test_mint() public virtual { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); bytes memory callData = abi.encodeCall(vbToken.mint, (amount, recipient)); _testPauseUnpause(owner, address(vbToken), callData); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); uint256 sharesToBeMinted = vbToken.previewMint(amount); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.mint(amount, sender); vm.stopPrank(); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAssetsAfterDeposit); assertEq(vbToken.balanceOf(sender), sharesToBeMinted); // shares minted to the recipient } @@ -558,13 +429,13 @@ contract GenericVaultBridgeTokenTest is Test { uint256 stateBeforeDeposit = vm.snapshotState(); - deal(asset, sender, amountGtMaxWithdraw); + deal(underlyingToken, sender, amountGtMaxWithdraw); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amountGtMaxWithdraw); + IERC20(underlyingToken).forceApprove(address(vbToken), amountGtMaxWithdraw); vbToken.deposit(amountGtMaxWithdraw, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); // make sure sender has deposited all assets + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); // make sure sender has deposited all assets assertEq(vbToken.balanceOf(sender), amountGtMaxWithdraw); uint256 withdrawableAmount = _calculateWithdrawableAmount(amountGtMaxWithdraw); @@ -578,32 +449,32 @@ contract GenericVaultBridgeTokenTest is Test { uint256 amountLtMaxWithdraw = MAX_WITHDRAW - 1; - deal(asset, sender, amountLtMaxWithdraw); + deal(underlyingToken, sender, amountLtMaxWithdraw); - IERC20(asset).forceApprove(address(vbToken), amountLtMaxWithdraw); + IERC20(underlyingToken).forceApprove(address(vbToken), amountLtMaxWithdraw); vbToken.deposit(amountLtMaxWithdraw, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); // make sure sender has deposited all assets + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); // make sure sender has deposited all assets assertEq(vbToken.balanceOf(sender), amountLtMaxWithdraw); - vm.expectRevert("TestVault: Insufficient balance"); + vm.expectRevert("MockVault: Insufficient balance"); vbToken.withdraw(amountLtMaxWithdraw + 1, sender, sender); vm.revertToState(stateBeforeDeposit); uint256 amount = 1 ether; - deal(asset, sender, amount); + deal(underlyingToken, sender, amount); - IERC20(asset).forceApprove(address(vbToken), amount); + IERC20(underlyingToken).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); // make sure sender has deposited all assets + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); // make sure sender has deposited all assets assertEq(vbToken.balanceOf(sender), amount); uint256 withdrawAmount = vbToken.stakedAssets(); uint256 slippageAmount = Math.mulDiv( withdrawAmount, YIELD_VAULT_ALLOWED_SLIPPAGE + Math.mulDiv(YIELD_VAULT_ALLOWED_SLIPPAGE, 1, 100), 1e18 ); - vbTokenVault.setSlippage(true, slippageAmount); + yieldVault.setSlippage(true, slippageAmount); vm.expectRevert( abi.encodeWithSelector( @@ -619,25 +490,24 @@ contract GenericVaultBridgeTokenTest is Test { function test_withdraw_from_reserve() public virtual { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); // make sure sender has deposited all assets + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); // make sure sender has deposited all assets assertEq(vbToken.balanceOf(sender), amount); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); uint256 reserveWithdrawAmount = (reserveAssetsAfterDeposit * 90) / 100; // withdraw 90% of reserve assets uint256 reserveAfterWithdraw = reserveAssetsAfterDeposit - reserveWithdrawAmount; vm.expectEmit(); emit IERC4626.Withdraw(sender, sender, sender, reserveWithdrawAmount, reserveWithdrawAmount); vbToken.withdraw(reserveWithdrawAmount, sender, sender); - assertEq(IERC20(asset).balanceOf(address(vbToken)), reserveAfterWithdraw); // reserve assets reduced - assertEq(IERC20(asset).balanceOf(sender), reserveWithdrawAmount); // assets returned to sender + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), reserveAfterWithdraw); // reserve assets reduced + assertEq(IERC20(underlyingToken).balanceOf(sender), reserveWithdrawAmount); // assets returned to sender assertEq(vbToken.balanceOf(sender), amount - reserveWithdrawAmount); // shares reduced vm.stopPrank(); @@ -646,11 +516,10 @@ contract GenericVaultBridgeTokenTest is Test { function test_withdraw_from_stake() public virtual { uint256 amount = 1 ether; - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); // make sure sender has deposited all assets + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); // make sure sender has deposited all assets assertEq(vbToken.balanceOf(sender), amount); uint256 amountToWithdraw = amount; @@ -658,9 +527,9 @@ contract GenericVaultBridgeTokenTest is Test { vm.expectEmit(); emit IERC4626.Withdraw(sender, sender, sender, amountToWithdraw, amountToWithdraw); vbToken.withdraw(amountToWithdraw, sender, sender); - assertEq(IERC20(asset).balanceOf(sender), amountToWithdraw); - assertEq(IERC20(asset).balanceOf(address(vbToken)), 0); // reserve assets reduced - assertEq(IERC20(asset).balanceOf(sender), amountToWithdraw); // assets returned to sender + assertEq(IERC20(underlyingToken).balanceOf(sender), amountToWithdraw); + assertEq(IERC20(underlyingToken).balanceOf(address(vbToken)), 0); // reserve assets reduced + assertEq(IERC20(underlyingToken).balanceOf(sender), amountToWithdraw); // assets returned to sender assertEq(vbToken.balanceOf(sender), amount - amountToWithdraw); // shares reduced vm.stopPrank(); } @@ -674,19 +543,17 @@ contract GenericVaultBridgeTokenTest is Test { } function test_rebalanceReserve_below() public virtual { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 + 1; // account for the minimum reserve percentage and add to make the amount greater than the max deposit limit assertGt(amount, MINIMUM_YIELD_VAULT_DEPOSIT, "Amount should be greater than the minimum deposit."); uint256 totalSupply; - deal(asset, sender, amount); // fund the sender + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); // fund the sender - uint256 reserveAmount = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAmount = calculateReserveAssets(amount, vaultMaxDeposit); // create reserve vm.startPrank(sender); - - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, recipient); totalSupply += amount; assertEq(vbToken.reservedAssets(), reserveAmount); @@ -698,7 +565,8 @@ contract GenericVaultBridgeTokenTest is Test { vm.stopPrank(); - uint256 finalReserveAmount = totalSupply * minimumReservePercentage / MAX_RESERVE_PERCENTAGE; + uint256 finalReserveAmount = + Math.mulDiv(totalSupply, minimumReservePercentage, MAX_RESERVE_PERCENTAGE, Math.Rounding.Ceil); uint256 finalPercentage = finalReserveAmount * 1e18 / totalSupply; vm.expectEmit(); emit VaultBridgeToken.ReserveRebalanced(reserveAmount - 100, finalReserveAmount, finalPercentage); @@ -710,18 +578,16 @@ contract GenericVaultBridgeTokenTest is Test { } function test_rebalanceReserve_above() public virtual { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = vaultMaxDeposit / 2; assertGt(amount, MINIMUM_YIELD_VAULT_DEPOSIT, "Amount should be greater than the minimum deposit."); uint256 totalSupply; - deal(asset, sender, amount); // fund the sender + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); // fund the sender - uint256 reserveAmount = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAmount = calculateReserveAssets(amount, vaultMaxDeposit); vm.startPrank(sender); - - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, recipient); totalSupply += amount; assertEq(vbToken.reservedAssets(), reserveAmount); @@ -757,16 +623,15 @@ contract GenericVaultBridgeTokenTest is Test { uint256 amount = 100 ether; uint256 yieldInAssets = 500 ether; - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.mint(amount, sender); vm.stopPrank(); - uint256 sharesBalanceBefore = vbTokenVault.balanceOf(address(vbToken)); - uint256 yieldShares = vbTokenVault.convertToShares(yieldInAssets); + uint256 sharesBalanceBefore = yieldVault.balanceOf(address(vbToken)); + uint256 yieldShares = yieldVault.convertToShares(yieldInAssets); - deal(address(vbTokenVault), address(vbToken), sharesBalanceBefore + yieldShares); // add yield to the vault + deal(address(yieldVault), address(vbToken), sharesBalanceBefore + yieldShares); // add yield to the vault uint256 expectedYieldAssets = vbToken.yield(); @@ -802,16 +667,15 @@ contract GenericVaultBridgeTokenTest is Test { address newRecipient = makeAddr("newRecipient"); // generate yield - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.mint(amount, sender); vm.stopPrank(); - uint256 sharesBalanceBefore = vbTokenVault.balanceOf(address(vbToken)); - uint256 yieldShares = vbTokenVault.convertToShares(yieldInAssets); + uint256 sharesBalanceBefore = yieldVault.balanceOf(address(vbToken)); + uint256 yieldShares = yieldVault.convertToShares(yieldInAssets); - deal(address(vbTokenVault), address(vbToken), sharesBalanceBefore + yieldShares); // add yield to the vault + deal(address(yieldVault), address(vbToken), sharesBalanceBefore + yieldShares); // add yield to the vault uint256 expectedYieldAssets = vbToken.yield(); @@ -856,18 +720,17 @@ contract GenericVaultBridgeTokenTest is Test { function test_redeem() public virtual { uint256 amount = 1 ether; - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); - assertEq(IERC20(asset).balanceOf(sender), 0); + assertEq(IERC20(underlyingToken).balanceOf(sender), 0); assertEq(vbToken.balanceOf(sender), amount); uint256 redeemAmount = vbToken.totalAssets(); vbToken.redeem(redeemAmount, sender, sender); // redeem from both staked and reserved assets - assertEq(IERC20(asset).balanceOf(sender), redeemAmount); + assertEq(IERC20(underlyingToken).balanceOf(sender), redeemAmount); vm.stopPrank(); } @@ -879,7 +742,7 @@ contract GenericVaultBridgeTokenTest is Test { vm.expectRevert(VaultBridgeToken.Unauthorized.selector); vbTokenPart2.completeMigration(NETWORK_ID_L2, 100, 100); - vm.startPrank(migrationManager); + vm.startPrank(migrationManagerAddr); // Wrong network id vm.expectRevert(VaultBridgeToken.InvalidOriginNetwork.selector); @@ -892,30 +755,30 @@ contract GenericVaultBridgeTokenTest is Test { } function test_completeMigration_no_discrepancy_shares_lt_max_deposit() public { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 - 1; uint256 shares = vbToken.convertToShares(amount); - deal(asset, address(vbToken), amount); + deal(underlyingToken, address(vbToken), amount); uint256 stakedAssetsBefore = vbToken.stakedAssets(); vm.expectEmit(); - emit BridgeEvent( + emit MockAgglayerBridge.BridgeEvent( LEAF_TYPE_ASSET, NETWORK_ID_L1, address(vbToken), NETWORK_ID_L2, address(0), shares, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() + tokenMetadata, + _IAgglayerBridge(agglayerBridge).depositCount() ); vm.expectEmit(); - emit IERC4626.Deposit(migrationManager, address(vbToken), amount, shares); + emit IERC4626.Deposit(migrationManagerAddr, address(vbToken), amount, shares); vm.expectEmit(); emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, amount, 0); - vm.prank(migrationManager); + vm.prank(migrationManagerAddr); vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, amount); uint256 reserveAssetsAfterDeposit = @@ -926,78 +789,80 @@ contract GenericVaultBridgeTokenTest is Test { } function test_completeMigration_no_discrepancy_shares_gt_max_deposit() public { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 + 1; uint256 shares = vbToken.convertToShares(amount); - deal(asset, address(vbToken), amount); + deal(underlyingToken, address(vbToken), amount); uint256 stakedAssetsBefore = vbToken.stakedAssets(); vm.expectEmit(); - emit BridgeEvent( + emit MockAgglayerBridge.BridgeEvent( LEAF_TYPE_ASSET, NETWORK_ID_L1, address(vbToken), NETWORK_ID_L2, address(0), shares, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() + tokenMetadata, + _IAgglayerBridge(agglayerBridge).depositCount() ); vm.expectEmit(); - emit IERC4626.Deposit(migrationManager, address(vbToken), amount, shares); + emit IERC4626.Deposit(migrationManagerAddr, address(vbToken), amount, shares); vm.expectEmit(); emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, amount, 0); - vm.prank(migrationManager); + vm.prank(migrationManagerAddr); vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, amount); // since max deposit is reached, the reserve amount should be calculated based on the max deposit limit - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); assertEq(vbToken.reservedAssets(), reserveAssetsAfterDeposit); assertGt(vbToken.stakedAssets(), stakedAssetsBefore); } function test_completeMigration_with_discrepancy() public { - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 amount = (vaultMaxDeposit * 10) / 9 - 1; uint256 shares = vbToken.convertToShares(amount) + vbToken.convertToShares(1); // no migration fees funds vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.CannotCompleteMigration.selector, shares, amount, 0)); - vm.prank(migrationManager); + vm.prank(migrationManagerAddr); vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, amount); // fund the migration fees - deal(asset, address(this), amount); - IERC20(asset).forceApprove(address(vbToken), amount); + deal(underlyingToken, address(this), amount); + IERC20(underlyingToken).forceApprove(address(vbToken), amount); vm.expectEmit(); emit VaultBridgeToken.DonatedForCompletingMigration(address(this), amount); vbTokenPart2.donateForCompletingMigration(amount); + assertEq(vbToken.migrationFeesFund(), amount); uint256 stakedAssetsBefore = vbToken.stakedAssets(); vm.expectEmit(); - emit BridgeEvent( + emit MockAgglayerBridge.BridgeEvent( LEAF_TYPE_ASSET, NETWORK_ID_L1, address(vbToken), NETWORK_ID_L2, address(0), shares, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() + tokenMetadata, + _IAgglayerBridge(agglayerBridge).depositCount() ); vm.expectEmit(); - emit IERC4626.Deposit(migrationManager, address(vbToken), amount, shares); + emit IERC4626.Deposit(migrationManagerAddr, address(vbToken), amount, shares); vm.expectEmit(); emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, amount, shares - amount); - vm.prank(migrationManager); + vm.prank(migrationManagerAddr); vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, amount); - uint256 reserveAssetsAfterDeposit = - vbToken.convertToAssets(shares) * minimumReservePercentage / MAX_RESERVE_PERCENTAGE; + uint256 reserveAssetsAfterDeposit = Math.mulDiv( + vbToken.convertToAssets(shares), minimumReservePercentage, MAX_RESERVE_PERCENTAGE, Math.Rounding.Ceil + ); assertEq(vbToken.reservedAssets(), reserveAssetsAfterDeposit); assertGt(vbToken.stakedAssets(), stakedAssetsBefore); @@ -1054,9 +919,8 @@ contract GenericVaultBridgeTokenTest is Test { assertEq(vbToken.maxWithdraw(address(0)), 0); // 0 if no shares - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); vm.stopPrank(); @@ -1065,7 +929,7 @@ contract GenericVaultBridgeTokenTest is Test { function test_previewWithdraw() public virtual { uint256 amount = 11 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); bytes memory callData = abi.encodeCall(vbToken.previewWithdraw, (amount)); _testPauseUnpause(owner, address(vbToken), callData); @@ -1073,9 +937,8 @@ contract GenericVaultBridgeTokenTest is Test { vm.expectRevert(VaultBridgeToken.InvalidAssets.selector); vbToken.previewWithdraw(0); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); vm.stopPrank(); @@ -1086,8 +949,8 @@ contract GenericVaultBridgeTokenTest is Test { ); vbToken.previewWithdraw(amount + 1 ether); - uint256 stakedAmount = vbTokenVault.convertToAssets(vbTokenVault.balanceOf(address(vbToken))); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 stakedAmount = yieldVault.convertToAssets(yieldVault.balanceOf(address(vbToken))); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); vm.assertEq(vbToken.previewWithdraw(reserveAssetsAfterDeposit), vbToken.reservedAssets()); // reserve assets vm.assertEq(vbToken.previewWithdraw(reserveAssetsAfterDeposit + stakedAmount), vbToken.totalAssets()); // reserve + staked assets @@ -1104,9 +967,8 @@ contract GenericVaultBridgeTokenTest is Test { assertEq(vbToken.maxRedeem(sender), 0); // 0 if no shares - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); vm.stopPrank(); @@ -1115,7 +977,7 @@ contract GenericVaultBridgeTokenTest is Test { function test_previewRedeem() public virtual { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); uint256 reserveAmount = (amount * minimumReservePercentage) / MAX_RESERVE_PERCENTAGE; bytes memory callData = abi.encodeCall(vbToken.previewRedeem, (amount)); @@ -1124,13 +986,12 @@ contract GenericVaultBridgeTokenTest is Test { vm.expectRevert(VaultBridgeToken.InvalidShares.selector); vbToken.previewRedeem(0); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); vm.stopPrank(); - uint256 stakedAmount = vbTokenVault.convertToAssets(vbTokenVault.balanceOf(address(vbToken))); + uint256 stakedAmount = yieldVault.convertToAssets(yieldVault.balanceOf(address(vbToken))); uint256 assetsToDeposit = amount - reserveAmount; uint256 assetsToDepositMax = (assetsToDeposit > vaultMaxDeposit) ? vaultMaxDeposit : assetsToDeposit; uint256 reserveAssetsAfterDeposit = amount - assetsToDepositMax; @@ -1141,21 +1002,256 @@ contract GenericVaultBridgeTokenTest is Test { function test_reservePercentage() public { uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbToken)); - deal(asset, sender, amount); + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); vm.startPrank(sender); - IERC20(asset).forceApprove(address(vbToken), amount); vbToken.deposit(amount, sender); vm.stopPrank(); - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); uint256 expectedPercentage = (reserveAssetsAfterDeposit * MAX_RESERVE_PERCENTAGE) / vbToken.totalSupply(); assertEq(vbToken.reservePercentage(), expectedPercentage); } + function test_burn_revert() public virtual { + uint256 amount = 1 ether; + + // Only yield recipient can burn + vm.expectRevert(VaultBridgeToken.Unauthorized.selector); + vbTokenPart2.burn(amount); + + // Cannot burn 0 shares + vm.expectRevert(VaultBridgeToken.InvalidShares.selector); + vm.prank(yieldRecipient); + vbTokenPart2.burn(0); + } + + function test_burn() public virtual { + uint256 amount = 1 ether; + + // First, set up yield to burn - need to deposit, collect yield, then burn + _dealAndApprove(underlyingToken, sender, amount, address(vbToken)); + vm.startPrank(sender); + vbToken.deposit(amount, sender); + vm.stopPrank(); + + // Generate some yield by adding shares to the vault + uint256 yieldShares = 0.1 ether; + uint256 sharesBalanceBefore = yieldVault.balanceOf(address(vbToken)); + deal(address(yieldVault), address(vbToken), sharesBalanceBefore + yieldShares); + + // Collect yield to mint vbTokens to yield recipient + vm.prank(owner); + vbTokenPart2.collectYield(); + + uint256 yieldRecipientBalance = vbToken.balanceOf(yieldRecipient); + assertGt(yieldRecipientBalance, 0); + + // Burn half of the yield recipient's balance + uint256 burnAmount = yieldRecipientBalance / 2; + + vm.expectEmit(); + emit VaultBridgeToken.Burned(burnAmount); + vm.prank(yieldRecipient); + vbTokenPart2.burn(burnAmount); + + assertEq(vbToken.balanceOf(yieldRecipient), yieldRecipientBalance - burnAmount); + } + + function test_donateAsYield_revert() public { + // Cannot donate 0 assets + vm.expectRevert(VaultBridgeToken.InvalidAssets.selector); + vbTokenPart2.donateAsYield(0); + } + + function test_donateAsYield() public { + uint256 amount = 1 ether; + + // Get initial reserved assets + uint256 initialReservedAssets = vbToken.reservedAssets(); + + deal(underlyingToken, address(this), amount); + IERC20(underlyingToken).forceApprove(address(vbToken), amount); + + vm.expectEmit(); + emit VaultBridgeToken.DonatedAsYield(address(this), amount); + vbTokenPart2.donateAsYield(amount); + + assertEq(vbToken.reservedAssets(), initialReservedAssets + amount); + assertEq(IERC20(underlyingToken).balanceOf(address(this)), 0); + } + + function test_drainYieldVault_revert() public { + uint256 shares = 1 ether; + + // Only admin can drain yield vault + vm.expectRevert(); + vbTokenPart2.drainYieldVault(shares, false); + + vm.startPrank(owner); + + // Cannot drain 0 shares + vm.expectRevert(VaultBridgeToken.InvalidShares.selector); + vbTokenPart2.drainYieldVault(0, false); + + vm.stopPrank(); + } + + function test_drainYieldVault() public { + uint256 depositAmount = 10 ether; + + // First, deposit some assets to generate yield vault shares + _dealAndApprove(underlyingToken, sender, depositAmount, address(vbToken)); + vm.startPrank(sender); + vbToken.deposit(depositAmount, sender); + vm.stopPrank(); + + uint256 initialReservedAssets = vbToken.reservedAssets(); + uint256 vaultSharesBalance = yieldVault.balanceOf(address(vbToken)); + + // Only proceed if there are vault shares to drain + vm.assume(vaultSharesBalance > 0); + + // Calculate shares to drain (half of the balance) + uint256 sharesToDrain = vaultSharesBalance / 2; + + // Preview how many assets we expect to receive + uint256 expectedAssets = yieldVault.previewRedeem(sharesToDrain); + + vm.expectEmit(); + emit VaultBridgeToken.YieldVaultDrained(sharesToDrain, expectedAssets); + vm.prank(owner); + vbTokenPart2.drainYieldVault(sharesToDrain, false); + + // Verify that reserved assets increased by the drained amount + assertEq(vbToken.reservedAssets(), initialReservedAssets + expectedAssets); + // Verify that yield vault shares decreased + assertEq(yieldVault.balanceOf(address(vbToken)), vaultSharesBalance - sharesToDrain); + } + + function test_setYieldVault_revert() public { + address newVault = makeAddr("newVault"); + + // Only admin can set yield vault + vm.expectRevert(); + vbTokenPart2.setYieldVault(newVault); + + vm.startPrank(owner); + + // Cannot set yield vault to zero address + vm.expectRevert(VaultBridgeToken.InvalidYieldVault.selector); + vbTokenPart2.setYieldVault(address(0)); + + vm.stopPrank(); + } + + function test_setYieldVault() public { + // Create a new test vault + MockVault newVault = new MockVault(underlyingToken); + newVault.setMaxDeposit(MAX_DEPOSIT); + newVault.setMaxWithdraw(MAX_WITHDRAW); + + // Get the current yield vault for comparison + address oldVault = address(vbToken.yieldVault()); + assertEq(oldVault, address(yieldVault)); + + // Expect the event to be emitted + vm.expectEmit(); + emit VaultBridgeToken.YieldVaultSet(address(newVault)); + + // Set the new yield vault as owner + vm.prank(owner); + vbTokenPart2.setYieldVault(address(newVault)); + + assertEq(address(vbToken.yieldVault()), address(newVault)); + assertNotEq(address(vbToken.yieldVault()), oldVault); + assertEq(IERC20(underlyingToken).allowance(address(vbToken), address(newVault)), type(uint256).max); + assertEq(IERC20(underlyingToken).allowance(address(vbToken), oldVault), 0); + } + + function test_setMinimumYieldVaultDeposit_revert() public { + uint256 newDeposit = 5e12; + + // Only admin can set minimum yield vault deposit + vm.expectRevert(); + vbTokenPart2.setMinimumYieldVaultDeposit(newDeposit); + } + + function test_setMinimumYieldVaultDeposit() public { + uint256 newDeposit = 5e12; + + // Get the current minimum yield vault deposit for comparison + uint256 oldDeposit = vbToken.minimumYieldVaultDeposit(); + assertEq(oldDeposit, MINIMUM_YIELD_VAULT_DEPOSIT); + + vm.prank(owner); + vbTokenPart2.setMinimumYieldVaultDeposit(newDeposit); + + assertEq(vbToken.minimumYieldVaultDeposit(), newDeposit); + assertNotEq(vbToken.minimumYieldVaultDeposit(), oldDeposit); + + // Test setting to 0 (disabled) + vm.prank(owner); + vbTokenPart2.setMinimumYieldVaultDeposit(0); + assertEq(vbToken.minimumYieldVaultDeposit(), 0); + } + + function test_setYieldVaultMaximumSlippagePercentage_revert() public { + // Test non-admin caller reverts + vm.expectRevert(); + vbTokenPart2.setYieldVaultMaximumSlippagePercentage(5e17); + + // Test invalid percentage (> 1e18) reverts + vm.startPrank(owner); + vm.expectRevert(VaultBridgeToken.InvalidYieldVaultMaximumSlippagePercentage.selector); + vbTokenPart2.setYieldVaultMaximumSlippagePercentage(1e18 + 1); + vm.stopPrank(); + } + + function test_setYieldVaultMaximumSlippagePercentage() public { + vm.startPrank(owner); + + // Test setting a valid percentage + uint256 newSlippagePercentage = 5e17; + + vm.expectEmit(); + emit VaultBridgeToken.YieldVaultMaximumSlippagePercentageSet(newSlippagePercentage); + vbTokenPart2.setYieldVaultMaximumSlippagePercentage(newSlippagePercentage); + + assertEq(vbToken.yieldVaultMaximumSlippagePercentage(), newSlippagePercentage); + + // Test setting to zero (disable slippage protection) + vm.expectEmit(); + emit VaultBridgeToken.YieldVaultMaximumSlippagePercentageSet(0); + vbTokenPart2.setYieldVaultMaximumSlippagePercentage(0); + + assertEq(vbToken.yieldVaultMaximumSlippagePercentage(), 0); + + // Test setting to maximum allowed (100%) + vm.expectEmit(); + emit VaultBridgeToken.YieldVaultMaximumSlippagePercentageSet(1e18); + vbTokenPart2.setYieldVaultMaximumSlippagePercentage(1e18); + + assertEq(vbToken.yieldVaultMaximumSlippagePercentage(), 1e18); + + vm.stopPrank(); + } + + function test_fallback_revert() public { + // Test that fallback function reverts with UnknownFunction error + bytes4 invalidSelector = bytes4(keccak256("nonExistentFunction()")); + + vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.UnknownFunction.selector, invalidSelector)); + (address(vbTokenPart2).call(abi.encodePacked(invalidSelector))); + + // Test the fallback by directly calling the implementation contract + vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.UnknownFunction.selector, invalidSelector)); + (address(vbTokenPart2Implementation).call(abi.encodePacked(invalidSelector))); + } + function test_pause_unpause() public { vm.expectRevert(); vbTokenPart2.pause(); @@ -1174,10 +1270,6 @@ contract GenericVaultBridgeTokenTest is Test { vm.stopPrank(); } - function test_version() public view { - assertEq(vbToken.version(), version); - } - function test_approve() public { assertTrue(vbToken.approve(address(0xBEEF), 1e18)); @@ -1274,30 +1366,4 @@ contract GenericVaultBridgeTokenTest is Test { assertEq(vbToken.allowance(sender, address(0xCAFE)), 1e18); assertEq(vbToken.nonces(sender), 1); } - - function _calculateReserveAssets(uint256 amount, uint256 vaultMaxDeposit) internal view returns (uint256) { - uint256 reserveAssets = (amount * minimumReservePercentage) / MAX_RESERVE_PERCENTAGE; - uint256 assetsToDeposit = amount - reserveAssets; - uint256 assetsToDepositMax = (assetsToDeposit > vaultMaxDeposit) ? vaultMaxDeposit : assetsToDeposit; - return amount - assetsToDepositMax; - } - - function _calculateWithdrawableAmount(uint256 amount) internal view returns (uint256) { - return amount - vbToken.reservedAssets() > MAX_WITHDRAW ? MAX_WITHDRAW : amount - vbToken.reservedAssets(); - } - - function _testPauseUnpause(address caller, address callee, bytes memory callData) internal { - vm.startPrank(caller); - (bool success, /* bytes memory data */ ) = callee.call(abi.encodeCall(vbTokenPart2.pause, ())); - - vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); - (success, /* bytes memory data */ ) = callee.call(callData); - - (success, /* bytes memory data */ ) = callee.call(abi.encodeCall(vbTokenPart2.unpause, ())); - vm.stopPrank(); - } - - function _proxify(address logic, address admin, bytes memory initData) internal returns (address proxy) { - proxy = address(new TransparentUpgradeableProxy(logic, admin, initData)); - } } diff --git a/test/unit/primary-chain/ethereum/vbETH/VbETHTest.t.sol b/test/unit/primary-chain/ethereum/vbETH/VbETHTest.t.sol new file mode 100644 index 00000000..42d73b6d --- /dev/null +++ b/test/unit/primary-chain/ethereum/vbETH/VbETHTest.t.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity 0.8.29; + +import {VbETHTestBase} from "test/base/primary-chain/VbETHTestBase.sol"; +import {VaultBridgeToken, PausableUpgradeable} from "src/primary-chain/VaultBridgeToken.sol"; +import {IAgglayerBridge} from "src/etc/IAgglayerBridge.sol"; +import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; +import {IWETH9} from "src/etc/IWETH9.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; + +/** + * @title VbEth Unit Tests + * @notice Comprehensive unit tests for VbEth contract + */ +contract VbETHTest is VbETHTestBase { + using SafeERC20 for IERC20; + + function setUp() public virtual { + deployVbETHInfrastructure(); + } + + function test_depositGasToken(address receiver, uint256 depositAmount) public { + vm.assume(receiver != address(0)); + vm.assume(receiver != address(vbETH)); + vm.assume(depositAmount > 0 && depositAmount < 100 ether); + + // Get initial balance + uint256 initialReceiverBalance = vbETH.balanceOf(receiver); + + // Deposit ETH + vm.deal(address(this), depositAmount); + uint256 shares = vbETH.depositGasToken{value: depositAmount}(receiver); + + // Verify + assertGt(shares, 0, "Should receive shares for deposit"); + assertEq(vbETH.balanceOf(receiver), initialReceiverBalance + shares, "Receiver should get correct shares"); + } + + function test_depositGasTokenAndBridge(address receiver, uint256 depositAmount) public { + vm.assume(receiver != address(0)); + vm.assume(receiver != address(vbETH)); + vm.assume(depositAmount > 0 && depositAmount < 100 ether); + + // Deposit ETH + vm.deal(address(this), depositAmount); + uint256 shares = vbETH.depositGasTokenAndBridge{value: depositAmount}(receiver, NETWORK_ID_L2, true); + + assertGt(shares, 0, "Should receive shares for deposit"); + } + + function test_depositWETH(address receiver, uint256 depositAmount) public { + vm.assume(receiver != address(0)); + vm.assume(receiver != address(vbETH)); + vm.assume(depositAmount > 0 && depositAmount < 100 ether); + + // Deal WETH directly + _dealWETH(address(this), depositAmount); + + // Approve and deposit WETH + IERC20(underlyingToken).forceApprove(address(vbETH), depositAmount); + uint256 shares = vbETH.deposit(depositAmount, receiver); + + assertEq(vbETH.balanceOf(receiver), shares, "Receiver should get correct shares"); + } + + function test_mint() public { + uint256 amount = 1 ether; + + // Deal ETH to test contract (with extra for refund test) + vm.deal(address(this), amount + 1 ether); + + uint256 initialBalance = IWETH9(underlyingToken).balanceOf(address(this)); + + // Mint with extra ETH to test refund functionality + vbETH.mintWithGasToken{value: amount + 1 ether}(amount, address(this)); + + // Check refund + assertEq(IWETH9(underlyingToken).balanceOf(address(this)), initialBalance + 1 ether); + + assertEq(vbETH.balanceOf(address(this)), amount); // shares minted to the sender + assertApproxEqAbs(vbETH.totalAssets(), amount, 2); // allow for rounding + + uint256 reserveAmount = calculateReserveAssets(amount, yieldVault.maxDeposit(address(vbETH))); + assertApproxEqAbs(vbETH.reservedAssets(), reserveAmount, 2); // allow for rounding + } + + function test_withdraw_from_reserve() public { + uint256 amount = 1 ether; + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbETH)); + + // Deal ETH and deposit + vm.deal(address(this), amount); + uint256 shares = vbETH.depositGasToken{value: amount}(address(this)); + assertEq(vbETH.balanceOf(address(this)), shares); // sender gets shares + + uint256 reserveAssetsAfterDeposit = calculateReserveAssets(amount, vaultMaxDeposit); + + uint256 reserveWithdrawAmount = (reserveAssetsAfterDeposit * 90) / 100; // withdraw 90% of reserve assets + uint256 reserveAfterWithdraw = reserveAssetsAfterDeposit - reserveWithdrawAmount; + + uint256 initialBalance = IWETH9(underlyingToken).balanceOf(address(this)); + + vm.expectEmit(); + emit IERC4626.Withdraw( + address(this), address(this), address(this), reserveWithdrawAmount, reserveWithdrawAmount + ); + vbETH.withdraw(reserveWithdrawAmount, address(this), address(this)); + assertEq(IWETH9(underlyingToken).balanceOf(address(vbETH)), reserveAfterWithdraw); // reserve assets reduced + assertEq(IWETH9(underlyingToken).balanceOf(address(this)), initialBalance + reserveWithdrawAmount); // assets returned to sender + assertEq(vbETH.balanceOf(address(this)), amount - reserveWithdrawAmount); // shares reduced + } + + function test_withdraw_from_stake() public { + uint256 amount = 1 ether; + + // Deal ETH and deposit + vm.deal(address(this), amount); + uint256 shares = vbETH.depositGasToken{value: amount}(address(this)); + assertEq(vbETH.balanceOf(address(this)), shares); // sender gets shares + + uint256 amountToWithdraw = amount - 1; + uint256 initialBalance = IWETH9(underlyingToken).balanceOf(address(this)); + + vm.expectEmit(); + emit IERC4626.Withdraw(address(this), address(this), address(this), amountToWithdraw, amountToWithdraw); + vbETH.withdraw(amountToWithdraw, address(this), address(this)); + assertEq(IWETH9(underlyingToken).balanceOf(address(vbETH)), 1); // reserve assets reduced (minimum reserve is 1 due to rounding) + assertEq(IWETH9(underlyingToken).balanceOf(address(this)), initialBalance + amountToWithdraw); // assets returned to sender + assertEq(vbETH.balanceOf(address(this)), amount - amountToWithdraw); // shares reduced + } + + function test_completeMigration_CUSTOM_no_discrepancy() public { + uint256 assets = 100 ether; + uint256 shares = 100 ether; + + // Make sure the assets is less than the max deposit limit + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbETH)); + if (assets > vaultMaxDeposit) { + assets = vaultMaxDeposit / 2; + shares = vaultMaxDeposit / 2; + } + + bytes memory callData = abi.encodeCall(vbETHPart2.completeMigration, (NETWORK_ID_L2, shares, assets)); + _testPauseUnpause(owner, address(vbETH), callData); + + // For VbEth, deal WETH tokens instead of ETH + _dealWETH(address(vbETH), assets); + + vm.expectRevert(VaultBridgeToken.Unauthorized.selector); + vbETHPart2.completeMigration(NETWORK_ID_L2, shares, assets); + + vm.startPrank(migrationManagerAddr); + + vm.expectRevert(VaultBridgeToken.InvalidOriginNetwork.selector); + vbETHPart2.completeMigration(NETWORK_ID_L1, 0, assets); + + vm.expectRevert(VaultBridgeToken.InvalidShares.selector); + vbETHPart2.completeMigration(NETWORK_ID_L2, 0, assets); + + uint256 stakedAssetsBefore = vbETH.stakedAssets(); + + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_ASSET, + NETWORK_ID_L1, + address(vbETH), + NETWORK_ID_L2, + address(0), + shares, + tokenMetadata, + MockAgglayerBridge(agglayerBridge).depositCount() + ); + vm.expectEmit(); + emit IERC4626.Deposit(migrationManagerAddr, address(vbETH), assets, shares); + vm.expectEmit(); + emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, assets, 0); + vbETHPart2.completeMigration(NETWORK_ID_L2, shares, assets); + + vm.stopPrank(); + + assertEq( + vbETH.reservedAssets(), vbETH.convertToAssets(shares) * minimumReservePercentage / MAX_RESERVE_PERCENTAGE + ); + assertGt(vbETH.stakedAssets(), stakedAssetsBefore); + } + + function test_completeMigration_CUSTOM_with_discrepancy() public { + uint256 assets = 100 ether; + uint256 shares = 110 ether; + + // Make sure the assets is less than the max deposit limit + uint256 vaultMaxDeposit = yieldVault.maxDeposit(address(vbETH)); + if (assets > vaultMaxDeposit) { + assets = vaultMaxDeposit / 2; + shares = (vaultMaxDeposit / 2) + 10; + } + + // For VbEth, deal WETH tokens instead of ETH + _dealWETH(address(vbETH), assets); + + vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.CannotCompleteMigration.selector, shares, assets, 0)); + vm.prank(migrationManagerAddr); + vbETHPart2.completeMigration(NETWORK_ID_L2, shares, assets); + + // Fund the migration fees + _dealWETH(address(this), assets); + IERC20(underlyingToken).forceApprove(address(vbETH), assets); + vm.expectEmit(); + emit VaultBridgeToken.DonatedForCompletingMigration(address(this), assets); + vbETHPart2.donateForCompletingMigration(assets); + + uint256 stakedAssetsBefore = vbETH.stakedAssets(); + + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_ASSET, + NETWORK_ID_L1, + address(vbETH), + NETWORK_ID_L2, + address(0), + shares, + tokenMetadata, + MockAgglayerBridge(agglayerBridge).depositCount() + ); + vm.expectEmit(); + emit IERC4626.Deposit(migrationManagerAddr, address(vbETH), assets, shares); + vm.expectEmit(); + emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, assets, shares - assets); + vm.prank(migrationManagerAddr); + vbETHPart2.completeMigration(NETWORK_ID_L2, shares, assets); + + assertEq( + vbETH.reservedAssets(), vbETH.convertToAssets(shares) * minimumReservePercentage / MAX_RESERVE_PERCENTAGE + ); + assertGt(vbETH.stakedAssets(), stakedAssetsBefore); + } +} diff --git a/test/unit/secondary-chain/CustomTokenTest.t.sol b/test/unit/secondary-chain/CustomTokenTest.t.sol new file mode 100644 index 00000000..556d9a04 --- /dev/null +++ b/test/unit/secondary-chain/CustomTokenTest.t.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + CustomTokenTestBase, + TestHarnessCustomToken, + ITransparentUpgradeableProxy +} from "test/base/secondary-chain/CustomTokenTestBase.sol"; + +// Core contracts +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; + +// OpenZeppelin +import {IAccessControl} from "@openzeppelin-contracts/access/IAccessControl.sol"; + +/// @dev CustomToken tests +contract CustomTokenTest is CustomTokenTestBase { + function setUp() public virtual { + deployCustomTokenInfrastructure(); + } + + function _testInitializationRevert( + bytes4 expectedError, + address owner_, + uint8 decimals_, + address bridge_, + address nativeConverter_ + ) internal { + vm.revertToState(stateBeforeInitialize); + + vm.expectRevert(expectedError); + bytes memory customTokenInitData = + abi.encodeCall(TestHarnessCustomToken.reinitialize2, (owner_, decimals_, bridge_, nativeConverter_)); + bytes memory customTokenUpgradeData = + abi.encodeCall(ITransparentUpgradeableProxy.upgradeToAndCall, (customTokenImpl, customTokenInitData)); + vm.prank(_getProxyAdmin(address(existingCustomTokenProxy))); + (address(existingCustomTokenProxy).call(customTokenUpgradeData)); + } + + function test_initialize_revertsWithInvalidOwner() public { + _testInitializationRevert( + CustomToken.InvalidOwner.selector, + address(0), // Invalid owner + customTokenDecimals, + address(mockAgglayerBridge), + dummyNativeConverter + ); + } + + function test_initialize_revertsWithInvalidDecimals() public { + _testInitializationRevert( + CustomToken.InvalidOriginalUnderlyingTokenDecimals.selector, + owner, + 0, // Invalid decimals + address(mockAgglayerBridge), + dummyNativeConverter + ); + } + + function test_initialize_revertsWithInvalidBridge() public { + _testInitializationRevert( + CustomToken.InvalidBridge.selector, + owner, + customTokenDecimals, + address(0), // Invalid bridge + dummyNativeConverter + ); + } + + function test_pauseUnpause_transfer() public { + deal(address(customTokenHarness), sender, 1000e18); + + bytes memory transferCallData = abi.encodeCall(customTokenHarness.transfer, (recipient, 100e18)); + _testPauseUnpause(proxyAdmin, address(customTokenHarness), transferCallData); + } + + function test_pauseUnpause_transferFrom() public { + deal(address(customTokenHarness), sender, 1000e18); + vm.prank(sender); + customTokenHarness.approve(proxyAdmin, 100e18); + + bytes memory transferFromCallData = abi.encodeCall(customTokenHarness.transferFrom, (sender, recipient, 100e18)); + _testPauseUnpause(proxyAdmin, address(customTokenHarness), transferFromCallData); + } + + function test_pauseUnpause_approve() public { + bytes memory approveCallData = abi.encodeCall(customTokenHarness.approve, (recipient, 100e18)); + + _testPauseUnpause(proxyAdmin, address(customTokenHarness), approveCallData); + } + + function test_pauseUnpause_permit() public { + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + uint256 nonce = customTokenHarness.nonces(sender); + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, sender, recipient, value, nonce, deadline)); + + bytes32 domainHash = customTokenHarness.DOMAIN_SEPARATOR(); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainHash, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(senderPrivateKey, digest); + + bytes memory permitCallData = + abi.encodeCall(customTokenHarness.permit, (sender, recipient, value, deadline, v, r, s)); + + _testPauseUnpause(proxyAdmin, address(customTokenHarness), permitCallData); + } + + function test_pause_onlyPauserRole() public { + vm.prank(sender); + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(this), + customTokenHarness.PAUSER_ROLE() + ) + ); + customTokenHarness.pause(); + } + + function test_unpause_onlyAdminRole() public { + vm.prank(owner); + customTokenHarness.pause(); + + // The default caller when no prank is set should not have admin role + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(this), // The test contract itself is the default caller + customTokenHarness.DEFAULT_ADMIN_ROLE() + ) + ); + customTokenHarness.unpause(); + } + + function test_pause_success() public { + assertFalse(customTokenHarness.paused()); + + vm.prank(owner); + customTokenHarness.pause(); + + assertTrue(customTokenHarness.paused()); + } + + function test_unpause_success() public { + vm.prank(owner); + customTokenHarness.pause(); + assertTrue(customTokenHarness.paused()); + + vm.prank(owner); + customTokenHarness.unpause(); + assertFalse(customTokenHarness.paused()); + } + + function test_erc20_transfer_success() public { + uint256 amount = 100e18; + deal(address(customTokenHarness), sender, amount); + + vm.prank(sender); + bool success = customTokenHarness.transfer(recipient, amount); + + assertTrue(success); + assertEq(customTokenHarness.balanceOf(sender), 0); + assertEq(customTokenHarness.balanceOf(recipient), amount); + } + + function test_erc20_transferFrom_success() public { + uint256 amount = 100e18; + deal(address(customTokenHarness), sender, amount); + + vm.prank(sender); + customTokenHarness.approve(proxyAdmin, amount); + + vm.prank(proxyAdmin); + bool success = customTokenHarness.transferFrom(sender, recipient, amount); + + assertTrue(success); + assertEq(customTokenHarness.balanceOf(sender), 0); + assertEq(customTokenHarness.balanceOf(recipient), amount); + } + + function test_erc20_approve_success() public { + uint256 amount = 100e18; + + vm.prank(sender); + bool success = customTokenHarness.approve(recipient, amount); + + assertTrue(success); + assertEq(customTokenHarness.allowance(sender, recipient), amount); + } + + function test_erc20_permit_success() public { + uint256 value = 100e18; + uint256 deadline = block.timestamp + 1 hours; + uint256 nonce = customTokenHarness.nonces(sender); + + bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, sender, recipient, value, nonce, deadline)); + + bytes32 domainHash = customTokenHarness.DOMAIN_SEPARATOR(); + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainHash, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(senderPrivateKey, digest); + + customTokenHarness.permit(sender, recipient, value, deadline, v, r, s); + + assertEq(customTokenHarness.allowance(sender, recipient), value); + assertEq(customTokenHarness.nonces(sender), nonce + 1); + } +} diff --git a/test/unit/secondary-chain/NativeConverterTest.t.sol b/test/unit/secondary-chain/NativeConverterTest.t.sol new file mode 100644 index 00000000..7c271688 --- /dev/null +++ b/test/unit/secondary-chain/NativeConverterTest.t.sol @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + NativeConverterTestBase, + NativeConverter, + TestHarnessNativeConverter +} from "test/base/secondary-chain/NativeConverterTestBase.sol"; +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; + +// OpenZeppelin +import {IAccessControl} from "@openzeppelin-contracts/access/IAccessControl.sol"; +import {IBridgeL2SovereignChain} from "test/interfaces/IBridgeL2SovereignChain.sol"; +import {IERC20Errors} from "@openzeppelin/contracts/interfaces/draft-IERC6093.sol"; +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; + +// Mocks +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; + +/// @dev NativeConverter tests +contract NativeConverterTest is NativeConverterTestBase { + function setUp() public virtual { + deployNativeConverterInfrastructure(); + } + + function test_initialize() public virtual { + vm.revertToState(stateBeforeInitialize); + + bytes memory initData; + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + address(0), + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidOwner.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(0), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidCustomToken.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(0), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidUnderlyingToken.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(0), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidAgglayerBridge.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + + vm.revertToState(stateBeforeInitialize); + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + 1e19, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidNonMigratableBackingPercentage.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + + initData = abi.encodeCall( + nativeConverter.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + address(0) + ) + ); + vm.expectRevert(NativeConverter.InvalidMigrationManager.selector); + TestHarnessNativeConverter(_proxify(nativeConverterImpl, address(this), initData)); + } + + function test_convert() public { + uint256 amount = 100; + + vm.startPrank(owner); + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.convert(amount, recipient); + nativeConverter.unpause(); + vm.stopPrank(); + + vm.startPrank(sender); + vm.expectRevert(NativeConverter.InvalidAssets.selector); + nativeConverter.convert(0, recipient); + + vm.expectRevert(NativeConverter.InvalidReceiver.selector); + nativeConverter.convert(amount, address(0)); + + underlyingToken.approve(address(nativeConverter), amount); + + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, sender, 0, amount)); + nativeConverter.convert(amount, recipient); + + deal(address(underlyingToken), sender, amount); + + underlyingToken.approve(address(nativeConverter), amount); + nativeConverter.convert(amount, recipient); + + assertEq(underlyingToken.balanceOf(sender), 0); + assertEq(underlyingToken.balanceOf(address(nativeConverter)), amount); + assertEq(customToken.balanceOf(recipient), amount); + assertEq(nativeConverter.backingOnSecondaryChain(), amount); + vm.stopPrank(); + } + + function test_convertWithPermit() public { + uint256 amount = 100; + + vm.startPrank(owner); + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.convertWithPermit(amount, recipient, ""); + nativeConverter.unpause(); + vm.stopPrank(); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + senderPrivateKey, + keccak256( + abi.encodePacked( + "\x19\x01", + underlyingToken.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + PERMIT_TYPEHASH, + sender, + address(nativeConverter), + amount, + vm.getNonce(sender), + block.timestamp + ) + ) + ) + ) + ); + bytes memory permitData = + abi.encodeWithSelector(PERMIT_SIGNATURE, sender, address(nativeConverter), amount, block.timestamp, v, r, s); + + vm.startPrank(sender); + + vm.expectRevert(NativeConverter.InvalidPermitData.selector); + nativeConverter.convertWithPermit(amount, recipient, ""); + + vm.expectRevert(NativeConverter.InvalidAssets.selector); + nativeConverter.convertWithPermit(0, recipient, permitData); + + vm.expectRevert(NativeConverter.InvalidReceiver.selector); + nativeConverter.convertWithPermit(amount, address(0), permitData); + + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, sender, 0, amount)); + nativeConverter.convertWithPermit(amount, recipient, permitData); + + deal(address(underlyingToken), sender, amount); + nativeConverter.convertWithPermit(amount, recipient, permitData); + + assertEq(underlyingToken.balanceOf(sender), 0); + assertEq(underlyingToken.balanceOf(address(nativeConverter)), amount); + assertEq(customToken.balanceOf(recipient), amount); + assertEq(nativeConverter.backingOnSecondaryChain(), amount); + vm.stopPrank(); + } + + function test_maxDeconvert() public { + uint256 amount = 100; + + vm.startPrank(owner); + nativeConverter.pause(); + vm.assertEq(nativeConverter.maxDeconvert(sender), 0); + nativeConverter.unpause(); + + vm.assertEq(nativeConverter.maxDeconvert(sender), 0); // owner has 0 shares + + deal(address(customToken), sender, amount); // mint shares + + uint256 backingOnSecondaryChain = 0; + assertEq(nativeConverter.maxDeconvert(sender), backingOnSecondaryChain); + + // create backing on Secondary Chain + deal(address(underlyingToken), owner, amount); + + underlyingToken.approve(address(nativeConverter), amount); + backingOnSecondaryChain += nativeConverter.convert(amount, recipient); + vm.stopPrank(); + + deal(address(customToken), sender, amount); // mint shares + assertEq(nativeConverter.maxDeconvert(sender), backingOnSecondaryChain); + + deal(address(customToken), sender, amount); // mint additional shares + assertLe(nativeConverter.maxDeconvert(sender), backingOnSecondaryChain); // sender has more shares than the backing on Secondary Chain + } + + function test_deconvert() public { + uint256 amount = 100; + + vm.startPrank(owner); + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.deconvert(amount, recipient); + nativeConverter.unpause(); + vm.stopPrank(); + + vm.startPrank(sender); + vm.expectRevert(NativeConverter.InvalidShares.selector); + nativeConverter.deconvert(0, recipient); + + vm.expectRevert(NativeConverter.InvalidReceiver.selector); + nativeConverter.deconvert(amount, address(0)); + + vm.expectRevert(abi.encodeWithSelector(NativeConverter.AssetsTooLarge.selector, 0, amount)); + nativeConverter.deconvert(amount, recipient); // no backing on Secondary Chain + + // create backing on Secondary Chain + uint256 backingOnSecondaryChain = 0; + deal(address(underlyingToken), owner, amount); + vm.startPrank(owner); + underlyingToken.approve(address(nativeConverter), amount); + backingOnSecondaryChain = nativeConverter.convert(amount, recipient); + vm.stopPrank(); + + vm.startPrank(sender); + vm.expectRevert(abi.encodeWithSelector(IERC20Errors.ERC20InsufficientBalance.selector, sender, 0, amount)); + nativeConverter.deconvert(amount, recipient); // sender has 0 shares + + deal(address(customToken), sender, amount); // mint shares + + uint256 returnedAssets = nativeConverter.deconvert(amount, recipient); + vm.stopPrank(); + + assertEq(returnedAssets, backingOnSecondaryChain); + assertEq(underlyingToken.balanceOf(recipient), amount); + assertEq(underlyingToken.balanceOf(address(nativeConverter)), 0); + assertEq(customToken.balanceOf(sender), 0); + assertEq(nativeConverter.backingOnSecondaryChain(), 0); + } + + function test_deconvertAndBridge() public { + uint256 amount = 100; + + vm.startPrank(owner); + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L1, true); + nativeConverter.unpause(); + vm.stopPrank(); + + vm.prank(sender); + vm.expectRevert(NativeConverter.InvalidDestinationNetworkId.selector); + nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L2, true); + + // create backing on Secondary Chain + uint256 backingOnSecondaryChain = 0; + underlyingToken.mint(owner, amount); + vm.startPrank(owner); + underlyingToken.approve(address(nativeConverter), amount); + backingOnSecondaryChain = nativeConverter.convert(amount, recipient); + vm.stopPrank(); + + deal(address(customToken), sender, amount); // mint shares + + vm.prank(sender); + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_ASSET, + NETWORK_ID_L2, + address(underlyingToken), + NETWORK_ID_L1, + recipient, + amount, + underlyingTokenMetadata, + 0 + ); + uint256 returnedAssets = nativeConverter.deconvertAndBridge(amount, recipient, NETWORK_ID_L1, true); + + assertEq(returnedAssets, backingOnSecondaryChain); + assertEq(underlyingToken.balanceOf(address(nativeConverter)), 0); + assertEq(customToken.balanceOf(sender), 0); + assertEq(nativeConverter.backingOnSecondaryChain(), 0); + } + + function test_migrateBackingToPrimaryChain() public { + uint256 amount = 100; + uint256 amountToMigrate = 90; + + // Try to migrate as the owner with a specific amount + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), nativeConverter.MIGRATOR_ROLE() + ) + ); // only owner can call this function + nativeConverter.migrateBackingToPrimaryChain(amount); + + underlyingToken.mint(owner, amount); + + vm.startPrank(owner); + + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.migrateBackingToPrimaryChain(amount); + nativeConverter.unpause(); + + vm.expectRevert(NativeConverter.InvalidAssets.selector); + nativeConverter.migrateBackingToPrimaryChain(0); // try with 0 backing + + uint256 currentBacking = nativeConverter.backingOnSecondaryChain(); + + vm.expectRevert( + abi.encodeWithSelector(NativeConverter.AssetsTooLarge.selector, currentBacking, currentBacking + 1) + ); + nativeConverter.migrateBackingToPrimaryChain(currentBacking + 1); + + // create backing on Secondary Chain + uint256 backingOnSecondaryChain = 0; + underlyingToken.approve(address(nativeConverter), amount); + backingOnSecondaryChain = nativeConverter.convert(amount, recipient); + + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_ASSET, + NETWORK_ID_L2, + address(underlyingToken), + NETWORK_ID_L1, + migrationManager, + amountToMigrate, + underlyingTokenMetadata, + 0 + ); + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_MESSAGE, + NETWORK_ID_L2, + address(nativeConverter), + NETWORK_ID_L1, + migrationManager, + 0, + abi.encode( + MigrationManager.CrossChainInstruction._0_COMPLETE_MIGRATION, + abi.encode(amountToMigrate, amountToMigrate) + ), + 1 + ); + vm.expectEmit(); + emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); + nativeConverter.migrateBackingToPrimaryChain(amountToMigrate); + assertEq(underlyingToken.balanceOf(address(nativeConverter)), backingOnSecondaryChain - amountToMigrate); + + vm.stopPrank(); + } +} diff --git a/test/unit/secondary-chain/agglayer/GenericCustomTokenAgglayer.t.sol b/test/unit/secondary-chain/agglayer/GenericCustomTokenAgglayer.t.sol new file mode 100644 index 00000000..da2f9210 --- /dev/null +++ b/test/unit/secondary-chain/agglayer/GenericCustomTokenAgglayer.t.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + GenericCustomTokenAgglayerTestBase, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy, + MockERC20Upgradeable +} from "test/base/secondary-chain/GenericCustomTokenAgglayerTestBase.sol"; + +// Core contracts +import {GenericCustomTokenAgglayer} from "src/secondary-chain/agglayer/GenericCustomTokenAgglayer.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @dev GenericCustomTokenAgglayer tests +contract GenericCustomTokenAgglayerTest is GenericCustomTokenAgglayerTestBase { + function setUp() public virtual { + deployGenericCustomTokenAgglayerInfrastructure(); + } + + function test_mint_success_fromBridge() public { + uint256 amount = 1000e18; + + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(recipient, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(recipient), amount); + assertEq(genericCustomTokenAgglayer.totalSupply(), amount); + } + + function test_mint_success_fromNativeConverter() public { + uint256 amount = 1000e18; + + vm.prank(dummyNativeConverter); + genericCustomTokenAgglayer.mint(recipient, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(recipient), amount); + assertEq(genericCustomTokenAgglayer.totalSupply(), amount); + } + + // @todo Update. + /* + function test_mint_success_toAddressZero_emitsTransfer() public { + uint256 amount = 1000e18; + + vm.expectEmit(true, true, true, true); + emit IERC20.Transfer(address(0), address(0), amount); + + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(address(0), amount); + + assertEq(genericCustomTokenAgglayer.totalSupply(), 0); + } + */ + + function test_mint_revertsWithUnauthorized() public { + uint256 amount = 1000e18; + + vm.expectRevert(CustomToken.Unauthorized.selector); + vm.prank(makeAddr("unauthorized")); + genericCustomTokenAgglayer.mint(recipient, amount); + } + + function test_mint_revertsWhenPaused() public { + uint256 amount = 1000e18; + + vm.prank(owner); + genericCustomTokenAgglayer.pause(); + + vm.expectRevert(); + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(recipient, amount); + } + + function test_burn_success_fromBridge() public { + uint256 amount = 1000e18; + + // First mint tokens + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(sender, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(sender), amount); + + // Then burn them + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.burn(sender, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(sender), 0); + assertEq(genericCustomTokenAgglayer.totalSupply(), 0); + } + + function test_burn_success_fromNativeConverter() public { + uint256 amount = 1000e18; + + // First mint tokens + vm.prank(dummyNativeConverter); + genericCustomTokenAgglayer.mint(sender, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(sender), amount); + + // Then burn them + vm.prank(dummyNativeConverter); + genericCustomTokenAgglayer.burn(sender, amount); + + assertEq(genericCustomTokenAgglayer.balanceOf(sender), 0); + assertEq(genericCustomTokenAgglayer.totalSupply(), 0); + } + + function test_burn_revertsWithUnauthorized() public { + uint256 amount = 1000e18; + + // First mint tokens + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(sender, amount); + + vm.expectRevert(CustomToken.Unauthorized.selector); + vm.prank(makeAddr("unauthorized")); + genericCustomTokenAgglayer.burn(sender, amount); + } + + function test_burn_revertsWhenPaused() public { + uint256 amount = 1000e18; + + // First mint tokens + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(sender, amount); + + vm.prank(owner); + genericCustomTokenAgglayer.pause(); + + vm.expectRevert(); + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.burn(sender, amount); + } + + function test_burn_revertsWithInsufficientBalance() public { + uint256 mintAmount = 500e18; + uint256 burnAmount = 1000e18; + + // Mint less than we try to burn + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(sender, mintAmount); + + vm.expectRevert(); + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.burn(sender, burnAmount); + } + + function test_mintAndBurn_multipleUsers() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + uint256 amount1 = 1000e18; + uint256 amount2 = 2000e18; + + // Mint to both users + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.mint(user1, amount1); + + vm.prank(dummyNativeConverter); + genericCustomTokenAgglayer.mint(user2, amount2); + + assertEq(genericCustomTokenAgglayer.balanceOf(user1), amount1); + assertEq(genericCustomTokenAgglayer.balanceOf(user2), amount2); + assertEq(genericCustomTokenAgglayer.totalSupply(), amount1 + amount2); + + // Burn from user1 + vm.prank(address(mockAgglayerBridge)); + genericCustomTokenAgglayer.burn(user1, amount1); + + assertEq(genericCustomTokenAgglayer.balanceOf(user1), 0); + assertEq(genericCustomTokenAgglayer.balanceOf(user2), amount2); + assertEq(genericCustomTokenAgglayer.totalSupply(), amount2); + } +} diff --git a/test/unit/secondary-chain/agglayer/vbETH/WethAgglayerTest.t.sol b/test/unit/secondary-chain/agglayer/vbETH/WethAgglayerTest.t.sol new file mode 100644 index 00000000..975e9fad --- /dev/null +++ b/test/unit/secondary-chain/agglayer/vbETH/WethAgglayerTest.t.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + WethAgglayerTestBase, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from "test/base/secondary-chain/WethAgglayerTestBase.sol"; + +// Core contracts +import {CustomTokenWethExtension} from "src/secondary-chain/CustomTokenWethExtension.sol"; + +contract WethAgglayerTest is WethAgglayerTestBase { + function setUp() public { + deployWethAgglayerInfrastructure(); + } + + function test_receive(uint256 amount) public { + assertEq(wethAgglayer.balanceOf(address(this)), 0); + deal(address(this), amount); + + vm.expectRevert(); + (address(wethAgglayer).call{value: amount}("")); + } + + function test_deposit(uint256 amount) public { + assertEq(wethAgglayer.balanceOf(address(this)), 0); + deal(address(this), amount); + + vm.expectEmit(); + emit CustomTokenWethExtension.Deposit(address(this), amount); + wethAgglayer.deposit{value: amount}(); + assertEq(wethAgglayer.balanceOf(address(this)), amount); + } + + function test_withdraw(uint256 amount) public { + // test withdrawal failure on onlyIfGasTokenIsEth + mockAgglayerBridge.setGasTokenAddress(address(this)); + mockAgglayerBridge.setGasTokenNetwork(0); + deployWethAgglayer(false); + vm.expectRevert(CustomTokenWethExtension.FunctionNotSupportedOnThisChain.selector); + wethAgglayer.withdraw(amount); + + mockAgglayerBridge.setGasTokenAddress(address(0)); + mockAgglayerBridge.setGasTokenNetwork(0); + deployWethAgglayer(true); + assertEq(wethAgglayer.balanceOf(address(this)), 0); + deal(address(this), amount); + + wethAgglayer.deposit{value: amount}(); + assertEq(wethAgglayer.balanceOf(address(this)), amount); + assertEq(address(this).balance, 0); + + vm.expectEmit(); + emit CustomTokenWethExtension.Withdrawal(address(this), amount); + wethAgglayer.withdraw(amount); + assertEq(wethAgglayer.balanceOf(address(this)), 0); + assertEq(address(this).balance, amount); + } + + function test_onlyIfGasTokenIsEth() public { + uint256 amount = 1 ether; + deal(address(this), amount); + + mockAgglayerBridge.setGasTokenAddress(address(this)); + mockAgglayerBridge.setGasTokenNetwork(0); + deployWethAgglayer(false); + vm.expectRevert(CustomTokenWethExtension.FunctionNotEnabledOnThisChain.selector); + wethAgglayer.deposit{value: amount}(); + + mockAgglayerBridge.setGasTokenAddress(address(0)); + mockAgglayerBridge.setGasTokenNetwork(1); + deployWethAgglayer(false); + vm.expectRevert(CustomTokenWethExtension.FunctionNotEnabledOnThisChain.selector); + wethAgglayer.deposit{value: amount}(); + + mockAgglayerBridge.setGasTokenAddress(address(0)); + mockAgglayerBridge.setGasTokenNetwork(0); + deployWethAgglayer(true); + vm.expectEmit(); + emit CustomTokenWethExtension.Deposit(address(this), amount); + wethAgglayer.deposit{value: amount}(); + assertEq(wethAgglayer.balanceOf(address(this)), amount); + } + + function test_onlyWethFunctionalityEnabled() public { + uint256 amount = 1 ether; + deal(address(this), amount); + + vm.prank(owner); + CustomTokenWethExtension(address(wethAgglayer)).setWethFunctionalityEnabled(false); + vm.expectRevert(CustomTokenWethExtension.FunctionNotEnabledOnThisChain.selector); + wethAgglayer.deposit{value: amount}(); + + vm.prank(owner); + CustomTokenWethExtension(address(wethAgglayer)).setWethFunctionalityEnabled(true); + vm.expectEmit(); + emit CustomTokenWethExtension.Deposit(address(this), amount); + wethAgglayer.deposit{value: amount}(); + assertEq(wethAgglayer.balanceOf(address(this)), amount); + } + + receive() external payable {} +} diff --git a/test/unit/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayerTest.t.sol b/test/unit/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayerTest.t.sol new file mode 100644 index 00000000..4be5c340 --- /dev/null +++ b/test/unit/secondary-chain/agglayer/vbETH/WethNativeConverterAgglayerTest.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + WethAgglayer, + WethNativeConverterAgglayerTestBase, + WethNativeConverterAgglayer +} from "test/base/secondary-chain/WethNativeConverterAgglayerTestBase.sol"; + +// Core contracts +import {NativeConverter} from "src/secondary-chain/NativeConverter.sol"; +import {MigrationManager} from "src/primary-chain/MigrationManager.sol"; + +// OpenZeppelin +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; + +// Mocks +import {MockAgglayerBridge} from "test/utils/mocks/MockAgglayerBridge.sol"; +import {MockERC20} from "test/utils/mocks/MockERC20.sol"; + +contract WethNativeConverterAgglayerTest is WethNativeConverterAgglayerTestBase { + function setUp() public { + deployWethNativeConverterAgglayerInfrastructure(); + } + + function test_initialize() public { + vm.revertToState(stateBeforeInitialize); + + bytes memory initData; + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + address(0), + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidOwner.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(0), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidCustomToken.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(0), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidUnderlyingToken.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(0), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidAgglayerBridge.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + NETWORK_ID_L2, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidAgglayerBridge.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + MockERC20 dummyToken = new MockERC20("Dummy Token", "DT", 6); + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(dummyToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + 1e19, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(abi.encodeWithSelector(NativeConverter.InvalidNonMigratableBackingPercentage.selector)); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + address(0), + maxNonMigratableGasBackingPercentage + ) + ); + vm.expectRevert(NativeConverter.InvalidMigrationManager.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + + initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + 1e19 + ) + ); + vm.expectRevert(NativeConverter.InvalidNonMigratableBackingPercentage.selector); + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverterImpl), proxyAdmin, initData))); + } + + function test_migrateGasBackingToPrimaryChain() public { + uint256 amount = 100; + uint256 amountToMigrate = 50; + + vm.startPrank(owner); + + nativeConverter.pause(); + vm.expectRevert(PausableUpgradeable.EnforcedPause.selector); + nativeConverter.migrateGasBackingToPrimaryChain(amountToMigrate); + nativeConverter.unpause(); + + vm.expectRevert(NativeConverter.InvalidAssets.selector); + nativeConverter.migrateGasBackingToPrimaryChain(0); // try with 0 backing + + // create backing on Secondary Chain + uint256 backingOnSecondaryChain = 0; + deal(address(underlyingToken), owner, amount); + underlyingToken.approve(address(nativeConverter), amount); + backingOnSecondaryChain = nativeConverter.convert(amount, recipient); + + // Properly deposit ETH into the WETH contract to update _depositedEth + deal(address(owner), amount); + WethAgglayer wethContract = WethAgglayer(payable(address(customToken))); + wethContract.deposit{value: amount}(); + + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_ASSET, NETWORK_ID_L1, address(0x00), NETWORK_ID_L1, migrationManager, amountToMigrate, "", 0 + ); + vm.expectEmit(); + emit MockAgglayerBridge.BridgeEvent( + LEAF_TYPE_MESSAGE, + NETWORK_ID_L2, + address(nativeConverter), + NETWORK_ID_L1, + migrationManager, + 0, + abi.encode( + MigrationManager.CrossChainInstruction._1_WRAP_GAS_TOKEN_AND_COMPLETE_MIGRATION, + abi.encode(amountToMigrate, amountToMigrate) + ), + 1 + ); + vm.expectEmit(); + emit NativeConverter.MigrationStarted(amountToMigrate, amountToMigrate); + nativeConverter.migrateGasBackingToPrimaryChain(amountToMigrate); + assertEq(address(customToken).balance, amountToMigrate); + + uint256 currentBacking = address(customToken).balance; + uint256 nonMigratableGasBacking = + Math.mulDiv(customToken.totalSupply(), maxNonMigratableGasBackingPercentage, 1e18); + + vm.expectRevert( + abi.encodeWithSelector( + NativeConverter.AssetsTooLarge.selector, currentBacking - nonMigratableGasBacking, currentBacking + 1 + ) + ); + nativeConverter.migrateGasBackingToPrimaryChain(currentBacking + 1); + + vm.stopPrank(); + } + + function test_onlyIfGasTokenIsEth() public { + uint256 amount = 100; + deal(address(this), amount); + + mockAgglayerBridge.setGasTokenAddress(address(this)); + mockAgglayerBridge.setGasTokenNetwork(0); + _deployWethNativeConverterAgglayer(); + vm.expectRevert(WethNativeConverterAgglayer.FunctionNotSupportedOnThisChain.selector); + (address(nativeConverter).call{value: amount}("")); + + mockAgglayerBridge.setGasTokenAddress(address(0)); + mockAgglayerBridge.setGasTokenNetwork(1); + _deployWethNativeConverterAgglayer(); + vm.expectRevert(WethNativeConverterAgglayer.FunctionNotSupportedOnThisChain.selector); + (address(nativeConverter).call{value: amount}("")); + + mockAgglayerBridge.setGasTokenAddress(address(0)); + mockAgglayerBridge.setGasTokenNetwork(0); + _deployWethNativeConverterAgglayer(); + (address(nativeConverter).call{value: amount}("")); + assertEq(address(nativeConverter).balance, amount); + } + + function _deployWethNativeConverterAgglayer() internal { + nativeConverter = new WethNativeConverterAgglayer(); + bytes memory initData = abi.encodeCall( + WethNativeConverterAgglayer.reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager, + maxNonMigratableGasBackingPercentage + ) + ); + nativeConverter = + WethNativeConverterAgglayer(payable(_proxify(address(nativeConverter), address(this), initData))); + } +} diff --git a/test/unit/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTest.t.sol b/test/unit/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTest.t.sol new file mode 100644 index 00000000..aa1b3b70 --- /dev/null +++ b/test/unit/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTest.t.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import {VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase} from + "test/base/secondary-chain/VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase.sol"; + +// Core contracts +import {NativeConverter} from "src/secondary-chain/NativeConverter.sol"; +import {VbUsdcNativeConverterAgglayerBridgedUsdcStandard} from + "src/secondary-chain/agglayer/vbUSDC/bridged-usdc-standard/VbUsdcNativeConverterAgglayerBridgedUsdcStandard.sol"; + +// OpenZeppelin +import {PausableUpgradeable} from "@openzeppelin-contracts-upgradeable/utils/PausableUpgradeable.sol"; + +/// @dev VbUsdcNativeConverterAgglayerBridgedUsdcStandard tests +contract VbUsdcNativeConverterAgglayerBridgedUsdcStandardTest is + VbUsdcNativeConverterAgglayerBridgedUsdcStandardTestBase +{ + function setUp() public { + deployVbUsdcNativeConverterAgglayerBridgedUsdcStandardInfrastructure(); + } + + function test_initialize() public { + vm.revertToState(stateBeforeInitialize); + + bytes memory initData; + + // Test invalid owner + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + address(0), + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidOwner.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + + // Test invalid custom token + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(0), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidCustomToken.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + + // Test invalid underlying token + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(customToken), + address(0), + address(mockAgglayerBridge), + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidUnderlyingToken.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + + // Test invalid agglayer bridge + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(0), // invalid agglayer bridge + primaryChainAgglayerId, + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidAgglayerBridge.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + + // Test invalid primary chain agglayer ID (0 is invalid) + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + 1, // invalid primary chain agglayer ID + maxNonMigratableBackingPercentage, + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidAgglayerBridge.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + + // Test invalid non-migratable backing percentage (must be <= 100%) + initData = abi.encodeCall( + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(address(0))).reinitialize1, + ( + owner, + address(customToken), + address(underlyingToken), + address(mockAgglayerBridge), + primaryChainAgglayerId, + 1.5e18, // 150% - should fail (above 100%) + migrationManager + ) + ); + vm.expectRevert(NativeConverter.InvalidNonMigratableBackingPercentage.selector); + VbUsdcNativeConverterAgglayerBridgedUsdcStandard(payable(_proxify(nativeConverterImpl, proxyAdmin, initData))); + } + + function test_burnCustomToken_viaDeconvert() public { + uint256 amount = 100e6; // 100 USDC (6 decimals) + + // First create backing by converting + deal(address(underlyingToken), owner, amount); + vm.startPrank(owner); + underlyingToken.approve(address(nativeConverter), amount); + nativeConverter.convert(amount, recipient); + vm.stopPrank(); + + // Verify vbUSDC tokens were minted + assertEq(vbUsdcToken.balanceOf(recipient), amount); + uint256 initialSupply = vbUsdcToken.totalSupply(); + assertGt(initialSupply, 0); + + // Then deconvert which will burn the custom tokens via _burnCustomToken + vm.startPrank(recipient); + vbUsdcToken.approve(address(nativeConverter), amount); + nativeConverter.deconvert(amount, sender); + vm.stopPrank(); + + // Verify tokens were burned during deconvert + assertEq(vbUsdcToken.balanceOf(recipient), 0); + assertEq(vbUsdcToken.totalSupply(), initialSupply - amount); + assertEq(underlyingToken.balanceOf(sender), amount); + } +} diff --git a/test/unit/secondary-chain/polygon/GenericCustomTokenPolygon.t.sol b/test/unit/secondary-chain/polygon/GenericCustomTokenPolygon.t.sol new file mode 100644 index 00000000..f9650896 --- /dev/null +++ b/test/unit/secondary-chain/polygon/GenericCustomTokenPolygon.t.sol @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + GenericCustomTokenPolygonTestBase, + TransparentUpgradeableProxy +} from "test/base/secondary-chain/GenericCustomTokenPolygonTestBase.sol"; + +// Core contracts +import {GenericCustomTokenPolygon} from "src/secondary-chain/polygon/GenericCustomTokenPolygon.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; + +/// @dev GenericCustomTokenPolygon tests +/// @notice Comprehensive tests for GenericCustomTokenPolygon which also cover CustomTokenPolygon functionality +contract GenericCustomTokenPolygonTest is GenericCustomTokenPolygonTestBase { + function setUp() public virtual { + deployGenericCustomTokenPolygonInfrastructure(); + } + + /// @notice Helper function to test initialization reverts with different parameters + function _testInitializationRevert( + bytes4 expectedError, + address owner_, + string memory name_, + string memory symbol_, + uint8 decimals_, + address childChainManager_ + ) internal { + vm.expectRevert(expectedError); + TransparentUpgradeableProxy( + payable( + _proxify( + genericCustomTokenPolygonImpl, + proxyAdmin, + abi.encodeCall( + GenericCustomTokenPolygon.reinitialize1, (owner_, name_, symbol_, decimals_, childChainManager_) + ) + ) + ) + ); + } + + function test_initialize_revertsWhenCalledTwice() public { + // Try to reinitialize again + vm.expectRevert(); + genericCustomTokenPolygon.reinitialize1(owner, "Another Name", "ANT", 18, childChainManager); + } + + function test_initialize_revertsWithInvalidOwner() public { + _testInitializationRevert( + CustomToken.InvalidOwner.selector, + address(0), // Invalid owner + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + childChainManager + ); + } + + function test_initialize_revertsWithInvalidName() public { + _testInitializationRevert( + CustomToken.InvalidName.selector, + owner, + "", // Invalid name + customTokenSymbol, + originalUnderlyingTokenDecimals, + childChainManager + ); + } + + function test_initialize_revertsWithInvalidSymbol() public { + _testInitializationRevert( + CustomToken.InvalidSymbol.selector, + owner, + customTokenName, + "", // Invalid symbol + originalUnderlyingTokenDecimals, + childChainManager + ); + } + + function test_initialize_revertsWithInvalidDecimals() public { + _testInitializationRevert( + CustomToken.InvalidOriginalUnderlyingTokenDecimals.selector, + owner, + customTokenName, + customTokenSymbol, + 0, // Invalid decimals + childChainManager + ); + } + + function test_initialize_revertsWithInvalidBridge() public { + _testInitializationRevert( + CustomToken.InvalidBridge.selector, + owner, + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + address(0) // Invalid bridge + ); + } + + function test_deposit_success() public { + uint256 amount = 1000e18; + + simulateDeposit(sender, amount); + + assertEq(genericCustomTokenPolygon.balanceOf(sender), amount); + assertEq(genericCustomTokenPolygon.totalSupply(), amount); + } + + function test_deposit_success_multipleUsers() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + uint256 amount1 = 1000e18; + uint256 amount2 = 2000e18; + + simulateDeposit(user1, amount1); + simulateDeposit(user2, amount2); + + assertEq(genericCustomTokenPolygon.balanceOf(user1), amount1); + assertEq(genericCustomTokenPolygon.balanceOf(user2), amount2); + assertEq(genericCustomTokenPolygon.totalSupply(), amount1 + amount2); + } + + function test_deposit_success_toAddressZero_revertsWithInvalidReceiver() public { + uint256 amount = 1000e18; + + bytes memory depositData = abi.encode(amount); + + // Deposit to address(0) should revert with ERC20InvalidReceiver + vm.expectRevert(); + vm.prank(childChainManager); + genericCustomTokenPolygon.deposit(address(0), depositData); + + assertEq(genericCustomTokenPolygon.totalSupply(), 0); + } + + function test_deposit_revertsWithUnauthorized() public { + uint256 amount = 1000e18; + bytes memory depositData = abi.encode(amount); + + vm.expectRevert(CustomToken.Unauthorized.selector); + vm.prank(makeAddr("unauthorized")); + genericCustomTokenPolygon.deposit(sender, depositData); + } + + function test_deposit_revertsWhenPaused() public { + uint256 amount = 1000e18; + bytes memory depositData = abi.encode(amount); + + vm.prank(owner); + genericCustomTokenPolygon.pause(); + + vm.expectRevert(); + vm.prank(childChainManager); + genericCustomTokenPolygon.deposit(sender, depositData); + } + + function test_deposit_revertsWithIncorrectDepositData() public { + bytes memory invalidDepositData = hex"1234"; + + vm.expectRevert(); + vm.prank(childChainManager); + genericCustomTokenPolygon.deposit(sender, invalidDepositData); + } + + function test_withdraw_success() public { + uint256 amount = 1000e18; + + // First deposit tokens + simulateDeposit(sender, amount); + assertEq(genericCustomTokenPolygon.balanceOf(sender), amount); + + // Then withdraw them + simulateWithdraw(sender, amount); + + assertEq(genericCustomTokenPolygon.balanceOf(sender), 0); + assertEq(genericCustomTokenPolygon.totalSupply(), 0); + } + + function test_withdraw_success_partialWithdraw() public { + uint256 depositAmount = 1000e18; + uint256 withdrawAmount = 300e18; + + // First deposit tokens + simulateDeposit(sender, depositAmount); + assertEq(genericCustomTokenPolygon.balanceOf(sender), depositAmount); + + // Then withdraw partial amount + simulateWithdraw(sender, withdrawAmount); + + assertEq(genericCustomTokenPolygon.balanceOf(sender), depositAmount - withdrawAmount); + assertEq(genericCustomTokenPolygon.totalSupply(), depositAmount - withdrawAmount); + } + + function test_withdraw_success_multipleUsers() public { + address user1 = makeAddr("user1"); + address user2 = makeAddr("user2"); + uint256 amount1 = 1000e18; + uint256 amount2 = 2000e18; + + // Deposit to both users + simulateDeposit(user1, amount1); + simulateDeposit(user2, amount2); + + // User1 withdraws + simulateWithdraw(user1, amount1); + + assertEq(genericCustomTokenPolygon.balanceOf(user1), 0); + assertEq(genericCustomTokenPolygon.balanceOf(user2), amount2); + assertEq(genericCustomTokenPolygon.totalSupply(), amount2); + } + + function test_withdraw_revertsWithInsufficientBalance() public { + uint256 depositAmount = 500e18; + uint256 withdrawAmount = 1000e18; + + // Deposit less than we try to withdraw + simulateDeposit(sender, depositAmount); + + vm.expectRevert(); + simulateWithdraw(sender, withdrawAmount); + } + + function test_withdraw_revertsWhenPaused() public { + uint256 amount = 1000e18; + + // First deposit tokens + simulateDeposit(sender, amount); + + vm.prank(owner); + genericCustomTokenPolygon.pause(); + + vm.expectRevert(); + simulateWithdraw(sender, amount); + } + + function test_onlyChildChainManager_allowsChildChainManager() public { + uint256 amount = 1000e18; + bytes memory depositData = abi.encode(amount); + + // Should succeed when called by child chain manager + vm.prank(childChainManager); + genericCustomTokenPolygon.deposit(sender, depositData); + + assertEq(genericCustomTokenPolygon.balanceOf(sender), amount); + } + + function test_onlyChildChainManager_revertsForUnauthorized() public { + uint256 amount = 1000e18; + bytes memory depositData = abi.encode(amount); + + // Should revert when called by unauthorized address + vm.expectRevert(CustomToken.Unauthorized.selector); + vm.prank(makeAddr("unauthorized")); + genericCustomTokenPolygon.deposit(sender, depositData); + } +} diff --git a/test/unit/secondary-chain/wormhole/GenericCustomTokenWormhole.t.sol b/test/unit/secondary-chain/wormhole/GenericCustomTokenWormhole.t.sol new file mode 100644 index 00000000..88f0336c --- /dev/null +++ b/test/unit/secondary-chain/wormhole/GenericCustomTokenWormhole.t.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import { + GenericCustomTokenWormholeTestBase, + TransparentUpgradeableProxy, + ITransparentUpgradeableProxy +} from "test/base/secondary-chain/GenericCustomTokenWormholeTestBase.sol"; + +// Core contracts +import {GenericCustomTokenWormhole} from "src/secondary-chain/wormhole/GenericCustomTokenWormhole.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; + +/// @dev GenericCustomTokenWormhole tests +/// @notice Comprehensive tests for GenericCustomTokenWormhole which also cover CustomTokenWormhole functionality +contract GenericCustomTokenWormholeTest is GenericCustomTokenWormholeTestBase { + function setUp() public virtual { + deployGenericCustomTokenWormholeInfrastructure(); + } + + /// @notice Helper function to test initialization reverts with different parameters + function _testInitializationRevert( + bytes4 expectedError, + address owner_, + string memory name_, + string memory symbol_, + uint8 originalUnderlyingTokenDecimals_, + address nttManager_ + ) internal { + vm.expectRevert(expectedError); + TransparentUpgradeableProxy( + payable( + _proxify( + genericCustomTokenWormholeImpl, + proxyAdmin, + abi.encodeCall( + GenericCustomTokenWormhole.reinitialize1, + (owner_, name_, symbol_, originalUnderlyingTokenDecimals_, nttManager_) + ) + ) + ) + ); + } + + function test_initialize_revert_zeroOwner() public { + _testInitializationRevert( + CustomToken.InvalidOwner.selector, + address(0), + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + nttManager + ); + } + + function test_initialize_revert_emptyName() public { + _testInitializationRevert( + CustomToken.InvalidName.selector, owner, "", customTokenSymbol, originalUnderlyingTokenDecimals, nttManager + ); + } + + function test_initialize_revert_emptySymbol() public { + _testInitializationRevert( + CustomToken.InvalidSymbol.selector, owner, customTokenName, "", originalUnderlyingTokenDecimals, nttManager + ); + } + + function test_initialize_revert_zeroDecimals() public { + _testInitializationRevert( + CustomToken.InvalidOriginalUnderlyingTokenDecimals.selector, + owner, + customTokenName, + customTokenSymbol, + 0, + nttManager + ); + } + + function test_initialize_revert_zeroNttManager() public { + _testInitializationRevert( + CustomToken.InvalidBridge.selector, + owner, + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + address(0) + ); + } + + function test_mint_success_fromNttManager() public { + uint256 amount = 1000e18; + + vm.prank(nttManager); + genericCustomTokenWormhole.mint(sender, amount); + + assertEq(genericCustomTokenWormhole.balanceOf(sender), amount); + assertEq(genericCustomTokenWormhole.totalSupply(), amount); + } + + function test_mint_revert_unauthorized() public { + uint256 amount = 1000e18; + + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(CustomToken.Unauthorized.selector)); + genericCustomTokenWormhole.mint(sender, amount); + } + + function test_burn_success_fromNttManager() public { + uint256 amount = 1000e18; + + // First mint tokens + vm.prank(nttManager); + genericCustomTokenWormhole.mint(nttManager, amount); + + // Then burn them + vm.prank(nttManager); + genericCustomTokenWormhole.burn(amount); + + assertEq(genericCustomTokenWormhole.balanceOf(nttManager), 0); + assertEq(genericCustomTokenWormhole.totalSupply(), 0); + } + + function test_burn_revert_unauthorized() public { + uint256 amount = 1000e18; + + // First mint tokens to sender + vm.prank(nttManager); + genericCustomTokenWormhole.mint(sender, amount); + + // Try to burn from sender (should fail) + vm.prank(sender); + vm.expectRevert(abi.encodeWithSelector(CustomToken.Unauthorized.selector)); + genericCustomTokenWormhole.burn(amount); + } + + function test_mint_and_burn_multipleUsers() public { + address user2 = makeAddr("user2"); + uint256 amount1 = 500e18; + uint256 amount2 = 1000e18; + + // Mint to sender + vm.prank(nttManager); + genericCustomTokenWormhole.mint(sender, amount1); + + // Mint to user2 + vm.prank(nttManager); + genericCustomTokenWormhole.mint(user2, amount2); + + assertEq(genericCustomTokenWormhole.balanceOf(sender), amount1); + assertEq(genericCustomTokenWormhole.balanceOf(user2), amount2); + assertEq(genericCustomTokenWormhole.totalSupply(), amount1 + amount2); + + // Transfer from user2 to nttManager, then burn + vm.prank(user2); + genericCustomTokenWormhole.transfer(nttManager, amount2); + + vm.prank(nttManager); + genericCustomTokenWormhole.burn(amount2); + + assertEq(genericCustomTokenWormhole.balanceOf(sender), amount1); + assertEq(genericCustomTokenWormhole.balanceOf(user2), 0); + assertEq(genericCustomTokenWormhole.balanceOf(nttManager), 0); + assertEq(genericCustomTokenWormhole.totalSupply(), amount1); + } +} diff --git a/test/unit/secondary-chain/wormhole/vbETH/WethWormholeTest.t.sol b/test/unit/secondary-chain/wormhole/vbETH/WethWormholeTest.t.sol new file mode 100644 index 00000000..2ffb5d27 --- /dev/null +++ b/test/unit/secondary-chain/wormhole/vbETH/WethWormholeTest.t.sol @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +// Test Base +import {WethWormholeTestBase, TransparentUpgradeableProxy} from "test/base/secondary-chain/WethWormholeTestBase.sol"; + +// Core contracts +import {WethWormhole} from "src/secondary-chain/wormhole/vbETH/WethWormhole.sol"; +import {CustomToken} from "src/secondary-chain/CustomToken.sol"; +import {CustomTokenWethExtension} from "src/secondary-chain/CustomTokenWethExtension.sol"; + +/// @dev WethWormhole tests +/// @notice Comprehensive tests for WethWormhole which covers both CustomTokenWormhole and CustomTokenWethExtension functionality +contract WethWormholeTest is WethWormholeTestBase { + function setUp() public virtual { + deployWethWormholeInfrastructure(); + } + + /// @notice Helper to test initialization reverts with different parameters + function _testInitializationRevert( + bytes4 expectedError, + address owner_, + string memory name_, + string memory symbol_, + uint8 originalUnderlyingTokenDecimals_, + address nttManager_, + bool gasTokenIsEth_ + ) internal returns (address) { + if (expectedError != bytes4(0)) { + vm.expectRevert(expectedError); + } + + TransparentUpgradeableProxy newProxy = TransparentUpgradeableProxy( + payable( + _proxify( + wethWormholeImpl, + proxyAdmin, + abi.encodeCall( + WethWormhole.reinitialize1, + ( + owner_, + name_, + symbol_, + originalUnderlyingTokenDecimals_, + nttManager_, + gasTokenIsEth_, + gasTokenIsEth_ + ) + ) + ) + ) + ); + return address(newProxy); + } + + function test_initialize_revert_zeroOwner() public { + _testInitializationRevert( + CustomToken.InvalidOwner.selector, + address(0), + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + nttManager, + gasTokenIsEth + ); + } + + function test_initialize_revert_emptyName() public { + _testInitializationRevert( + CustomToken.InvalidName.selector, + owner, + "", + customTokenSymbol, + originalUnderlyingTokenDecimals, + nttManager, + gasTokenIsEth + ); + } + + function test_initialize_revert_emptySymbol() public { + _testInitializationRevert( + CustomToken.InvalidSymbol.selector, + owner, + customTokenName, + "", + originalUnderlyingTokenDecimals, + nttManager, + gasTokenIsEth + ); + } + + function test_initialize_revert_zeroDecimals() public { + _testInitializationRevert( + CustomToken.InvalidOriginalUnderlyingTokenDecimals.selector, + owner, + customTokenName, + customTokenSymbol, + 0, + nttManager, + gasTokenIsEth + ); + } + + function test_initialize_revert_zeroNttManager() public { + _testInitializationRevert( + CustomToken.InvalidBridge.selector, + owner, + customTokenName, + customTokenSymbol, + originalUnderlyingTokenDecimals, + address(0), + gasTokenIsEth + ); + } + + function test_init_gasTokenIsEth_true() public { + address testWethWormholeProxy = _testInitializationRevert( + bytes4(0), + owner, + "Test WETH", + "tWETH", + 18, + nttManager, + true // gasTokenIsEth = true + ); + WethWormhole testWeth = WethWormhole(payable(testWethWormholeProxy)); + + // Test that deposit works (only works when gasTokenIsEth=true) + uint256 depositAmount = 1 ether; + deal(address(this), depositAmount); + + testWeth.deposit{value: depositAmount}(); + assertEq(testWeth.balanceOf(address(this)), depositAmount); + } + + function test_init_wethFunctionalityEnabled_true() public { + address testWethWormholeProxy = _testInitializationRevert( + bytes4(0), + owner, + "Test WETH", + "tWETH", + 18, + nttManager, + true // gasTokenIsEth = true & by extension wethFunctionalityEnabled = true + ); + WethWormhole testWeth = WethWormhole(payable(testWethWormholeProxy)); + + // Verify wethFunctionalityEnabled is set correctly + assertTrue(testWeth.wethFunctionalityEnabled()); + + // Test that deposit works when wethFunctionalityEnabled=true + uint256 depositAmount = 1 ether; + deal(address(this), depositAmount); + + testWeth.deposit{value: depositAmount}(); + assertEq(testWeth.balanceOf(address(this)), depositAmount); + } + + function test_init_wethFunctionalityEnabled_false() public { + address testWethWormholeProxy = _testInitializationRevert( + bytes4(0), + owner, + "Test WETH", + "tWETH", + 18, + nttManager, + false // gasTokenIsEth = false & by externsion wethFunctionalityEnabled = false + ); + WethWormhole testWeth = WethWormhole(payable(testWethWormholeProxy)); + + // Verify wethFunctionalityEnabled is set correctly + assertFalse(testWeth.wethFunctionalityEnabled()); + + // Test that deposit reverts when wethFunctionalityEnabled=false + uint256 depositAmount = 1 ether; + deal(address(this), depositAmount); + + vm.expectRevert(CustomTokenWethExtension.FunctionNotEnabledOnThisChain.selector); + testWeth.deposit{value: depositAmount}(); + } +} diff --git a/test/etc/ZkEVMCommon.sol b/test/utils/ZkEVMCommon.sol similarity index 89% rename from test/etc/ZkEVMCommon.sol rename to test/utils/ZkEVMCommon.sol index 8bdeac81..af5a55e5 100644 --- a/test/etc/ZkEVMCommon.sol +++ b/test/utils/ZkEVMCommon.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available pragma solidity ^0.8.29; import "forge-std/Test.sol"; @@ -15,7 +15,7 @@ abstract contract ZkEVMCommon is Test { function _getMerkleTreeRoot(string memory encodedLeaves) public returns (bytes32) { string[] memory operation = new string[](5); operation[0] = "node"; - operation[1] = "test/etc/zkevm-commonjs-wrapper"; + operation[1] = "test/utils/zkevm-commonjs-wrapper"; operation[2] = "makeTreeAndGetRoot"; operation[3] = MERKLE_TREE_HEIGHT; operation[4] = encodedLeaves; @@ -27,7 +27,7 @@ abstract contract ZkEVMCommon is Test { function _getProofByIndex(string memory encodedLeaves, string memory index) public returns (bytes32[32] memory) { string[] memory operation = new string[](6); operation[0] = "node"; - operation[1] = "test/etc/zkevm-commonjs-wrapper"; + operation[1] = "test/utils/zkevm-commonjs-wrapper"; operation[2] = "makeTreeAndGetProofByIndex"; operation[3] = MERKLE_TREE_HEIGHT; operation[4] = encodedLeaves; diff --git a/test/utils/mocks/MockAgglayerBridge.sol b/test/utils/mocks/MockAgglayerBridge.sol new file mode 100644 index 00000000..58fc0fe5 --- /dev/null +++ b/test/utils/mocks/MockAgglayerBridge.sol @@ -0,0 +1,137 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.29; + +import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @title MockAgglayerBridge + * @dev A mock implementation of the Agglayer Bridge for testing without fork dependency + */ +contract MockAgglayerBridge { + uint32 public depositCount; + uint32 public networkID; + + // Gas token properties for VbETH testing + address public gasTokenAddress; + uint32 public gasTokenNetwork; + + address public globalExitRootManager; + + event BridgeEvent( + uint8 leafType, + uint32 originNetwork, + address originAddress, + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + bytes metadata, + uint32 depositCount + ); + + event ClaimEvent( + uint256 globalIndex, uint32 originNetwork, address originAddress, address destinationAddress, uint256 amount + ); + + function bridgeAsset( + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + address token, + bool, /* forceUpdateGlobalExitRoot */ + bytes calldata /* permitData */ + ) external payable { + bytes memory metadata; + + if (token == address(0)) { + require(msg.value == amount, "MockAgglayerBridge: ETH amount mismatch"); + metadata = ""; + } else { + // Transfer tokens to this bridge contract (like the real bridge does) + IERC20Metadata erc20 = IERC20Metadata(token); + require(erc20.transferFrom(msg.sender, address(this), amount), "MockAgglayerBridge: transfer failed"); + + // Read token metadata + string memory name = erc20.name(); + string memory symbol = erc20.symbol(); + uint8 decimals = erc20.decimals(); + + // Encode metadata the same way the real bridge does + metadata = abi.encode(name, symbol, decimals); + } + + // Determine origin network based on token type + uint32 originNetwork; + if (token == address(0)) { + // For ETH (gas token), the origin is always the primary chain (network 0) + originNetwork = 0; + } else { + // For other tokens, use the current network + originNetwork = networkID; + } + + // Emit BridgeEvent with the current depositCount (before incrementing) + emit BridgeEvent( + 0, // leafType: LEAF_TYPE_ASSET + originNetwork, + token, // originAddress + destinationNetwork, + destinationAddress, + amount, + metadata, + depositCount + ); + + depositCount++; + } + + function bridgeMessage( + uint32 destinationNetwork, + address destinationAddress, + bool, /* forceUpdateGlobalExitRoot */ + bytes calldata message + ) external payable { + emit BridgeEvent( + 1, // leafType: LEAF_TYPE_MESSAGE + networkID, // originNetwork + msg.sender, // originAddress + destinationNetwork, + destinationAddress, + 0, // amount + message, + depositCount + ); + + depositCount++; + } + + /// @dev Set the Global Exit Root Manager address + function setGlobalExitRootManager(address _globalExitRootManager) external { + globalExitRootManager = _globalExitRootManager; + } + + /// @dev Set network id for testing different networks + function setNetworkId(uint32 _networkID) external { + networkID = _networkID; + } + + /// @dev Set deposit count for testing + function setDepositCount(uint32 _depositCount) external { + depositCount = _depositCount; + } + + /// @dev Set gas token address for VbETH testing + function setGasTokenAddress(address _gasTokenAddress) external { + gasTokenAddress = _gasTokenAddress; + } + + /// @dev Set gas token network for VbETH testing + function setGasTokenNetwork(uint32 _gasTokenNetwork) external { + gasTokenNetwork = _gasTokenNetwork; + } + + /// @dev For testing, assume all wrapped addresses are not mintable + function wrappedAddressIsNotMintable(address wrappedAddress) external pure returns (bool isNotMintable) { + (wrappedAddress); + return true; + } +} diff --git a/test/utils/mocks/MockChildChainManager.sol b/test/utils/mocks/MockChildChainManager.sol new file mode 100644 index 00000000..de79dac7 --- /dev/null +++ b/test/utils/mocks/MockChildChainManager.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +/// @title Mock Child Chain Manager +/// @notice Simulates the Polygon PoS ChildChainManager for testing +contract MockChildChainManager { + mapping(address => address) public rootToChildToken; + mapping(address => bool) public isTokenMapped; + + event TokensMapped(address indexed rootToken, address indexed childToken); + + function mapToken(address rootToken, address childToken) external { + rootToChildToken[rootToken] = childToken; + isTokenMapped[childToken] = true; + emit TokensMapped(rootToken, childToken); + } + + function onStateReceive(uint256, bytes calldata data) external { + (address user, address rootToken, uint256 amount) = abi.decode(data, (address, address, uint256)); + address childToken = rootToChildToken[rootToken]; + require(isTokenMapped[childToken], "Token not mapped"); + + (bool success,) = childToken.call(abi.encodeWithSignature("deposit(address,bytes)", user, abi.encode(amount))); + require(success, "Deposit failed"); + } +} diff --git a/test/utils/mocks/MockERC20.sol b/test/utils/mocks/MockERC20.sol new file mode 100644 index 00000000..fd786afb --- /dev/null +++ b/test/utils/mocks/MockERC20.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; + +/// @title Mock ERC20 Token +/// @notice Simple mock ERC20 token for testing purposes with permit support +/// @dev Provides a basic ERC20 implementation with permit and public mint function for testing +contract MockERC20 is ERC20Permit { + uint8 private _decimals; + + /// @notice Constructor to create a mock ERC20 token + /// @param name_ The name of the token + /// @param symbol_ The symbol of the token + /// @param decimals_ The number of decimals for the token + constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) ERC20Permit(name_) { + _decimals = decimals_; + } + + /// @notice Override decimals to return the specified value + /// @return The number of decimals + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + /// @notice Mint tokens to an address (for testing) + /// @param to The address to mint tokens to + /// @param amount The amount of tokens to mint + function mint(address to, uint256 amount) external { + _mint(to, amount); + } + + /// @notice Burn tokens from an address (for testing) + /// @param from The address to burn tokens from + /// @param amount The amount of tokens to burn + function burn(address from, uint256 amount) external { + _burn(from, amount); + } +} diff --git a/test/utils/mocks/MockERC20Upgradeable.sol b/test/utils/mocks/MockERC20Upgradeable.sol new file mode 100644 index 00000000..5851ba44 --- /dev/null +++ b/test/utils/mocks/MockERC20Upgradeable.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {ERC20PermitUpgradeable} from + "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; + +/// @title Mock ERC20 Mintable and Burnable +/// @notice An updradeable version of the mock ERC20 token that supports minting and burning +contract MockERC20Upgradeable is ERC20PermitUpgradeable { + /// @notice Initialize the mock ERC20 token + function initialize(string memory name_, string memory symbol_) external initializer { + __ERC20_init(name_, symbol_); + __ERC20Permit_init(name_); + } + + /// @notice Mint tokens to a specified account + /// @param account The address to mint tokens to + /// @param amount The amount of tokens to mint + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + /// @notice Burn tokens from a specified account + /// @param account The address to burn tokens from + /// @param amount The amount of tokens to burn + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} diff --git a/test/utils/mocks/MockFiatTokenV2_2.sol b/test/utils/mocks/MockFiatTokenV2_2.sol new file mode 100644 index 00000000..3ca93d62 --- /dev/null +++ b/test/utils/mocks/MockFiatTokenV2_2.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {ERC20PermitUpgradeable} from + "@openzeppelin-contracts-upgradeable/token/ERC20/extensions/ERC20PermitUpgradeable.sol"; +import {IFiatTokenV2_2} from "src/etc/IFiatTokenV2_2.sol"; + +/// @title Mock FiatToken V2.2 (USDC) +/// @notice Mock implementation of IFiatTokenV2_2 for testing vbUSDC Native Converter +contract MockFiatTokenV2_2 is ERC20PermitUpgradeable, IFiatTokenV2_2 { + /// @notice Initialize the mock FiatToken + function initialize(string memory name_, string memory symbol_) external initializer { + __ERC20_init(name_, symbol_); + __ERC20Permit_init(name_); + } + + /// @notice Mint tokens to a specified account + /// @param account The address to mint tokens to + /// @param amount The amount of tokens to mint + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + /// @notice Burn tokens from the caller's balance + /// @param amount The amount of tokens to burn + function burn(uint256 amount) external override { + _burn(msg.sender, amount); + } + + /// @notice Burn tokens from a specified account (for testing) + /// @param account The address to burn tokens from + /// @param amount The amount of tokens to burn + function burnFrom(address account, uint256 amount) external { + _burn(account, amount); + } + + /// @notice Get decimals (USDC has 6 decimals) + function decimals() public pure override returns (uint8) { + return 6; + } +} diff --git a/test/utils/mocks/MockGlobalExitRootManager.sol b/test/utils/mocks/MockGlobalExitRootManager.sol new file mode 100644 index 00000000..d448e66e --- /dev/null +++ b/test/utils/mocks/MockGlobalExitRootManager.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +/// @dev A mock GlobalExitRootManager for testing without fork dependency +contract MockGlobalExitRootManager { + /// @dev Mock function to simulate updating the exit root + function updateExitRoot(bytes32 newRoot) public pure { + (newRoot); + } +} diff --git a/test/utils/mocks/MockInitializationCounterUpgradeable.sol b/test/utils/mocks/MockInitializationCounterUpgradeable.sol new file mode 100644 index 00000000..8257569b --- /dev/null +++ b/test/utils/mocks/MockInitializationCounterUpgradeable.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity 0.8.29; + +import {InitializationCounterUpgradeable} from "src/etc/InitializationCounterUpgradeable.sol"; + +/// @title Mock InitializationCounterUpgradeable +/// @notice An implementation of InitializationCounterUpgradeable for testing purposes +/// @dev This contract exposes internal functionality of InitializationCounterUpgradeable for testing +contract MockInitializationCounterUpgradeable is InitializationCounterUpgradeable { + /// @notice Expose the private _localInitializationCounter for testing + function localInitializationCounter() external view returns (uint64) { + InitializationCounterUpgradeableStorage storage $; + assembly { + $.slot := 0x8d679e361eeeac0b879fa197c8b3bda76a3db4f57c9f89335c04a065390bbb00 + } + return $._localInitializationCounter; + } + + /// @notice Expose the private _extensionInitializationCounter + function extensionInitializationCounter() external view returns (uint64) { + InitializationCounterUpgradeableStorage storage $; + assembly { + $.slot := 0x8d679e361eeeac0b879fa197c8b3bda76a3db4f57c9f89335c04a065390bbb00 + } + return $._extensionInitializationCounter[Extension.WETH]; + } + + /// @notice Public wrapper for _incrementGlobalInitializationCounter + function incrementGlobalInitializationCounter(uint64 expectedNewValue) external returns (uint64) { + return _incrementGlobalInitializationCounter(expectedNewValue); + } + + /// @notice Function that uses incrementsLocalInitializationCounter modifier + function incrementLocalInitializationCounterWithModifier(uint64 expectedNewValue) + external + incrementsLocalInitializationCounter(expectedNewValue) + { + // Function body can be empty, the modifier does the work + } + + /// @notice Function that uses incrementsExtensionInitializationCounter modifier + function incrementExtensionInitializationCounterWithModifier( + uint64 requiredLocalValue, + uint64 expectedNewExtensionValue + ) + external + incrementsExtensionInitializationCounter(requiredLocalValue, Extension.WETH, expectedNewExtensionValue) + { + // Function body can be empty, the modifier does the work + } + + /// @notice Helper function to get storage slot for testing + function getStorageSlot() external pure returns (bytes32) { + return 0x8d679e361eeeac0b879fa197c8b3bda76a3db4f57c9f89335c04a065390bbb00; + } +} diff --git a/test/utils/mocks/MockLxlyBridgeWrappedToken.sol b/test/utils/mocks/MockLxlyBridgeWrappedToken.sol new file mode 100644 index 00000000..28efd5a0 --- /dev/null +++ b/test/utils/mocks/MockLxlyBridgeWrappedToken.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/// @title MockLxlyBridgeWrappedToken +/// @notice Mock implementation of a bridge wrapped token for testing +/// @dev Simulates the token created by Lxly bridge when bridging assets +contract MockLxlyBridgeWrappedToken is ERC20 { + // Domain typehash + bytes32 public constant DOMAIN_TYPEHASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + // Permit typehash + bytes32 public constant PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + // Version + string public constant VERSION = "1"; + + // Chain id on deployment + uint256 public immutable deploymentChainId; + + // Domain separator calculated on deployment + bytes32 private immutable _DEPLOYMENT_DOMAIN_SEPARATOR; + + // PolygonZkEVM Bridge address + address public immutable bridgeAddress; + + // Decimals + uint8 private immutable _decimals; + + // Permit nonces + mapping(address => uint256) public nonces; + + modifier onlyBridge() { + require(msg.sender == bridgeAddress, "MockLxlyBridgeWrappedToken::onlyBridge: Not PolygonZkEVMBridge"); + _; + } + + constructor(string memory name, string memory symbol, uint8 __decimals) ERC20(name, symbol) { + bridgeAddress = msg.sender; + _decimals = __decimals; + deploymentChainId = block.chainid; + _DEPLOYMENT_DOMAIN_SEPARATOR = _calculateDomainSeparator(block.chainid); + } + + function mint(address to, uint256 value) external onlyBridge { + _mint(to, value); + } + + // Notice that is not require to approve wrapped tokens to use the bridge + function burn(address account, uint256 value) external onlyBridge { + _burn(account, value); + } + + function decimals() public view virtual override returns (uint8) { + return _decimals; + } + + // Permit relative functions + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) + external + { + require(block.timestamp <= deadline, "MockLxlyBridgeWrappedToken::permit: Expired permit"); + + bytes32 hashStruct = keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline)); + + bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR(), hashStruct)); + + address signer = ecrecover(digest, v, r, s); + require(signer != address(0) && signer == owner, "MockLxlyBridgeWrappedToken::permit: Invalid signature"); + + _approve(owner, spender, value); + } + + /** + * @notice Calculate domain separator, given a chainID. + * @param chainId Current chainID + */ + function _calculateDomainSeparator(uint256 chainId) private view returns (bytes32) { + return keccak256( + abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name())), keccak256(bytes(VERSION)), chainId, address(this)) + ); + } + + /// @dev Return the DOMAIN_SEPARATOR. + function DOMAIN_SEPARATOR() public view returns (bytes32) { + return + block.chainid == deploymentChainId ? _DEPLOYMENT_DOMAIN_SEPARATOR : _calculateDomainSeparator(block.chainid); + } +} diff --git a/test/utils/mocks/MockRootChainManager.sol b/test/utils/mocks/MockRootChainManager.sol new file mode 100644 index 00000000..25aaa4fb --- /dev/null +++ b/test/utils/mocks/MockRootChainManager.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Mock Root Chain Manager +/// @notice Simulates the Polygon PoS RootChainManager for testing +contract MockRootChainManager { + mapping(address => address) public rootToChildToken; + mapping(address => bool) public isTokenMapped; + + event TokensMapped(address indexed rootToken, address indexed childToken, bytes32 indexed tokenType); + + function mapToken(address rootToken, address childToken, bytes32 tokenType) external { + rootToChildToken[rootToken] = childToken; + isTokenMapped[rootToken] = true; + emit TokensMapped(rootToken, childToken, tokenType); + } + + function depositFor(address, /* user */ address rootToken, bytes calldata depositData) external { + require(isTokenMapped[rootToken], "Token not mapped"); + + uint256 amount = abi.decode(depositData, (uint256)); + + // Transfer tokens from user to this contract (simulate locking) + IERC20(rootToken).transferFrom(msg.sender, address(this), amount); + } + + function exit(address user, address rootToken, uint256 amount) external { + // Simulate exit functionality - transfer tokens back to user + // In real implementation this would verify merkle proofs + IERC20(rootToken).transfer(user, amount); + } +} diff --git a/test/utils/mocks/MockVault.sol b/test/utils/mocks/MockVault.sol new file mode 100644 index 00000000..ae02aa30 --- /dev/null +++ b/test/utils/mocks/MockVault.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity 0.8.29; + +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract MockVault { + using SafeERC20 for IERC20; + + uint256 private _maxDeposit; + uint256 private _maxWithdraw; + + IERC20 public asset; + uint256 public slippageAmount; + bool public slippage; + + mapping(address => uint256) public balanceOf; + + constructor(address _asset) { + asset = IERC20(_asset); + } + + function convertToAssets(uint256 amount) external pure returns (uint256) { + return amount; + } + + function convertToShares(uint256 amount) external pure returns (uint256) { + return amount; + } + + function setSlippage(bool _slippage, uint256 _slippageAmount) external { + slippage = _slippage; + slippageAmount = _slippageAmount; + } + + function setBalance(address user, uint256 amount) external { + balanceOf[user] = amount; + } + + function setMaxDeposit(uint256 amount) external { + _maxDeposit = amount; + } + + function setMaxWithdraw(uint256 amount) external { + _maxWithdraw = amount; + } + + function maxDeposit(address user) external view returns (uint256) { + // silence the compiler + { + user; + } + return _maxDeposit; + } + + function maxWithdraw(address user) external view returns (uint256) { + // silence the compiler + { + user; + } + return _maxWithdraw; + } + + function maxRedeem(address user) external view returns (uint256) { + // silence the compiler + { + user; + } + return balanceOf[user]; + } + + function previewRedeem(uint256 shares) external pure returns (uint256) { + return shares; + } + + function redeem(uint256 shares, address receiver, address user) external returns (uint256) { + require(balanceOf[user] >= shares, "MockVault: Insufficient balance"); + _sendAssets(shares, receiver, user); + if (slippage) { + require(shares > slippageAmount, "MockVault: Slippage amount is too high"); + return shares + slippageAmount; + } else { + return shares; + } + } + + function deposit(uint256 amount, address user) external payable returns (uint256) { + if (slippage) { + require(amount > slippageAmount, "MockVault: Slippage amount is too high"); + _receiveAssets(amount - slippageAmount, user); + return amount - slippageAmount; + } else { + _receiveAssets(amount, user); + return amount; + } + } + + function withdraw(uint256 amount, address receiver, address user) external returns (uint256) { + require(balanceOf[user] >= amount, "MockVault: Insufficient balance"); + _sendAssets(amount, receiver, user); + if (slippage) { + require(amount > slippageAmount, "MockVault: Slippage amount is too high"); + return amount + slippageAmount; + } else { + return amount; + } + } + + function previewWithdraw(uint256 amount) external view returns (uint256) { + if (slippage) { + require(amount > slippageAmount, "MockVault: Slippage amount is too high"); + return amount + slippageAmount; + } else { + return amount; + } + } + + function _receiveAssets(uint256 amount, address user) internal { + asset.safeTransferFrom(user, address(this), amount); + balanceOf[user] += amount; + } + + function _sendAssets(uint256 amount, address receiver, address user) internal { + asset.safeTransfer(receiver, amount); + balanceOf[user] -= amount; + } +} diff --git a/test/utils/mocks/MockVbToken.sol b/test/utils/mocks/MockVbToken.sol new file mode 100644 index 00000000..054039ce --- /dev/null +++ b/test/utils/mocks/MockVbToken.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: LicenseRef-PolygonLabs-Source-Available +pragma solidity ^0.8.29; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @title Mock Vault Bridge Token for Migration Manager Testing +/// @notice A mock implementation of VaultBridgeToken for testing MigrationManager +contract MockVbToken { + IERC20 public underlyingToken; + + function setUnderlyingToken(address _underlyingToken) external { + underlyingToken = IERC20(_underlyingToken); + } + + function completeMigration(uint32 originNetwork, uint256 shares, uint256 assets) external {} +} diff --git a/test/utils/mocks/MockWETH.sol b/test/utils/mocks/MockWETH.sol new file mode 100644 index 00000000..043fde98 --- /dev/null +++ b/test/utils/mocks/MockWETH.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.29; + +import {MockERC20} from "test/utils/mocks/MockERC20.sol"; +import {IWETH9} from "src/etc/IWETH9.sol"; + +/** + * @title MockWETH + * @dev A mock implementation of WETH9 for testing without fork dependency + * @dev Extends MockERC20 to provide standard ERC20 functionality + */ +contract MockWETH is MockERC20, IWETH9 { + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + constructor() MockERC20("Wrapped Ether", "WETH", 18) {} + + /// @inheritdoc IWETH9 + function deposit() external payable override { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + /// @inheritdoc IWETH9 + function withdraw(uint256 wad) external override { + require(balanceOf(msg.sender) >= wad, "MockWETH: insufficient balance"); + _burn(msg.sender, wad); + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + /// @dev Allow contract to receive ETH + receive() external payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + /// @dev Fallback function calls deposit + fallback() external payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } +} diff --git a/test/etc/zkevm-commonjs-wrapper.js b/test/utils/zkevm-commonjs-wrapper.js similarity index 100% rename from test/etc/zkevm-commonjs-wrapper.js rename to test/utils/zkevm-commonjs-wrapper.js diff --git a/test/yield-exposed-tokens/VbETH.t.sol b/test/yield-exposed-tokens/VbETH.t.sol deleted file mode 100644 index 01c44496..00000000 --- a/test/yield-exposed-tokens/VbETH.t.sol +++ /dev/null @@ -1,457 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity 0.8.29; - -import {VbETH} from "src/vault-bridge-tokens/vbETH/VbETH.sol"; -import {VaultBridgeToken, PausableUpgradeable} from "src/VaultBridgeToken.sol"; -import {ILxLyBridge} from "src/etc/ILxLyBridge.sol"; -import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol"; -import {IWETH9} from "src/etc/IWETH9.sol"; -import { - GenericVaultBridgeTokenTest, - GenericVaultBridgeToken, - VaultBridgeTokenPart2, - IERC20, - SafeERC20 -} from "test/GenericVaultBridgeToken.t.sol"; -import {VaultBridgeTokenInitializer} from "src/VaultBridgeTokenInitializer.sol"; -import {TestVault} from "test/etc/TestVault.sol"; -import {ILxLyBridge as _ILxLyBridge} from "test/interfaces/ILxLyBridge.sol"; -import {WETHNativeConverter} from "src/custom-tokens/WETH/WETHNativeConverter.sol"; - -contract LXLYBridgeMock { - address public gasTokenAddress; - uint32 public gasTokenNetwork; - - function setGasTokenAddress(address _gasTokenAddress) external { - gasTokenAddress = _gasTokenAddress; - } - - function setGasTokenNetwork(uint32 _gasTokenNetwork) external { - gasTokenNetwork = _gasTokenNetwork; - } - - function networkID() external pure returns (uint32) { - return 1; - } - - function wrappedAddressIsNotMintable(address wrappedAddress) external pure returns (bool isNotMintable) { - (wrappedAddress); - return true; - } -} - -contract VbETHTest is GenericVaultBridgeTokenTest { - using SafeERC20 for IERC20; - - VbETH public vbETH; - LXLYBridgeMock public lxlyBridgeMock; - address public morphoVault; - - address constant DUMMY_ADDRESS = 0xAd1490c248c5d3CbAE399Fd529b79B42984277DF; - uint32 constant DUMMY_NETWORK_ID = 2; - - address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - uint32 constant ZKEVM_NETWORK_ID = 1; // zkEVM - - function setUp() public override { - mainnetFork = vm.createSelectFork("mainnet"); - - lxlyBridgeMock = new LXLYBridgeMock(); - asset = WETH; - vbTokenVault = new TestVault(asset); - version = "0.5.0"; - name = "Vault Bridge ETH"; - symbol = "vbETH"; - decimals = 18; - vbTokenMetaData = abi.encode(name, symbol, decimals); - minimumReservePercentage = 1e17; - initializer = address(new VaultBridgeTokenInitializer()); - - vbTokenVault.setMaxDeposit(MAX_DEPOSIT); - vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); - - // Deploy implementation - vbToken = GenericVaultBridgeToken(payable(address(new VbETH()))); - vbTokenImplementation = address(vbToken); - - vbTokenPart2 = new VaultBridgeTokenPart2(); - - stateBeforeInitialize = vm.snapshotState(); - - // prepare calldata - VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ - owner: owner, - name: name, - symbol: symbol, - underlyingToken: asset, - minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), - yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, - minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, - yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) - }); - bytes memory initData = abi.encodeCall(vbETH.initialize, (initializer, initParams)); - - // deploy proxy and initialize implementation - vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbTokenImplementation), address(this), initData))); - vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); - vbETH = VbETH(payable(address(vbToken))); - - // fund the migration manager manually since the test is not using the actual migration manager - deal(asset, migrationManager, 10000000 ether); - vm.prank(migrationManager); - IERC20(asset).forceApprove(address(vbToken), 10000000 ether); - - vm.label(address(vbTokenVault), "WETH Vault"); - vm.label(address(vbToken), "vbETH"); - vm.label(address(vbTokenImplementation), "vbETH Implementation"); - vm.label(address(this), "Default Address"); - vm.label(asset, "Underlying Asset"); - vm.label(migrationManager, "Migration Manager"); - vm.label(owner, "Owner"); - vm.label(recipient, "Recipient"); - vm.label(sender, "Sender"); - vm.label(yieldRecipient, "Yield Recipient"); - vm.label(LXLY_BRIDGE, "Lxly Bridge"); - } - - function test_initialize() public override { - vm.revertToState(stateBeforeInitialize); - - bytes memory initData; - VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ - owner: address(0), - name: name, - symbol: symbol, - underlyingToken: asset, - minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), - yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, - minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, - yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) - }); - - initData = abi.encodeCall(vbToken.initialize, (address(0), initParams)); - vm.expectRevert(VaultBridgeToken.InvalidInitializer.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidOwner.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - vm.revertToState(stateBeforeInitialize); - - initParams.owner = owner; - initParams.name = ""; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidName.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.name = name; - initParams.symbol = ""; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidSymbol.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.symbol = symbol; - initParams.underlyingToken = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - /// forge-config: default.allow_internal_expect_revert = true - vm.expectRevert(VaultBridgeToken.InvalidUnderlyingToken.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.underlyingToken = asset; - initParams.minimumReservePercentage = 1e19; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidMinimumReservePercentage.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.minimumReservePercentage = minimumReservePercentage; - initParams.yieldVault = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidYieldVault.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.yieldVault = address(vbTokenVault); - initParams.yieldRecipient = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidYieldRecipient.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.yieldRecipient = yieldRecipient; - initParams.lxlyBridge = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidLxLyBridge.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.lxlyBridge = address(lxlyBridgeMock); - initParams.migrationManager = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidMigrationManager.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.migrationManager = migrationManager; - initParams.yieldVaultMaximumSlippagePercentage = 1e19; - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidYieldVaultMaximumSlippagePercentage.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - initParams.yieldVaultMaximumSlippagePercentage = YIELD_VAULT_ALLOWED_SLIPPAGE; - initParams.vaultBridgeTokenPart2 = address(0); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VaultBridgeToken.InvalidVaultBridgeTokenPart2.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - lxlyBridgeMock.setGasTokenAddress(address(0)); - lxlyBridgeMock.setGasTokenNetwork(DUMMY_NETWORK_ID); - - initParams.vaultBridgeTokenPart2 = address(vbTokenPart2); - initParams.lxlyBridge = address(lxlyBridgeMock); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VbETH.ContractNotSupportedOnThisNetwork.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - - lxlyBridgeMock.setGasTokenAddress(DUMMY_ADDRESS); - lxlyBridgeMock.setGasTokenNetwork(0); - - initParams.lxlyBridge = address(lxlyBridgeMock); - initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vm.expectRevert(VbETH.ContractNotSupportedOnThisNetwork.selector); - vbToken = GenericVaultBridgeToken(payable(_proxify(vbTokenImplementation, address(this), initData))); - } - - function test_depositWithPermit() public override { - // WETH has no permit function. - } - function test_depositAndBridgePermit() public override { - // WETH has no permit function. - } - - function test_basicFunctions() public view { - assertEq(vbETH.name(), "Vault Bridge ETH"); - assertEq(vbETH.symbol(), "vbETH"); - assertEq(vbETH.asset(), WETH); - } - - function test_depositGasToken(address receiver, uint256 depositAmount) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(vbETH)); - vm.assume(depositAmount > 0 && depositAmount < 100 ether); - - // Get initial balance - uint256 initialReceiverBalance = vbETH.balanceOf(receiver); - - // Deposit ETH - vm.deal(address(this), depositAmount); - uint256 shares = vbETH.depositGasToken{value: depositAmount}(receiver); - - // Verify - assertGt(shares, 0, "Should receive shares for deposit"); - assertEq(vbETH.balanceOf(receiver), initialReceiverBalance + shares, "Receiver should get correct shares"); - } - - function test_depositGasTokenAndBridge(address receiver, uint256 depositAmount) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(vbETH)); - vm.assume(depositAmount > 0 && depositAmount < 100 ether); - - // Deposit ETH - vm.deal(address(this), depositAmount); - uint256 shares = vbETH.depositGasTokenAndBridge{value: depositAmount}(receiver, ZKEVM_NETWORK_ID, true); - - assertGt(shares, 0, "Should receive shares for deposit"); - } - - function test_depositWETH(address receiver, uint256 depositAmount) public { - vm.assume(receiver != address(0)); - vm.assume(receiver != address(vbETH)); - vm.assume(depositAmount > 0 && depositAmount < 100 ether); - - // Deposit ETH - vm.deal(address(this), depositAmount); - - // convert and approve - IWETH9 weth = IWETH9(WETH); - weth.deposit{value: depositAmount}(); - weth.approve(address(vbETH), depositAmount); - - uint256 shares = vbETH.deposit(depositAmount, receiver); - - assertEq(vbETH.balanceOf(receiver), shares, "Receiver should get correct shares"); - } - - function test_mint() public override { - uint256 amount = 1 ether; - vm.deal(address(this), amount + 1 ether); - - uint256 initialBalance = IWETH9(WETH).balanceOf(address(this)); - - // sending a bit more to test refund func - vbETH.mintWithGasToken{value: amount + 1 ether}(amount, address(this)); - - // check refund - assertEq(IWETH9(WETH).balanceOf(address(this)), initialBalance + 1 ether); - - assertEq(vbETH.balanceOf(address(this)), amount); // shares minted to the sender - assertApproxEqAbs(vbETH.totalAssets(), amount, 2); // allow for rounding - - uint256 reserveAmount = _calculateReserveAssets(amount, vbTokenVault.maxDeposit(address(vbToken))); - assertApproxEqAbs(vbETH.reservedAssets(), reserveAmount, 2); // allow for rounding - } - - function test_withdraw_from_reserve() public override { - uint256 amount = 1 ether; - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); - - // Deposit ETH - vm.deal(address(this), amount); - uint256 shares = vbETH.depositGasToken{value: amount}(address(this)); - assertEq(vbETH.balanceOf(address(this)), shares); // sender gets 100 shares - - uint256 reserveAssetsAfterDeposit = _calculateReserveAssets(amount, vaultMaxDeposit); - - uint256 reserveWithdrawAmount = (reserveAssetsAfterDeposit * 90) / 100; // withdraw 90% of reserve assets - uint256 reserveAfterWithdraw = reserveAssetsAfterDeposit - reserveWithdrawAmount; - - uint256 initialBalance = IWETH9(WETH).balanceOf(address(this)); - - vm.expectEmit(); - emit IERC4626.Withdraw( - address(this), address(this), address(this), reserveWithdrawAmount, reserveWithdrawAmount - ); - vbETH.withdraw(reserveWithdrawAmount, address(this), address(this)); - assertEq(IWETH9(WETH).balanceOf(address(vbETH)), reserveAfterWithdraw); // reserve assets reduced - assertEq(IWETH9(WETH).balanceOf(address(this)), initialBalance + reserveWithdrawAmount); // assets returned to sender - assertEq(vbETH.balanceOf(address(this)), amount - reserveWithdrawAmount); // shares reduced - } - - function test_withdraw_from_stake() public override { - uint256 amount = 1 ether; - - // Deposit ETH - vm.deal(address(this), amount); - uint256 shares = vbETH.depositGasToken{value: amount}(address(this)); - assertEq(vbETH.balanceOf(address(this)), shares); // sender gets 100 shares - - uint256 amountToWithdraw = amount - 1; - uint256 initialBalance = IWETH9(WETH).balanceOf(address(this)); - - vm.expectEmit(); - emit IERC4626.Withdraw(address(this), address(this), address(this), amountToWithdraw, amountToWithdraw); - vbToken.withdraw(amountToWithdraw, address(this), address(this)); - assertEq(IWETH9(WETH).balanceOf(address(vbETH)), 0); // reserve assets reduced - assertEq(IWETH9(WETH).balanceOf(address(this)), initialBalance + amountToWithdraw); // assets returned to sender - assertEq(vbETH.balanceOf(address(this)), amount - amountToWithdraw); // shares reduced - } - - function test_completeMigration_CUSTOM_no_discrepancy() public { - uint256 assets = 100 ether; - uint256 shares = 100 ether; - - // make sure the assets is less than the max deposit limit - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); - if (assets > vaultMaxDeposit) { - assets = vaultMaxDeposit / 2; - shares = vaultMaxDeposit / 2; - } - - bytes memory callData = abi.encodeCall(vbTokenPart2.completeMigration, (NETWORK_ID_L2, shares, assets)); - _testPauseUnpause(owner, address(vbETH), callData); - - deal(address(vbToken), assets); - - vm.expectRevert(VaultBridgeToken.Unauthorized.selector); - vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, assets); - - vm.startPrank(migrationManager); - - vm.expectRevert(VaultBridgeToken.InvalidOriginNetwork.selector); - vbTokenPart2.completeMigration(NETWORK_ID_L1, 0, assets); - - vm.expectRevert(VaultBridgeToken.InvalidShares.selector); - vbTokenPart2.completeMigration(NETWORK_ID_L2, 0, assets); - - uint256 stakedAssetsBefore = vbToken.stakedAssets(); - - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_ASSET, - NETWORK_ID_L1, - address(vbToken), - NETWORK_ID_L2, - address(0), - shares, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() - ); - vm.expectEmit(); - emit IERC4626.Deposit(migrationManager, address(vbToken), assets, shares); - vm.expectEmit(); - emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, assets, 0); - vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, assets); - - vm.stopPrank(); - - assertEq( - vbToken.reservedAssets(), - vbToken.convertToAssets(shares) * minimumReservePercentage / MAX_RESERVE_PERCENTAGE - ); - assertGt(vbToken.stakedAssets(), stakedAssetsBefore); - } - - function test_completeMigration_CUSTOM_with_discrepancy() public { - uint256 assets = 100 ether; - uint256 shares = 110 ether; - - // make sure the assets is less than the max deposit limit - uint256 vaultMaxDeposit = vbTokenVault.maxDeposit(address(vbToken)); - if (assets > vaultMaxDeposit) { - assets = vaultMaxDeposit / 2; - shares = (vaultMaxDeposit / 2) + 10; - } - - deal(address(vbToken), assets); - - vm.expectRevert(abi.encodeWithSelector(VaultBridgeToken.CannotCompleteMigration.selector, shares, assets, 0)); - vm.prank(migrationManager); - vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, assets); - - // fund the migration fees - deal(asset, address(this), assets); - IERC20(asset).forceApprove(address(vbToken), assets); - vm.expectEmit(); - emit VaultBridgeToken.DonatedForCompletingMigration(address(this), assets); - vbTokenPart2.donateForCompletingMigration(assets); - - uint256 stakedAssetsBefore = vbToken.stakedAssets(); - - vm.expectEmit(); - emit BridgeEvent( - LEAF_TYPE_ASSET, - NETWORK_ID_L1, - address(vbToken), - NETWORK_ID_L2, - address(0), - shares, - vbTokenMetaData, - _ILxLyBridge(LXLY_BRIDGE).depositCount() - ); - vm.expectEmit(); - emit IERC4626.Deposit(migrationManager, address(vbToken), assets, shares); - vm.expectEmit(); - emit VaultBridgeToken.MigrationCompleted(NETWORK_ID_L2, shares, assets, shares - assets); - vm.prank(migrationManager); - vbTokenPart2.completeMigration(NETWORK_ID_L2, shares, assets); - - assertEq( - vbToken.reservedAssets(), - vbToken.convertToAssets(shares) * minimumReservePercentage / MAX_RESERVE_PERCENTAGE - ); - assertGt(vbToken.stakedAssets(), stakedAssetsBefore); - } -} diff --git a/test/yield-exposed-tokens/VbUSDT.t.sol b/test/yield-exposed-tokens/VbUSDT.t.sol deleted file mode 100644 index f0e77b43..00000000 --- a/test/yield-exposed-tokens/VbUSDT.t.sol +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-PolygonLabs-Open-Attribution OR LicenseRef-PolygonLabs-Source-Available -pragma solidity ^0.8.29; - -import {VaultBridgeToken} from "src/VaultBridgeToken.sol"; -import {TestVault} from "test/etc/TestVault.sol"; -import { - IERC20, - SafeERC20, - GenericVaultBridgeTokenTest, - GenericVaultBridgeToken, - VaultBridgeTokenPart2, - stdStorage, - StdStorage -} from "test/GenericVaultBridgeToken.t.sol"; -import {VaultBridgeTokenInitializer} from "src/VaultBridgeTokenInitializer.sol"; - -contract VbUSDTTest is GenericVaultBridgeTokenTest { - using SafeERC20 for IERC20; - using stdStorage for StdStorage; - - address internal constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; - - GenericVaultBridgeToken vbUSDT; - - function setUp() public override { - mainnetFork = vm.createSelectFork("mainnet"); - - asset = USDT; - vbTokenVault = new TestVault(asset); - version = "0.5.0"; - name = "Vault USDT"; - symbol = "vbUSDT"; - decimals = 6; - vbTokenMetaData = abi.encode(name, symbol, decimals); - minimumReservePercentage = 1e17; - initializer = address(new VaultBridgeTokenInitializer()); - - vbTokenVault.setMaxDeposit(MAX_DEPOSIT); - vbTokenVault.setMaxWithdraw(MAX_WITHDRAW); - - vbTokenPart2 = new VaultBridgeTokenPart2(); - - vbToken = GenericVaultBridgeToken(payable(address(new GenericVaultBridgeToken()))); - vbTokenImplementation = address(vbToken); - stateBeforeInitialize = vm.snapshotState(); - VaultBridgeToken.InitializationParameters memory initParams = VaultBridgeToken.InitializationParameters({ - owner: owner, - name: name, - symbol: symbol, - underlyingToken: asset, - minimumReservePercentage: minimumReservePercentage, - yieldVault: address(vbTokenVault), - yieldRecipient: yieldRecipient, - lxlyBridge: LXLY_BRIDGE, - minimumYieldVaultDeposit: MINIMUM_YIELD_VAULT_DEPOSIT, - migrationManager: migrationManager, - yieldVaultMaximumSlippagePercentage: YIELD_VAULT_ALLOWED_SLIPPAGE, - vaultBridgeTokenPart2: address(vbTokenPart2) - }); - bytes memory initData = abi.encodeCall(vbToken.initialize, (initializer, initParams)); - vbToken = GenericVaultBridgeToken(payable(_proxify(address(vbToken), address(this), initData))); - vbTokenPart2 = VaultBridgeTokenPart2(payable(address(vbToken))); - vbUSDT = GenericVaultBridgeToken(payable(address(vbToken))); - - // fund the migration manager manually since the test is not using the actual migration manager - deal(asset, migrationManager, 10000000 ether); - vm.prank(migrationManager); - IERC20(asset).forceApprove(address(vbToken), 10000000 ether); - - vm.label(address(vbTokenVault), "USDT Vault"); - vm.label(address(vbToken), "vbUSDT"); - vm.label(address(vbTokenImplementation), "vbUSDT Implementation"); - vm.label(address(this), "Default Address"); - vm.label(asset, "Underlying Asset"); - vm.label(migrationManager, "Migration Manager"); - vm.label(owner, "Owner"); - vm.label(recipient, "Recipient"); - vm.label(sender, "Sender"); - vm.label(yieldRecipient, "Yield Recipient"); - vm.label(LXLY_BRIDGE, "Lxly Bridge"); - } - - function test_depositWithPermit() public override { - // USDT has no permit function. - } - function test_depositAndBridgePermit() public override { - // USDT has no permit function. - } -}