diff --git a/.solhint.json b/.solhint.json index 14f780e..c956bd5 100644 --- a/.solhint.json +++ b/.solhint.json @@ -2,10 +2,9 @@ "extends": "solhint:recommended", "rules": { "code-complexity": ["error", 8], - "compiler-version": ["error", ">=0.8.25"], + "compiler-version": ["error", ">=0.8.0"], "func-name-mixedcase": "off", "func-visibility": ["error", { "ignoreConstructors": true }], - "max-line-length": ["error", 120], "named-parameters-mapping": "warn", "no-console": "off", "not-rely-on-time": "off", diff --git a/README.md b/README.md index 2b03fae..5836713 100644 --- a/README.md +++ b/README.md @@ -9,16 +9,19 @@ [license]: https://opensource.org/licenses/MIT [license-badge]: https://img.shields.io/badge/License-MIT-blue.svg -This repo is the LSP7 version of the [`HypERC20`] and [`HypERC20Collateral`] of `@hyperlane-xyz/core` package. They are used to bridge tokens between the Ethereum and LUKSO chains using the [Hashi Bridge](https://crosschain-alliance.gitbook.io/hashi). +This repo is the LSP7 version of the [`HypERC20`] and [`HypERC20Collateral`] of `@hyperlane-xyz/core` package. They are +used to bridge tokens between the Ethereum and LUKSO chains using the +[Hashi Bridge](https://crosschain-alliance.gitbook.io/hashi). For more details on the **architecture and bridging flow**, see the [**`docs/`**](./docs/README.md) folder. ### Examples of bridged tokens -- ETH -> LUKSO: https://explorer.hyperlane.xyz/message/0x53a383e32fdb68748c8af5c86be3669e58eadc377db2a9f420826cb9474dd55c - -- LUKSO -> ETH: https://explorer.hyperlane.xyz/message/0xf9c86a22e7b5584fc87a9d4ffc39f967a8745cd28b98ed2eaeb220c43996c4ca +- ETH -> LUKSO: + https://explorer.hyperlane.xyz/message/0x53a383e32fdb68748c8af5c86be3669e58eadc377db2a9f420826cb9474dd55c +- LUKSO -> ETH: + https://explorer.hyperlane.xyz/message/0xf9c86a22e7b5584fc87a9d4ffc39f967a8745cd28b98ed2eaeb220c43996c4ca ## Getting Started @@ -45,7 +48,6 @@ This is how to install dependencies: Note that OpenZeppelin Contracts is pre-installed, so you can follow that as an example. - ### Sensible Defaults This template comes with a set of sensible default configurations for you to use. These defaults can be found in the @@ -67,7 +69,6 @@ This is a list of the most frequently needed commands. ### Build & Compile - ```sh # Build the contracts: forge build @@ -100,18 +101,21 @@ bun run test:coverage:report ### GitHub Actions -This repository uses pre-configured GitHub Actions. The contracts are linted and tested on every push and pull requests. You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). - +This repository uses pre-configured GitHub Actions. The contracts are linted and tested on every push and pull requests. +You can edit the CI script in [.github/workflows/ci.yml](./.github/workflows/ci.yml). ## Foundry Resources -This template builds upon the frameworks and libraries mentioned above, so please consult their respective documentation for details about their specific features. +This template builds upon the frameworks and libraries mentioned above, so please consult their respective documentation +for details about their specific features. For example, if you're interested in exploring Foundry in more detail, you should look at the [Foundry Book](https://book.getfoundry.sh/). In particular, you may be interested in reading the [Writing Tests](https://book.getfoundry.sh/forge/writing-tests.html) tutorial. - -[`HypERC20Collateral`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20Collateral.sol -[`HypERC20`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20.sol -[`Mailbox`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/Mailbox.sol +[`HypERC20Collateral`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20Collateral.sol +[`HypERC20`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20.sol +[`Mailbox`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/Mailbox.sol diff --git a/docs/README.md b/docs/README.md index 5a55e26..acef484 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,9 +23,11 @@ graph TD end ``` -**scenario 2:** the token was migrated from LUKSO to Ethereum and an HypERC20 token contract was created as a wrapper on the Ethereum side (_e.g: wrapped Chillwhale or wrapped FABS as HypERC20_). +**scenario 2:** the token was migrated from LUKSO to Ethereum and an HypERC20 token contract was created as a wrapper on +the Ethereum side (_e.g: wrapped Chillwhale or wrapped FABS as HypERC20_). -The user burn the wrapped token `HypERC20` on Ethereum, and the tokens are unlocked on the LUKSO side and transferred to the user. +The user burns the wrapped token `HypERC20` on Ethereum, and the tokens are unlocked on the LUKSO side and transferred +to the user. ```mermaid %% Ethereum -> LUKSO - LSP7 token that was initially bridged from LUKSO @@ -45,7 +47,8 @@ graph TD - **scenario 3:** the LSP7 token was originally created and deployed on LUKSO (_e.g: Chillwhale, FABS, etc..._). -The user transfer the LSP7 token to its `HypLSP7Collateral` contract on LUKSO where it is locked. The HypERC20 token on Ethereum is then minted for the user. +The user transfers the LSP7 token to its `HypLSP7Collateral` contract on LUKSO where it is locked. The HypERC20 token on +Ethereum is then minted for the user. ```mermaid graph TD @@ -61,7 +64,8 @@ graph TD end ``` -- **scenario 4:** an ERC20 token was bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g: wrapped DAI as HypLSP7_). +- **scenario 4:** an ERC20 token was bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g: + wrapped DAI as HypLSP7_). This HypLSP7 token is burnt on LUKSO, on Ethereum it is unlocked. @@ -79,48 +83,58 @@ graph TD end ``` - ## Detailed Architecture Diagrams > **Notes:** in the architecture diagram below: +> > - The `Yaho` contracts handle the dispatching and batching of messages across chains. -> - The `Yaru` contracts ensures that the messages are properly executed on the destination chain by calling relevant functions like `onMessage`. +> - The `Yaru` contracts ensures that the messages are properly executed on the destination chain by calling relevant +> functions like `onMessage`. For more infos, see the +> [**Key Contracts**](https://crosschain-alliance.gitbook.io/hashi/api-and-smart-contracts/key-contracts) section on +> the Hashi Alliance docs. ### Ethereum -> LUKSO -> **Note:** This detailed diagram corresponds to the [**scenario 1**](#ethereum---lukso) above. Where an ERC20 token that initially exists on Ethereum (_e.g: DAI, USDC, etc..._) is bridged to LUKSO. +> **Note:** This detailed diagram corresponds to the [**scenario 1**](#ethereum---lukso) above. Where an ERC20 token +> that initially exists on Ethereum (_e.g: DAI, USDC, etc..._) is bridged to LUKSO. ![Ethereum to LUKSO bridge flow](../assets/flow-ethereum-lukso-hashi-bridge.png) - **on Ethereum chain** 1. User transfer ERC20 tokens to [`HypERC20Collateral`]. This locks the tokens in the collateral contract. -2. `HypERC20Collateral` contract call [`Mailbox`] to pass the message. +2. `HypERC20Collateral` contract calls [`Mailbox`] to pass the message via the `transferRemote(...)` function. + (Internally, the functions `__Router_dispatch(..) -> mailbox.dispatch(...)` are called to dispatch the message to the + mailbox). 3. The `Mailbox` calls: - 3.1. the default Hook (created by Hyperlane), - 3.2. and the Hashi Hook (created by CCIA team). 4. Hashi Hook dispatch the token relaying message from `Yaho` contracts. - - **Off chain** -5. Hashi relayer (managed by CCIA team) listen for events from `Yaho` contracts and request the reporter contracts to relay token relaying message. -6. Hashi executor (managed by CCIA team) listen to event from each Hashi adapter contracts and call `Yaru.executeMessages`. **This step checks whether the Hashi adapters agree on a specify message id** (a threshold number of hash is stored), and set the message Id to verified status. +5. Hashi relayer (managed by CCIA team) listen for events from `Yaho` contracts and request the reporter contracts to + relay token relaying message. +6. Hashi executor (managed by CCIA team) listen to event from each Hashi adapter contracts and call + `Yaru.executeMessages`. **This step checks whether the Hashi adapters agree on a specify message id** (a threshold + number of hash is stored), and set the message Id to verified status. 7. Validator (run by Hyperlane & LUKSO team) will sign the Merkle root when new dispatches happen in Mailbox. -8. Hyperlane relayer (run by Hyperlane team) relays the message by calling Mailbox.process(). +8. Hyperlane relayer (run by Hyperlane team) relays the message by calling `Mailbox.process(...)`. **on LUKSO chain** -8. When [`Mailbox.process(...)`](https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/3d116132b87d36af9576d6b116f31a53d680db4a/solidity/contracts/Mailbox.sol#L188-L197) is called, it will: - - 8.1. check with Multisig ISM (includes Hashi ISM), whether the message is signed by validators & verified by Hashi ISM. - - 8.2. If so, it will mint [HypLSP7](./src/HypLSP7.sol) tokens to the receiver. +8. When + [`Mailbox.process(...)`](https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/3d116132b87d36af9576d6b116f31a53d680db4a/solidity/contracts/Mailbox.sol#L188-L197) + is called, it will: +- 8.1. check with Multisig ISM (includes Hashi ISM), whether the message is signed by validators & verified by Hashi + ISM. +- 8.2. If so, it will mint [HypLSP7](./src/HypLSP7.sol) tokens to the receiver. ### LUKSO -> Ethereum -> **Note:** This detailed diagram corresponds to the [**scenario 4**](#lukso---ethereum) above. Where an ERC20 token was bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g: wrapped DAI as HypLSP7_). +> **Note:** This detailed diagram corresponds to the [**scenario 4**](#lukso---ethereum) above. Where an ERC20 token was +> bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g: wrapped DAI as HypLSP7_). ![LUKSO to Ethereum bridge flow](../assets/flow-lukso-ethereum-hashi-bridge.png) @@ -128,31 +142,31 @@ graph TD > _Step 1 to 3 needs to be confirmed_ -1. User transfer LSP7 token to HypLSP7 contract and the tokens are burnt. +1. User transfers LSP7 token to HypLSP7 contract and the tokens are burnt. 2. HypLSP7 contract calls `Mailbox` to pass the message. 3. `Mailbox` calls Default Hook (created by Hyperlane) and Hashi Hook (created by CCIA team). -4. Hashi Hook dispatch the token relaying message from Yaho contracts. +4. Hashi Hook dispatches the token relaying message from Yaho contracts. **Off chain** -4. Off chain process remains the same as before, _except there is no Light Client support for Hashi from LUKSO → Ethereum_. +4. Off chain process remains the same as before, _except there is no Light Client support for Hashi from LUKSO → + Ethereum_. **on Ethereum chain** 5. When `Mailbox.process()` is called: - - 5.1. it will check with Multisig ISM (includes Hashi ISM), whether the message is signed by validators & verified by Hashi ISM. + - 5.1. it will check with Multisig ISM (includes Hashi ISM), whether the message is signed by validators & verified + by Hashi ISM. - 5.2. If so, it will unlock ERC20 token to the receiver on the Ethereum chain. - - - - - ## Relevant links & resources - [Cross Chain Alliance - Hashi](https://crosschain-alliance.gitbook.io/hashi) - [Hyperlane smart contracts monorepo](https://github.com/hyperlane-xyz/hyperlane-monorepo) -[`HypERC20Collateral`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20Collateral.sol -[`HypERC20`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20.sol -[`Mailbox`]: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/Mailbox.sol +[`HypERC20Collateral`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20Collateral.sol +[`HypERC20`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20.sol +[`Mailbox`]: + https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/Mailbox.sol diff --git a/foundry.toml b/foundry.toml index dacd6da..6746770 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,55 +1,55 @@ # Full reference https://github.com/foundry-rs/foundry/tree/master/crates/config [profile.default] - auto_detect_solc = false - block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT - bytecode_hash = "none" - evm_version = "shanghai" - fuzz = { runs = 1_000 } - gas_reports = ["*"] - optimizer = true - optimizer_runs = 10_000 - out = "out" - script = "script" - solc = "0.8.25" - src = "src" - test = "test" +auto_detect_solc = false +block_timestamp = 1_680_220_800 # March 31, 2023 at 00:00 GMT +bytecode_hash = "none" +evm_version = "shanghai" +fuzz = { runs = 1_000 } +gas_reports = ["*"] +optimizer = true +optimizer_runs = 10_000 +out = "out" +script = "script" +solc = "0.8.25" +src = "src" +test = "test" [profile.ci] - fuzz = { runs = 10_000 } - verbosity = 4 +fuzz = { runs = 10_000 } +verbosity = 4 [etherscan] - arbitrum = { key = "${API_KEY_ARBISCAN}" } - avalanche = { key = "${API_KEY_SNOWTRACE}" } - base = { key = "${API_KEY_BASESCAN}" } - bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } - gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } - goerli = { key = "${API_KEY_ETHERSCAN}" } - mainnet = { key = "${API_KEY_ETHERSCAN}" } - optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } - polygon = { key = "${API_KEY_POLYGONSCAN}" } - sepolia = { key = "${API_KEY_ETHERSCAN}" } +arbitrum = { key = "${API_KEY_ARBISCAN}" } +avalanche = { key = "${API_KEY_SNOWTRACE}" } +base = { key = "${API_KEY_BASESCAN}" } +bnb_smart_chain = { key = "${API_KEY_BSCSCAN}" } +gnosis_chain = { key = "${API_KEY_GNOSISSCAN}" } +goerli = { key = "${API_KEY_ETHERSCAN}" } +mainnet = { key = "${API_KEY_ETHERSCAN}" } +optimism = { key = "${API_KEY_OPTIMISTIC_ETHERSCAN}" } +polygon = { key = "${API_KEY_POLYGONSCAN}" } +sepolia = { key = "${API_KEY_ETHERSCAN}" } [fmt] - bracket_spacing = true - int_types = "long" - line_length = 120 - multiline_func_header = "all" - number_underscore = "thousands" - quote_style = "double" - tab_width = 4 - wrap_comments = true +bracket_spacing = true +int_types = "long" +line_length = 120 +multiline_func_header = "all" +number_underscore = "thousands" +quote_style = "double" +tab_width = 4 +wrap_comments = true [rpc_endpoints] - arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" - avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" - base = "https://mainnet.base.org" - bnb_smart_chain = "https://bsc-dataseed.binance.org" - gnosis_chain = "https://rpc.gnosischain.com" - goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" - localhost = "http://localhost:8545" - mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" - optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" - polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" - sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" +arbitrum = "https://arbitrum-mainnet.infura.io/v3/${API_KEY_INFURA}" +avalanche = "https://avalanche-mainnet.infura.io/v3/${API_KEY_INFURA}" +base = "https://mainnet.base.org" +bnb_smart_chain = "https://bsc-dataseed.binance.org" +gnosis_chain = "https://rpc.gnosischain.com" +goerli = "https://goerli.infura.io/v3/${API_KEY_INFURA}" +localhost = "http://localhost:8545" +mainnet = "https://eth-mainnet.g.alchemy.com/v2/${API_KEY_ALCHEMY}" +optimism = "https://optimism-mainnet.infura.io/v3/${API_KEY_INFURA}" +polygon = "https://polygon-mainnet.infura.io/v3/${API_KEY_INFURA}" +sepolia = "https://sepolia.infura.io/v3/${API_KEY_INFURA}" diff --git a/lib/forge-std b/lib/forge-std index 1714bee..8f24d6b 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/remappings.txt b/remappings.txt index e59c580..4b7447e 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ @/=node_modules/@ -forge-std/=node_modules/forge-std/ +forge-std/=node_modules/forge-std diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol deleted file mode 100644 index 22f5f9c..0000000 --- a/script/Deploy.s.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity >=0.8.25 <0.9.0; - - -import { BaseScript } from "./Base.s.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract Deploy is BaseScript { - // function run() public broadcast returns (Foo foo) { - // foo = new Foo(); - // } -} diff --git a/test/HypLSP7.t.sol b/test/HypLSP7.t.sol new file mode 100644 index 0000000..6acfdc5 --- /dev/null +++ b/test/HypLSP7.t.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +import { Test } from "forge-std/src/Test.sol"; + +/// Hyperlane testing environnement +/// @dev See https://docs.hyperlane.xyz/docs/guides/developer-tips/unit-testing +import { TypeCasts } from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol"; +import { TestMailbox } from "@hyperlane-xyz/core/contracts/test/TestMailbox.sol"; +import { TestPostDispatchHook } from "@hyperlane-xyz/core/contracts/test/TestPostDispatchHook.sol"; +import { TestInterchainGasPaymaster } from "@hyperlane-xyz/core/contracts/test/TestInterchainGasPaymaster.sol"; +import { GasRouter } from "@hyperlane-xyz/core/contracts/client/GasRouter.sol"; +import { HypNative } from "@hyperlane-xyz/core/contracts/token/HypNative.sol"; +import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol"; + +// Mocks + contracts to test +import { LSP7Mock } from "./LSP7Mock.sol"; +import { HypLSP7 } from "../src/HypLSP7.sol"; +import { HypLSP7Collateral } from "../src/HypLSP7Collateral.sol"; + +abstract contract HypTokenTest is Test { + using TypeCasts for address; + + uint32 internal constant ORIGIN = 11; + uint32 internal constant DESTINATION = 12; + uint8 internal constant DECIMALS = 18; + uint256 internal constant TOTAL_SUPPLY = 1_000_000e18; + uint256 internal constant GAS_LIMIT = 10_000; + uint256 internal constant TRANSFER_AMOUNT = 100e18; + string internal constant NAME = "HyperlaneInu"; + string internal constant SYMBOL = "HYP"; + + address internal ALICE = makeAddr("alice"); + address internal BOB = makeAddr("bob"); + address internal OWNER = makeAddr("owner"); + uint256 internal REQUIRED_VALUE; // initialized in setUp + + LSP7Mock internal primaryToken; + TokenRouter internal localToken; + HypLSP7 internal remoteToken; + TestMailbox internal localMailbox; + TestMailbox internal remoteMailbox; + TestPostDispatchHook internal noopHook; + TestInterchainGasPaymaster internal igp; + + event SentTransferRemote(uint32 indexed destination, bytes32 indexed recipient, uint256 amount); + + event ReceivedTransferRemote(uint32 indexed origin, bytes32 indexed recipient, uint256 amount); + + function setUp() public virtual { + localMailbox = new TestMailbox(ORIGIN); + remoteMailbox = new TestMailbox(DESTINATION); + + primaryToken = new LSP7Mock(NAME, SYMBOL, address(this), TOTAL_SUPPLY); + + noopHook = new TestPostDispatchHook(); + localMailbox.setDefaultHook(address(noopHook)); + localMailbox.setRequiredHook(address(noopHook)); + + REQUIRED_VALUE = noopHook.quoteDispatch("", ""); + + remoteToken = new HypLSP7(DECIMALS, address(remoteMailbox)); + + remoteToken.initialize(TOTAL_SUPPLY, NAME, SYMBOL, address(noopHook), address(0), OWNER); + + vm.prank(OWNER); + remoteToken.enrollRemoteRouter(ORIGIN, address(localToken).addressToBytes32()); + + igp = new TestInterchainGasPaymaster(); + + vm.deal(ALICE, 125_000); + } + + function _enrollRemoteTokenRouter() internal { + vm.prank(OWNER); + remoteToken.enrollRemoteRouter(ORIGIN, address(localToken).addressToBytes32()); + } + + function _expectRemoteBalance(address _user, uint256 _balance) internal view { + assertEq(remoteToken.balanceOf(_user), _balance); + } + + function _processTransfers(address _recipient, uint256 _amount) internal { + vm.prank(address(remoteMailbox)); + remoteToken.handle( + ORIGIN, address(localToken).addressToBytes32(), abi.encodePacked(_recipient.addressToBytes32(), _amount) + ); + } + + function _setCustomGasConfig() internal { + vm.prank(OWNER); + localToken.setHook(address(igp)); + + TokenRouter.GasRouterConfig[] memory config = new TokenRouter.GasRouterConfig[](1); + config[0] = GasRouter.GasRouterConfig({ domain: DESTINATION, gas: GAS_LIMIT }); + + vm.prank(OWNER); + localToken.setDestinationGas(config); + } + + function _performRemoteTransfer(uint256 _msgValue, uint256 _amount) internal { + vm.prank(ALICE); + + localToken.transferRemote{ value: _msgValue }(DESTINATION, BOB.addressToBytes32(), _amount); + + vm.expectEmit(true, true, false, true); + + emit ReceivedTransferRemote(ORIGIN, BOB.addressToBytes32(), _amount); + _processTransfers(BOB, _amount); + + assertEq(remoteToken.balanceOf(BOB), _amount); + } + + function _performRemoteTransferAndGas(uint256 _msgValue, uint256 _amount, uint256 _gasOverhead) internal { + uint256 ethBalance = ALICE.balance; + + _performRemoteTransfer(_msgValue + _gasOverhead, _amount); + + assertEq(ALICE.balance, ethBalance - REQUIRED_VALUE - _gasOverhead); + } + + function _performRemoteTransferWithEmit(uint256 _msgValue, uint256 _amount, uint256 _gasOverhead) internal { + vm.expectEmit(true, true, false, true); + emit SentTransferRemote(DESTINATION, BOB.addressToBytes32(), _amount); + _performRemoteTransferAndGas(_msgValue, _amount, _gasOverhead); + } + + function testBenchmark_overheadGasUsage() public { + vm.prank(address(localMailbox)); + + uint256 gasBefore = gasleft(); + localToken.handle( + DESTINATION, + address(remoteToken).addressToBytes32(), + abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMOUNT) + ); + uint256 gasAfter = gasleft(); + } +} + +contract HypLSP7Test is HypTokenTest { + using TypeCasts for address; + + HypLSP7 internal lsp7Token; + + function setUp() public override { + super.setUp(); + + localToken = new HypLSP7(DECIMALS, address(localMailbox)); + lsp7Token = HypLSP7(payable(address(localToken))); + + vm.prank(OWNER); + lsp7Token.initialize(TOTAL_SUPPLY, NAME, SYMBOL, address(noopHook), address(0), OWNER); + + vm.prank(OWNER); + lsp7Token.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); + + // from, to, amount, force, data + vm.prank(OWNER); + lsp7Token.transfer(OWNER, ALICE, 1000e18, true, ""); + + _enrollRemoteTokenRouter(); + } + + function testInitialize_revert_ifAlreadyInitialized() public { + vm.expectRevert("Initializable: contract is already initialized"); + lsp7Token.initialize(TOTAL_SUPPLY, NAME, SYMBOL, address(noopHook), address(0), OWNER); + } + + function testTotalSupply() public view { + assertEq(lsp7Token.totalSupply(), TOTAL_SUPPLY); + } + + function testDecimals() public view { + assertEq(lsp7Token.decimals(), DECIMALS); + } + + function testLocalTransfers() public { + assertEq(lsp7Token.balanceOf(ALICE), 1000e18); + assertEq(lsp7Token.balanceOf(BOB), 0); + + vm.prank(ALICE); + lsp7Token.transfer(ALICE, BOB, 100e18, true, ""); + assertEq(lsp7Token.balanceOf(ALICE), 900e18); + assertEq(lsp7Token.balanceOf(BOB), 100e18); + } + + function testRemoteTransfer() public { + vm.prank(OWNER); + remoteToken.enrollRemoteRouter(ORIGIN, address(localToken).addressToBytes32()); + uint256 balanceBefore = lsp7Token.balanceOf(ALICE); + + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMOUNT, 0); + assertEq(lsp7Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMOUNT); + } + + function testRemoteTransfer_invalidAmount() public { + vm.expectRevert(); + _performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMOUNT * 11); + assertEq(lsp7Token.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + uint256 balanceBefore = lsp7Token.balanceOf(ALICE); + + _performRemoteTransferAndGas(REQUIRED_VALUE, TRANSFER_AMOUNT, GAS_LIMIT * igp.gasPrice()); + + assertEq(lsp7Token.balanceOf(ALICE), balanceBefore - TRANSFER_AMOUNT); + } +} + +contract HypLSP7CollateralTest is HypTokenTest { + using TypeCasts for address; + + HypLSP7Collateral internal lsp7Collateral; + + function setUp() public override { + super.setUp(); + + localToken = new HypLSP7Collateral(address(primaryToken), address(localMailbox)); + + lsp7Collateral = HypLSP7Collateral(address(localToken)); + + lsp7Collateral.initialize(address(noopHook), address(0), OWNER); + + vm.prank(OWNER); + lsp7Collateral.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); + + primaryToken.transfer(address(this), address(localToken), 1000e18, true, ""); + + primaryToken.transfer(address(this), ALICE, 1000e18, true, ""); + + _enrollRemoteTokenRouter(); + } + + function testRemoteTransfer() public { + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.authorizeOperator(address(localToken), TRANSFER_AMOUNT, ""); + + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMOUNT, 0); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMOUNT); + } + + function testRemoteTransfer_invalidAllowance() public { + vm.expectRevert(); + _performRemoteTransfer(REQUIRED_VALUE, TRANSFER_AMOUNT); + assertEq(localToken.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + uint256 balanceBefore = localToken.balanceOf(ALICE); + + vm.prank(ALICE); + primaryToken.authorizeOperator(address(localToken), TRANSFER_AMOUNT, ""); + _performRemoteTransferAndGas(REQUIRED_VALUE, TRANSFER_AMOUNT, GAS_LIMIT * igp.gasPrice()); + assertEq(localToken.balanceOf(ALICE), balanceBefore - TRANSFER_AMOUNT); + } +} + +contract HypNativeTest is HypTokenTest { + using TypeCasts for address; + + HypNative internal nativeToken; + + function setUp() public override { + super.setUp(); + + localToken = new HypNative(address(localMailbox)); + nativeToken = HypNative(payable(address(localToken))); + + nativeToken.initialize(address(noopHook), address(0), OWNER); + + vm.prank(OWNER); + nativeToken.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); + + vm.deal(address(localToken), 1000e18); + vm.deal(ALICE, 1000e18); + + _enrollRemoteTokenRouter(); + } + + function testRemoteTransfer() public { + _performRemoteTransferWithEmit(REQUIRED_VALUE, TRANSFER_AMOUNT, TRANSFER_AMOUNT); + } + + function testRemoteTransfer_invalidAmount() public { + vm.expectRevert("Native: amount exceeds msg.value"); + _performRemoteTransfer(REQUIRED_VALUE + TRANSFER_AMOUNT, TRANSFER_AMOUNT * 10); + assertEq(localToken.balanceOf(ALICE), 1000e18); + } + + function testRemoteTransfer_withCustomGasConfig() public { + _setCustomGasConfig(); + + _performRemoteTransferAndGas(REQUIRED_VALUE, TRANSFER_AMOUNT, TRANSFER_AMOUNT + GAS_LIMIT * igp.gasPrice()); + } +} diff --git a/test/LSP7Mock.sol b/test/LSP7Mock.sol new file mode 100644 index 0000000..db0cce2 --- /dev/null +++ b/test/LSP7Mock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +import { LSP7DigitalAsset } from "@lukso/lsp7-contracts/contracts/LSP7DigitalAsset.sol"; + +contract LSP7Mock is LSP7DigitalAsset { + constructor( + string memory name_, + string memory symbol_, + address initialAccount_, + uint256 initialBalance_ + ) + LSP7DigitalAsset(name_, symbol_, initialAccount_, 0, false) + { + _mint(initialAccount_, initialBalance_, true, ""); + } + + function mint(uint256 amount) public { + _mint(msg.sender, amount, true, ""); + } + + function mintTo(address account, uint256 amount) public { + _mint(account, amount, true, ""); + } + + function burnFrom(address account, uint256 amount) public { + _burn(account, amount, ""); + } +}