Skip to content

Commit

Permalink
🗞️ Add MintBurnOFTAdapter.sol to oft-evm package (#1116)
Browse files Browse the repository at this point in the history
  • Loading branch information
St0rmBr3w authored Dec 10, 2024
1 parent e3fced8 commit 5c47a41
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-buttons-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/oft-evm": minor
---

Added MintBurnOFTAdapter
115 changes: 115 additions & 0 deletions packages/oft-evm/contracts/MintBurnOFTAdapter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

// External imports
import { IERC20, IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

// Local imports
import { IMintableBurnable } from "./interfaces/IMintableBurnable.sol";
import { OFTCore } from "./OFTCore.sol";

/**
* @title MintBurnOFTAdapter
* @notice A variant of the standard OFT Adapter that uses an existing ERC20's mint and burn mechanisms for cross-chain transfers.
*
* @dev Inherits from OFTCore and provides implementations for _debit and _credit functions using a mintable and burnable token.
*/
abstract contract MintBurnOFTAdapter is OFTCore {
/// @dev The underlying ERC20 token.
IERC20 internal immutable innerToken;

/// @notice The contract responsible for minting and burning tokens.
IMintableBurnable public immutable minterBurner;

/**
* @notice Initializes the MintBurnOFTAdapter contract.
*
* @param _token The address of the underlying ERC20 token.
* @param _minterBurner The contract responsible for minting and burning tokens.
* @param _lzEndpoint The LayerZero endpoint address.
* @param _delegate The address of the delegate.
*
* @dev Calls the OFTCore constructor with the token's decimals, the endpoint, and the delegate.
*/
constructor(
address _token,
IMintableBurnable _minterBurner,
address _lzEndpoint,
address _delegate
) OFTCore(IERC20Metadata(_token).decimals(), _lzEndpoint, _delegate) {
innerToken = IERC20(_token);
minterBurner = _minterBurner;
}

/**
* @notice Retrieves the address of the underlying ERC20 token.
*
* @return The address of the adapted ERC20 token.
*
* @dev In the case of MintBurnOFTAdapter, address(this) and erc20 are NOT the same contract.
*/
function token() public view returns (address) {
return address(innerToken);
}

/**
* @notice Indicates whether the OFT contract requires approval of the underlying token to send.
*
* @return requiresApproval True if approval is required, false otherwise.
*
* @dev In this MintBurnOFTAdapter, approval is NOT required because it uses mint and burn privileges.
*/
function approvalRequired() external pure virtual returns (bool) {
return false;
}

/**
* @notice Burns tokens from the sender's balance to prepare for sending.
*
* @param _from The address to debit the tokens from.
* @param _amountLD The amount of tokens to send in local decimals.
* @param _minAmountLD The minimum amount to send in local decimals.
* @param _dstEid The destination chain ID.
*
* @return amountSentLD The amount sent in local decimals.
* @return amountReceivedLD The amount received in local decimals on the remote.
*
* @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, i.e., 1 token in, 1 token out.
* If the 'innerToken' applies something like a transfer fee, the default will NOT work.
* A pre/post balance check will need to be done to calculate the amountReceivedLD.
*/
function _debit(
address _from,
uint256 _amountLD,
uint256 _minAmountLD,
uint32 _dstEid
) internal virtual override returns (uint256 amountSentLD, uint256 amountReceivedLD) {
(amountSentLD, amountReceivedLD) = _debitView(_amountLD, _minAmountLD, _dstEid);
// Burns tokens from the caller.
minterBurner.burn(_from, amountSentLD);
}

/**
* @notice Mints tokens to the specified address upon receiving them.
*
* @param _to The address to credit the tokens to.
* @param _amountLD The amount of tokens to credit in local decimals.
*
* @return amountReceivedLD The amount of tokens actually received in local decimals.
*
* @dev WARNING: The default OFTAdapter implementation assumes LOSSLESS transfers, i.e., 1 token in, 1 token out.
* If the 'innerToken' applies something like a transfer fee, the default will NOT work.
* A pre/post balance check will need to be done to calculate the amountReceivedLD.
*/
function _credit(
address _to,
uint256 _amountLD,
uint32 /* _srcEid */
) internal virtual override returns (uint256 amountReceivedLD) {
if (_to == address(0x0)) _to = address(0xdead); // _mint(...) does not support address(0x0)
// Mints the tokens and transfers to the recipient.
minterBurner.mint(_to, _amountLD);
// In the case of NON-default OFTAdapter, the amountLD MIGHT not be equal to amountReceivedLD.
return _amountLD;
}
}
22 changes: 22 additions & 0 deletions packages/oft-evm/contracts/interfaces/IMintableBurnable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

/// @title Interface for mintable and burnable tokens
interface IMintableBurnable {

/**
* @notice Burns tokens from a specified account
* @param _from Address from which tokens will be burned
* @param _amount Amount of tokens to be burned
* @return success Indicates whether the operation was successful
*/
function burn(address _from, uint256 _amount) external returns (bool success);

/**
* @notice Mints tokens to a specified account
* @param _to Address to which tokens will be minted
* @param _amount Amount of tokens to be minted
* @return success Indicates whether the operation was successful
*/
function mint(address _to, uint256 _amount) external returns (bool success);
}
118 changes: 115 additions & 3 deletions packages/oft-evm/test/OFT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { MessagingFee, MessagingReceipt } from "../contracts/OFTCore.sol";
import { NativeOFTAdapterMock } from "./mocks/NativeOFTAdapterMock.sol";
import { OFTAdapterMock } from "./mocks/OFTAdapterMock.sol";
import { ERC20Mock } from "./mocks/ERC20Mock.sol";
import { MintBurnERC20Mock } from "./mocks/MintBurnERC20Mock.sol";
import { ElevatedMinterBurnerMock } from "./mocks/ElevatedMinterBurnerMock.sol";
import { MintBurnOFTAdapterMock } from "./mocks/MintBurnOFTAdapterMock.sol";
import { OFTComposerMock } from "./mocks/OFTComposerMock.sol";
import { OFTInspectorMock, IOAppMsgInspector } from "./mocks/OFTInspectorMock.sol";
import { IOAppOptionsType3, EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
Expand All @@ -18,13 +21,16 @@ import { OFTComposeMsgCodec } from "../contracts/libs/OFTComposeMsgCodec.sol";

import { IOFT, SendParam, OFTReceipt } from "../contracts/interfaces/IOFT.sol";
import { OFT } from "../contracts/OFT.sol";
import { MintBurnOFTAdapter } from "../contracts/MintBurnOFTAdapter.sol";
import { IMintableBurnable } from "../contracts/interfaces/IMintableBurnable.sol";
import { NativeOFTAdapter } from "../contracts/NativeOFTAdapter.sol";
import { OFTAdapter } from "../contracts/OFTAdapter.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";

import { OFTMockCodec } from "./lib/OFTMockCodec.sol";
import { OFTAdapterMockCodec } from "./lib/OFTAdapterMockCodec.sol";
import { NativeOFTAdapterMockCodec } from "./lib/NativeOFTAdapterMockCodec.sol";
import { MintBurnOFTAdapterMockCodec } from "./lib/MintBurnOFTAdapterMockCodec.sol";

import "forge-std/console.sol";
import { TestHelperOz5 } from "@layerzerolabs/test-devtools-evm-foundry/contracts/TestHelperOz5.sol";
Expand All @@ -34,39 +40,49 @@ contract OFTTest is TestHelperOz5 {
using OFTMockCodec for OFT;
using OFTAdapterMockCodec for OFTAdapter;
using NativeOFTAdapterMockCodec for NativeOFTAdapter;
using MintBurnOFTAdapterMockCodec for MintBurnOFTAdapter;

uint32 internal constant A_EID = 1;
uint32 internal constant B_EID = 2;
uint32 internal constant C_EID = 3;
uint32 internal constant D_EID = 4;
uint32 internal constant E_EID = 5;

string internal constant A_OFT_NAME = "aOFT";
string internal constant A_OFT_SYMBOL = "aOFT";
string internal constant B_OFT_NAME = "bOFT";
string internal constant B_OFT_SYMBOL = "bOFT";
string internal constant C_TOKEN_NAME = "cToken";
string internal constant C_TOKEN_SYMBOL = "cToken";
string internal constant E_MINTABLE_TOKEN_NAME = "eMintableToken";
string internal constant E_MINTABLE_TOKEN_SYMBOL = "eToken";


OFT internal aOFT;
OFT internal bOFT;
NativeOFTAdapter internal dNativeOFTAdapter;
OFTAdapter internal cOFTAdapter;
ERC20Mock internal cERC20Mock;
NativeOFTAdapter internal dNativeOFTAdapter;
MintBurnOFTAdapter internal eMintBurnOFTAdapter;
ElevatedMinterBurnerMock internal eMinterBurnerMock;
MintBurnERC20Mock internal eMintBurnERC20Mock;

OFTInspectorMock internal oAppInspector;

address public userA = makeAddr("userA");
address public userB = makeAddr("userB");
address public userC = makeAddr("userC");
address public userD = makeAddr("userD");
address public userE = makeAddr("userE");
address public attacker = makeAddr("attacker");
uint256 public initialBalance = 100 ether;
uint256 public initialNativeBalance = 1000 ether;

function setUp() public virtual override {
_deal();

super.setUp();
setUpEndpoints(4, LibraryType.UltraLightNode);
setUpEndpoints(5, LibraryType.UltraLightNode);

aOFT = OFTMock(
_deployOApp(
Expand Down Expand Up @@ -97,18 +113,30 @@ contract OFTTest is TestHelperOz5 {
)
);

eMintBurnERC20Mock = new MintBurnERC20Mock(E_MINTABLE_TOKEN_NAME, E_MINTABLE_TOKEN_SYMBOL);
eMinterBurnerMock = new ElevatedMinterBurnerMock(IMintableBurnable(eMintBurnERC20Mock), address(this));
eMintBurnOFTAdapter = MintBurnOFTAdapterMock(
_deployOApp(
type(MintBurnOFTAdapterMock).creationCode,
abi.encode(address(eMintBurnERC20Mock), address(eMinterBurnerMock), address(endpoints[E_EID]), address(this))
)
);
eMinterBurnerMock.setOperator(address(eMintBurnOFTAdapter), true);

// config and wire the ofts
address[] memory ofts = new address[](4);
address[] memory ofts = new address[](5);
ofts[0] = address(aOFT);
ofts[1] = address(bOFT);
ofts[2] = address(cOFTAdapter);
ofts[3] = address(dNativeOFTAdapter);
ofts[4] = address(eMintBurnOFTAdapter);
this.wireOApps(ofts);

// mint tokens
aOFT.asOFTMock().mint(userA, initialBalance);
bOFT.asOFTMock().mint(userB, initialBalance);
cERC20Mock.mint(userC, initialBalance);
eMintBurnERC20Mock.mint(userE, initialBalance);

// deploy a universal inspector, can be used by each oft
oAppInspector = new OFTInspectorMock();
Expand All @@ -119,24 +147,29 @@ contract OFTTest is TestHelperOz5 {
vm.deal(userB, initialNativeBalance);
vm.deal(userC, initialNativeBalance);
vm.deal(userD, initialNativeBalance);
vm.deal(userE, initialNativeBalance);
}

function test_constructor() public {
assertEq(aOFT.owner(), address(this));
assertEq(bOFT.owner(), address(this));
assertEq(cOFTAdapter.owner(), address(this));
assertEq(dNativeOFTAdapter.owner(), address(this));
assertEq(eMintBurnOFTAdapter.owner(), address(this));

assertEq(aOFT.balanceOf(userA), initialBalance);
assertEq(bOFT.balanceOf(userB), initialBalance);
assertEq(IERC20(cOFTAdapter.token()).balanceOf(userC), initialBalance);
assertEq(IERC20(eMintBurnOFTAdapter.token()).balanceOf(userE), initialBalance);

assertEq(aOFT.token(), address(aOFT));
assertEq(bOFT.token(), address(bOFT));
assertEq(cOFTAdapter.token(), address(cERC20Mock));
assertEq(dNativeOFTAdapter.token(), address(0));
assertEq(eMintBurnOFTAdapter.token(), address(eMintBurnERC20Mock));

assertEq(dNativeOFTAdapter.approvalRequired(), false);
assertEq(eMintBurnOFTAdapter.approvalRequired(), false);
}

function test_oftVersion() public {
Expand Down Expand Up @@ -474,6 +507,85 @@ contract OFTTest is TestHelperOz5 {
dNativeOFTAdapter.asNativeOFTAdapterMock().send{ value: extraMsgValue}(sendParam, fee, userD);
}

function test_set_minter_burner_operator() public {
vm.prank(attacker);
vm.expectRevert();
eMinterBurnerMock.setOperator(address(eMintBurnOFTAdapter), true);
}

function test_minter_burner_operator() public {
vm.prank(attacker);
vm.expectRevert();
eMinterBurnerMock.mint(attacker, initialBalance);
}

function test_burn_operator() public {
vm.prank(attacker);
vm.expectRevert();
eMinterBurnerMock.burn(attacker, initialBalance);
}

function test_mint_burn_oft_adapter_debit() public virtual {
uint256 amountToSendLD = 1 ether;
uint256 minAmountToCreditLD = 1 ether;
uint32 dstEid = E_EID;

vm.prank(userE);
vm.expectRevert(
abi.encodeWithSelector(IOFT.SlippageExceeded.selector, amountToSendLD, minAmountToCreditLD + 1)
);
eMintBurnOFTAdapter.asMintBurnOFTAdapterMock().debit(amountToSendLD, minAmountToCreditLD + 1, dstEid);

vm.prank(userE);
(uint256 amountDebitedLD, uint256 amountToCreditLD) = eMintBurnOFTAdapter.asMintBurnOFTAdapterMock().debit(
amountToSendLD,
minAmountToCreditLD,
dstEid
);

assertEq(amountDebitedLD, amountToSendLD);
assertEq(amountToCreditLD, amountToSendLD);
}

function test_mint_burn_oft_adapter_credit() public {
uint256 amountToCreditLD = 1 ether;
uint32 srcEid = C_EID;

vm.prank(userC);
cERC20Mock.transfer(address(cOFTAdapter), amountToCreditLD);

uint256 amountReceived = eMintBurnOFTAdapter.asMintBurnOFTAdapterMock().credit(userE, amountToCreditLD, srcEid);

assertEq(cERC20Mock.balanceOf(userC), initialBalance - amountToCreditLD);
assertEq(eMintBurnERC20Mock.balanceOf(address(userE)), initialBalance + amountReceived);
}

function test_mint_burn_oft_adapter_send() public {
uint256 tokensToSend = 1 ether;
bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(200000, 0);
SendParam memory sendParam = SendParam(
B_EID,
addressToBytes32(userB),
tokensToSend,
tokensToSend,
options,
"",
""
);
MessagingFee memory fee = eMintBurnOFTAdapter.quoteSend(sendParam, false);

assertEq(eMintBurnERC20Mock.balanceOf(userE), initialBalance);
assertEq(bOFT.balanceOf(userB), initialBalance);

vm.startPrank(userE);
eMintBurnOFTAdapter.send{ value: fee.nativeFee }(sendParam, fee, payable(address(this)));
vm.stopPrank();
verifyPackets(B_EID, addressToBytes32(address(bOFT)));

assertEq(eMintBurnERC20Mock.balanceOf(userE), initialBalance - tokensToSend);
assertEq(bOFT.balanceOf(userB), initialBalance + tokensToSend);
}

function decodeOFTMsgCodec(
bytes calldata message
) public pure returns (bool isComposed, bytes32 sendTo, uint64 amountSD, bytes memory composeMsg) {
Expand Down
15 changes: 15 additions & 0 deletions packages/oft-evm/test/lib/MintBurnOFTAdapterMockCodec.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-LICENSE-Identifier: UNLICENSED

pragma solidity ^0.8.22;

import { MintBurnOFTAdapterMock } from "../mocks/MintBurnOFTAdapterMock.sol";
import { MintBurnOFTAdapter } from "../../contracts/MintBurnOFTAdapter.sol";

// @title MintBurnOFTAdapterMockCodec
// @notice Codec to convert MintBurnOFTAdapter to MintBurnOFTAdapterMock in a consistent, readable manner.
// @dev For testing purposes only.
library MintBurnOFTAdapterMockCodec {
function asMintBurnOFTAdapterMock(MintBurnOFTAdapter _oft) internal pure returns (MintBurnOFTAdapterMock) {
return MintBurnOFTAdapterMock(address(_oft));
}
}
Loading

0 comments on commit 5c47a41

Please sign in to comment.