Skip to content

Commit e024c8e

Browse files
committed
Add superchain ERC20 example
1 parent 697d843 commit e024c8e

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.22;
3+
4+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
5+
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
6+
import { IMintableBurnable } from "@layerzerolabs/oft-evm/contracts/interfaces/IMintableBurnable.sol";
7+
8+
interface IERC7802 {
9+
event CrosschainBurn(address indexed from, uint256 amount, address indexed sender);
10+
event CrosschainMint(address indexed to, uint256 amount, address indexed sender);
11+
12+
function crosschainBurn(address _from, uint256 _amount) external;
13+
function crosschainMint(address _to, uint256 _amount) external;
14+
}
15+
16+
contract SuperChainMintBurnERC20 is ERC20, IERC7802, IMintableBurnable, Ownable {
17+
address public TOKEN_BRIDGE;
18+
19+
error Unauthorized();
20+
error InvalidTokenBridgeAddress();
21+
22+
event SetTokenBridge(address _tokenBridge);
23+
24+
constructor(string memory name, string memory symbol) ERC20(name, symbol) Ownable(msg.sender) {}
25+
26+
modifier onlyTokenBridge() {
27+
if (msg.sender != TOKEN_BRIDGE) revert Unauthorized();
28+
_;
29+
}
30+
31+
function setTokenBridge(address _tokenBridge) external onlyOwner {
32+
if (_tokenBridge == address(0)) revert InvalidTokenBridgeAddress();
33+
34+
TOKEN_BRIDGE = _tokenBridge;
35+
emit SetTokenBridge(_tokenBridge);
36+
}
37+
38+
// @notice 'sender' in these contexts is the caller, i.e. the current tokenBridge,
39+
// It is NOT the 'sender' from the src chain who initialized the transfer
40+
41+
// Functions to handle IERC7802.sol
42+
function crosschainBurn(address _from, uint256 _amount) external onlyTokenBridge {
43+
_burn(_from, _amount);
44+
emit CrosschainBurn(_from, _amount, msg.sender);
45+
}
46+
function crosschainMint(address _to, uint256 _amount) external onlyTokenBridge {
47+
_mint(_to, _amount);
48+
emit CrosschainMint(_to, _amount, msg.sender);
49+
}
50+
51+
// Functions to handle MintBurnOFTAdapter.sol
52+
function burn(address _from, uint256 _amount) external onlyTokenBridge returns (bool) {
53+
_burn(_from, _amount);
54+
emit CrosschainBurn(_from, _amount, msg.sender);
55+
return true;
56+
}
57+
function mint(address _to, uint256 _amount) external onlyTokenBridge returns (bool) {
58+
_mint(_to, _amount);
59+
emit CrosschainMint(_to, _amount, msg.sender);
60+
return true;
61+
}
62+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
2+
import { expect } from 'chai'
3+
import { Contract, ContractFactory } from 'ethers'
4+
import { deployments, ethers } from 'hardhat'
5+
6+
import { Options } from '@layerzerolabs/lz-v2-utilities'
7+
8+
describe('MySuperChainMintBurnOFTAdapter Test', function () {
9+
// Constant representing a mock Endpoint ID for testing purposes
10+
const eidA = 1
11+
const eidB = 2
12+
// Declaration of variables to be used in the test suite
13+
let MyMintBurnOFTAdapter: ContractFactory
14+
let MyOFT: ContractFactory
15+
let MintBurnERC20Mock: ContractFactory
16+
let EndpointV2Mock: ContractFactory
17+
let ownerA: SignerWithAddress
18+
let ownerB: SignerWithAddress
19+
let endpointOwner: SignerWithAddress
20+
let token: Contract
21+
let myMintBurnOFTAdapterA: Contract
22+
let myOFTB: Contract
23+
let mockEndpointV2A: Contract
24+
let mockEndpointV2B: Contract
25+
26+
// Before hook for setup that runs once before all tests in the block
27+
before(async function () {
28+
// Contract factory for our tested contract
29+
//
30+
// We are using a derived contract that exposes a mint() function for testing purposes
31+
MyMintBurnOFTAdapter = await ethers.getContractFactory('MyMintBurnOFTAdapterMock')
32+
33+
MyOFT = await ethers.getContractFactory('MyOFTMock')
34+
35+
MintBurnERC20Mock = await ethers.getContractFactory('SuperChainMintBurnERC20')
36+
37+
// Fetching the first three signers (accounts) from Hardhat's local Ethereum network
38+
const signers = await ethers.getSigners()
39+
40+
;[ownerA, ownerB, endpointOwner] = signers
41+
42+
// The EndpointV2Mock contract comes from @layerzerolabs/test-devtools-evm-hardhat package
43+
// and its artifacts are connected as external artifacts to this project
44+
//
45+
// Unfortunately, hardhat itself does not yet provide a way of connecting external artifacts,
46+
// so we rely on hardhat-deploy to create a ContractFactory for EndpointV2Mock
47+
//
48+
// See https://github.com/NomicFoundation/hardhat/issues/1040
49+
const EndpointV2MockArtifact = await deployments.getArtifact('EndpointV2Mock')
50+
EndpointV2Mock = new ContractFactory(EndpointV2MockArtifact.abi, EndpointV2MockArtifact.bytecode, endpointOwner)
51+
})
52+
53+
// beforeEach hook for setup that runs before each test in the block
54+
beforeEach(async function () {
55+
// Deploying a mock LZEndpoint with the given Endpoint ID
56+
mockEndpointV2A = await EndpointV2Mock.deploy(eidA)
57+
mockEndpointV2B = await EndpointV2Mock.deploy(eidB)
58+
59+
token = await MintBurnERC20Mock.deploy('Token', 'TOKEN')
60+
61+
// Deploying two instances of MyOFT contract with different identifiers and linking them to the mock LZEndpoint
62+
myMintBurnOFTAdapterA = await MyMintBurnOFTAdapter.deploy(
63+
token.address,
64+
token.address,
65+
mockEndpointV2A.address,
66+
ownerA.address
67+
)
68+
// Set the token bridge to be the adapter
69+
await token.setTokenBridge(myMintBurnOFTAdapterA.address)
70+
myOFTB = await MyOFT.deploy('bOFT', 'bOFT', mockEndpointV2B.address, ownerB.address)
71+
72+
// Setting destination endpoints in the LZEndpoint mock for each MyOFT instance
73+
await mockEndpointV2A.setDestLzEndpoint(myOFTB.address, mockEndpointV2B.address)
74+
await mockEndpointV2B.setDestLzEndpoint(myMintBurnOFTAdapterA.address, mockEndpointV2A.address)
75+
76+
// Setting each MyOFT instance as a peer of the other in the mock LZEndpoint
77+
await myMintBurnOFTAdapterA.connect(ownerA).setPeer(eidB, ethers.utils.zeroPad(myOFTB.address, 32))
78+
await myOFTB.connect(ownerB).setPeer(eidA, ethers.utils.zeroPad(myMintBurnOFTAdapterA.address, 32))
79+
})
80+
81+
// A test case to verify token transfer functionality
82+
it('should send a token from A address to B address via OFTAdapter/OFT', async function () {
83+
// Minting an initial amount of tokens to ownerA's address in the myOFTA contract
84+
const initialAmount = ethers.utils.parseEther('100')
85+
86+
// Temporarily setting the token bridge to the ownerA address for minting
87+
await token.setTokenBridge(ownerA.address)
88+
await token.mint(ownerA.address, initialAmount)
89+
await token.setTokenBridge(myMintBurnOFTAdapterA.address)
90+
91+
// Defining the amount of tokens to send and constructing the parameters for the send operation
92+
const tokensToSend = ethers.utils.parseEther('1')
93+
94+
// Defining extra message execution options for the send operation
95+
const options = Options.newOptions().addExecutorLzReceiveOption(200000, 0).toHex().toString()
96+
97+
const sendParam = [
98+
eidB,
99+
ethers.utils.zeroPad(ownerB.address, 32),
100+
tokensToSend,
101+
tokensToSend,
102+
options,
103+
'0x',
104+
'0x',
105+
]
106+
107+
// Fetching the native fee for the token send operation
108+
const [nativeFee] = await myMintBurnOFTAdapterA.quoteSend(sendParam, false)
109+
110+
// Executing the send operation from myOFTA contract
111+
await myMintBurnOFTAdapterA.send(sendParam, [nativeFee, 0], ownerA.address, { value: nativeFee })
112+
113+
// Fetching the final token balances of ownerA and ownerB
114+
const finalBalanceA = await token.balanceOf(ownerA.address)
115+
const finalBalanceAdapter = await token.balanceOf(myMintBurnOFTAdapterA.address)
116+
const finalBalanceB = await myOFTB.balanceOf(ownerB.address)
117+
118+
// Asserting that the final balances are as expected after the send operation
119+
expect(finalBalanceA).eql(initialAmount.sub(tokensToSend))
120+
expect(finalBalanceAdapter).eql(ethers.utils.parseEther('0'))
121+
expect(finalBalanceB).eql(tokensToSend)
122+
})
123+
})

0 commit comments

Comments
 (0)