From 9555f716663cc3b1d6d5c83b0b12b260d7a37d09 Mon Sep 17 00:00:00 2001 From: ChadChillBro7 Date: Tue, 1 Apr 2025 12:31:43 -0700 Subject: [PATCH] Add superchain ERC20 example --- .../mocks/SuperChainMintBurnERC20.sol | 62 +++++++++ .../MySuperChainMintBurnOFTAdapter.test.ts | 123 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 examples/mint-burn-oft-adapter/contracts/mocks/SuperChainMintBurnERC20.sol create mode 100644 examples/mint-burn-oft-adapter/test/hardhat/MySuperChainMintBurnOFTAdapter.test.ts diff --git a/examples/mint-burn-oft-adapter/contracts/mocks/SuperChainMintBurnERC20.sol b/examples/mint-burn-oft-adapter/contracts/mocks/SuperChainMintBurnERC20.sol new file mode 100644 index 0000000000..488f5a6962 --- /dev/null +++ b/examples/mint-burn-oft-adapter/contracts/mocks/SuperChainMintBurnERC20.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.22; + +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import { IMintableBurnable } from "@layerzerolabs/oft-evm/contracts/interfaces/IMintableBurnable.sol"; + +interface IERC7802 { + event CrosschainBurn(address indexed from, uint256 amount, address indexed sender); + event CrosschainMint(address indexed to, uint256 amount, address indexed sender); + + function crosschainBurn(address _from, uint256 _amount) external; + function crosschainMint(address _to, uint256 _amount) external; +} + +contract SuperChainMintBurnERC20 is ERC20, IERC7802, IMintableBurnable, Ownable { + address public TOKEN_BRIDGE; + + error Unauthorized(); + error InvalidTokenBridgeAddress(); + + event SetTokenBridge(address _tokenBridge); + + constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) {} + + modifier onlyTokenBridge() { + if (msg.sender != TOKEN_BRIDGE) revert Unauthorized(); + _; + } + + function setTokenBridge(address _tokenBridge) external onlyOwner { + if (_tokenBridge == address(0)) revert InvalidTokenBridgeAddress(); + + TOKEN_BRIDGE = _tokenBridge; + emit SetTokenBridge(_tokenBridge); + } + + // @notice 'sender' in these contexts is the caller, i.e. the current tokenBridge, + // It is NOT the 'sender' from the src chain who initialized the transfer + + // Functions to handle IERC7802.sol + function crosschainBurn(address _from, uint256 _amount) external onlyTokenBridge { + _burn(_from, _amount); + emit CrosschainBurn(_from, _amount, msg.sender); + } + function crosschainMint(address _to, uint256 _amount) external onlyTokenBridge { + _mint(_to, _amount); + emit CrosschainMint(_to, _amount, msg.sender); + } + + // Functions to handle MintBurnOFTAdapter.sol + function burn(address _from, uint256 _amount) external onlyTokenBridge returns (bool) { + _burn(_from, _amount); + emit CrosschainBurn(_from, _amount, msg.sender); + return true; + } + function mint(address _to, uint256 _amount) external onlyTokenBridge returns (bool) { + _mint(_to, _amount); + emit CrosschainMint(_to, _amount, msg.sender); + return true; + } +} diff --git a/examples/mint-burn-oft-adapter/test/hardhat/MySuperChainMintBurnOFTAdapter.test.ts b/examples/mint-burn-oft-adapter/test/hardhat/MySuperChainMintBurnOFTAdapter.test.ts new file mode 100644 index 0000000000..42f941caa8 --- /dev/null +++ b/examples/mint-burn-oft-adapter/test/hardhat/MySuperChainMintBurnOFTAdapter.test.ts @@ -0,0 +1,123 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { Contract, ContractFactory } from 'ethers' +import { deployments, ethers } from 'hardhat' + +import { Options } from '@layerzerolabs/lz-v2-utilities' + +describe('MySuperChainMintBurnOFTAdapter Test', function () { + // Constant representing a mock Endpoint ID for testing purposes + const eidA = 1 + const eidB = 2 + // Declaration of variables to be used in the test suite + let MyMintBurnOFTAdapter: ContractFactory + let MyOFT: ContractFactory + let MintBurnERC20Mock: ContractFactory + let EndpointV2Mock: ContractFactory + let ownerA: SignerWithAddress + let ownerB: SignerWithAddress + let endpointOwner: SignerWithAddress + let token: Contract + let myMintBurnOFTAdapterA: Contract + let myOFTB: Contract + let mockEndpointV2A: Contract + let mockEndpointV2B: Contract + + // Before hook for setup that runs once before all tests in the block + before(async function () { + // Contract factory for our tested contract + // + // We are using a derived contract that exposes a mint() function for testing purposes + MyMintBurnOFTAdapter = await ethers.getContractFactory('MyMintBurnOFTAdapterMock') + + MyOFT = await ethers.getContractFactory('MyOFTMock') + + MintBurnERC20Mock = await ethers.getContractFactory('SuperChainMintBurnERC20') + + // Fetching the first three signers (accounts) from Hardhat's local Ethereum network + const signers = await ethers.getSigners() + + ;[ownerA, ownerB, endpointOwner] = signers + + // The EndpointV2Mock contract comes from @layerzerolabs/test-devtools-evm-hardhat package + // and its artifacts are connected as external artifacts to this project + // + // Unfortunately, hardhat itself does not yet provide a way of connecting external artifacts, + // so we rely on hardhat-deploy to create a ContractFactory for EndpointV2Mock + // + // See https://github.com/NomicFoundation/hardhat/issues/1040 + const EndpointV2MockArtifact = await deployments.getArtifact('EndpointV2Mock') + EndpointV2Mock = new ContractFactory(EndpointV2MockArtifact.abi, EndpointV2MockArtifact.bytecode, endpointOwner) + }) + + // beforeEach hook for setup that runs before each test in the block + beforeEach(async function () { + // Deploying a mock LZEndpoint with the given Endpoint ID + mockEndpointV2A = await EndpointV2Mock.deploy(eidA) + mockEndpointV2B = await EndpointV2Mock.deploy(eidB) + + token = await MintBurnERC20Mock.deploy('Token', 'TOKEN') + + // Deploying two instances of MyOFT contract with different identifiers and linking them to the mock LZEndpoint + myMintBurnOFTAdapterA = await MyMintBurnOFTAdapter.deploy( + token.address, + token.address, + mockEndpointV2A.address, + ownerA.address + ) + // Set the token bridge to be the adapter + await token.setTokenBridge(myMintBurnOFTAdapterA.address) + myOFTB = await MyOFT.deploy('bOFT', 'bOFT', mockEndpointV2B.address, ownerB.address) + + // Setting destination endpoints in the LZEndpoint mock for each MyOFT instance + await mockEndpointV2A.setDestLzEndpoint(myOFTB.address, mockEndpointV2B.address) + await mockEndpointV2B.setDestLzEndpoint(myMintBurnOFTAdapterA.address, mockEndpointV2A.address) + + // Setting each MyOFT instance as a peer of the other in the mock LZEndpoint + await myMintBurnOFTAdapterA.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myOFTB.address, 32)) + await myOFTB.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myMintBurnOFTAdapterA.address, 32)) + }) + + // A test case to verify token transfer functionality + it('should send a token from A address to B address via OFTAdapter/OFT', async function () { + // Minting an initial amount of tokens to ownerA's address in the myOFTA contract + const initialAmount = ethers.utils.parseEther('100') + + // Temporarily setting the token bridge to the ownerA address for minting + await token.setTokenBridge(ownerA.address) + await token.mint(ownerA.address, initialAmount) + await token.setTokenBridge(myMintBurnOFTAdapterA.address) + + // Defining the amount of tokens to send and constructing the parameters for the send operation + const tokensToSend = ethers.utils.parseEther('1') + + // Defining extra message execution options for the send operation + const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString() + + const sendParam = [ + eidB, + ethers.utils.zeroPad(ownerB.address, 32), + tokensToSend, + tokensToSend, + options, + '0x', + '0x', + ] + + // Fetching the native fee for the token send operation + const [nativeFee] = await myMintBurnOFTAdapterA.quoteSend(sendParam, false) + + // Executing the send operation from myOFTA contract + await myMintBurnOFTAdapterA.send(sendParam, [nativeFee, 0], ownerA.address, { value: nativeFee }) + + // Fetching the final token balances of ownerA and ownerB + const finalBalanceA = await token.balanceOf(ownerA.address) + const finalBalanceAdapter = await token.balanceOf(myMintBurnOFTAdapterA.address) + const finalBalanceB = await myOFTB.balanceOf(ownerB.address) + + // Asserting that the final balances are as expected after the send operation + expect(finalBalanceA).eql(initialAmount.sub(tokensToSend)) + expect(finalBalanceAdapter).eql(ethers.utils.parseEther('0')) + expect(finalBalanceB).eql(tokensToSend) + }) +})