Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this in examples instead of oft-evm? Shouldn't it be in oft-evm so folks don't need to copy + paste to use it?

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;
}
}
Original file line number Diff line number Diff line change
@@ -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)
})
})