Skip to content

Commit 40ec10c

Browse files
Upgrade to ONFT (#1)
1 parent 58dafda commit 40ec10c

13 files changed

+826
-23
lines changed

.gitmodules

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,12 @@
44
[submodule "lib/openzeppelin-contracts"]
55
path = lib/openzeppelin-contracts
66
url = https://github.com/OpenZeppelin/openzeppelin-contracts
7+
[submodule "lib/devtools"]
8+
path = lib/devtools
9+
url = https://github.com/layerzero-labs/devtools
10+
[submodule "lib/LayerZero-v2"]
11+
path = lib/LayerZero-v2
12+
url = https://github.com/layerzero-labs/LayerZero-v2
13+
[submodule "lib/solidity-bytes-utils"]
14+
path = lib/solidity-bytes-utils
15+
url = https://github.com/GNSPS/solidity-bytes-utils.git

foundry.lock

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
{
2+
"lib/devtools": {
3+
"rev": "270a49d03f223523c9ae2b1900a490f8ba083bdc"
4+
},
5+
"lib/forge-std": {
6+
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
7+
},
28
"lib/openzeppelin-contracts": {
39
"tag": {
410
"name": "v5.4.0",
511
"rev": "c64a1edb67b6e3f4a15cca8909c9482ad33a02b0"
612
}
7-
},
8-
"lib/forge-std": {
9-
"rev": "8bbcf6e3f8f62f419e5429a0bd89331c85c37824"
1013
}
1114
}

lib/LayerZero-v2

Submodule LayerZero-v2 added at ab9b083

lib/devtools

Submodule devtools added at 270a49d

lib/solidity-bytes-utils

Submodule solidity-bytes-utils added at fc50245

remappings.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@
22
erc4626-tests/=lib/openzeppelin-contracts/lib/erc4626-tests/
33
forge-std/=lib/forge-std/src/
44
halmos-cheatcodes/=lib/openzeppelin-contracts/lib/halmos-cheatcodes/src/
5-
openzeppelin-contracts/=lib/openzeppelin-contracts/
5+
openzeppelin-contracts/=lib/openzeppelin-contracts/
6+
@layerzerolabs/onft-evm/=lib/devtools/packages/onft-evm/
7+
@layerzerolabs/oapp-evm/=lib/devtools/packages/oapp-evm/
8+
@layerzerolabs/lz-evm-protocol-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/
9+
@layerzerolabs/lz-evm-messagelib-v2/=lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib/
10+
solidity-bytes-utils/=lib/solidity-bytes-utils/

script/FraxiversarryDeployScript.s.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ contract FraxiversarryDeployScript is Script {
1212
function run() public {
1313
vm.startBroadcast();
1414

15-
fraxiversarry = new Fraxiversarry(msg.sender);
15+
fraxiversarry = new Fraxiversarry(msg.sender, address(0xf00));
1616

1717
vm.stopBroadcast();
1818
}

src/Fraxiversarry.sol

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,35 @@ import {ERC721Burnable} from "openzeppelin-contracts/contracts/token/ERC721/exte
77
import {ERC721Enumerable} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
88
import {ERC721Pausable} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Pausable.sol";
99
import {ERC721URIStorage} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
10-
import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol";
1110

1211
import {IFraxiversarryErrors} from "./interfaces/IFraxiversarryErrors.sol";
1312
import {IFraxiversarryEvents} from "./interfaces/IFraxiversarryEvents.sol";
1413
import {IERC6454} from "./interfaces/IERC6454.sol";
1514
import {IERC7590} from "./interfaces/IERC7590.sol";
1615
import {IERC4906} from "openzeppelin-contracts/contracts/interfaces/IERC4906.sol";
1716

17+
import {ONFT721Core} from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721Core.sol";
18+
import {IONFT721, SendParam} from "@layerzerolabs/onft-evm/contracts/onft721/interfaces/IONFT721.sol";
19+
import {ONFT721MsgCodec} from "@layerzerolabs/onft-evm/contracts/onft721/libs/ONFT721MsgCodec.sol";
20+
import {ONFTComposeMsgCodec} from "@layerzerolabs/onft-evm/contracts/libs/ONFTComposeMsgCodec.sol";
21+
import {IOAppMsgInspector} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMsgInspector.sol";
22+
import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
23+
1824
contract Fraxiversarry is
1925
ERC721,
2026
ERC721Enumerable,
2127
ERC721URIStorage,
2228
ERC721Pausable,
23-
Ownable,
2429
ERC721Burnable,
2530
IERC6454,
2631
IERC7590,
2732
IFraxiversarryErrors,
28-
IFraxiversarryEvents
33+
IFraxiversarryEvents,
34+
ONFT721Core
2935
{
36+
using ONFT721MsgCodec for bytes;
37+
using ONFT721MsgCodec for bytes32;
38+
3039
address public constant WFRAX_ADDRESS = 0xFc00000000000000000000000000000000000002;
3140

3241
mapping(address erc20 => uint256 price) public mintPrices;
@@ -48,6 +57,8 @@ contract Fraxiversarry is
4857
uint256 public giftMintingLimit;
4958
uint256 public giftMintingPrice;
5059

60+
bool private _isBridgeOperation;
61+
5162
enum TokenType {
5263
NONEXISTENT, // 0 - Token does not exist
5364
BASE, // 1 - NFTs that are minted using ERC20 tokens
@@ -59,7 +70,10 @@ contract Fraxiversarry is
5970
string private giftTokenUri;
6071
string private premiumTokenUri;
6172

62-
constructor(address initialOwner) ERC721("Fraxiversarry", "FRAX5Y") Ownable(initialOwner) {
73+
constructor(address initialOwner, address lzEndpoint)
74+
ERC721("Fraxiversarry", "FRAX5Y")
75+
ONFT721Core(lzEndpoint, initialOwner)
76+
{
6377
mintingLimit = 12_000;
6478
giftMintingLimit = 50_000;
6579
giftMintingPrice = 50 * 1e18; // 50 WFRAX
@@ -460,23 +474,33 @@ contract Fraxiversarry is
460474
emit TokenUnfused(msg.sender, tokenId1, tokenId2, tokenId3, tokenId4, premiumTokenId);
461475
}
462476

477+
// ********** ONFT functional overrides **********
478+
479+
function token() external view override returns (address) {
480+
return address(this);
481+
}
482+
483+
function approvalRequired() public view override returns (bool) {
484+
return false;
485+
}
486+
463487
// ********** Internal functions to facilitate the ERC6454 functionality **********
464488

465489
function _soulboundCheck(uint256 tokenId) internal view {
466-
if (isNonTransferrable[tokenId]) revert CannotTransferSoulboundToken();
490+
if (!_isBridgeOperation && isNonTransferrable[tokenId]) revert CannotTransferSoulboundToken();
467491
}
468492

469493
// ********** Internal functions to facilitate the ERC7590 functionality **********
470494

471495
function _transferHeldERC20FromToken(address erc20Contract, uint256 tokenId, address to, uint256 amount) internal {
472-
IERC20 token = IERC20(erc20Contract);
496+
IERC20 erc20Token = IERC20(erc20Contract);
473497

474498
if (erc20Balances[tokenId][erc20Contract] < amount) revert InsufficientBalance();
475499

476500
erc20Balances[tokenId][erc20Contract] -= amount;
477501
transferOutNonces[tokenId]++;
478502

479-
if (!token.transfer(to, amount)) revert TransferFailed();
503+
if (!erc20Token.transfer(to, amount)) revert TransferFailed();
480504

481505
emit TransferredERC20(erc20Contract, tokenId, to, amount);
482506
}
@@ -488,19 +512,122 @@ contract Fraxiversarry is
488512
}
489513

490514
function _transferERC20ToToken(address erc20Contract, uint256 tokenId, address from, uint256 amount) internal {
491-
IERC20 token = IERC20(erc20Contract);
515+
IERC20 erc20Token = IERC20(erc20Contract);
492516

493-
if (token.allowance(from, address(this)) < amount) revert InsufficientAllowance();
494-
if (token.balanceOf(from) < amount) revert InsufficientBalance();
517+
if (erc20Token.allowance(from, address(this)) < amount) revert InsufficientAllowance();
518+
if (erc20Token.balanceOf(from) < amount) revert InsufficientBalance();
495519

496-
if (!token.transferFrom(from, address(this), amount)) revert TransferFailed();
520+
if (!erc20Token.transferFrom(from, address(this), amount)) revert TransferFailed();
497521

498522
erc20Balances[tokenId][erc20Contract] += amount;
499523

500524
emit ReceivedERC20(erc20Contract, tokenId, from, amount);
501525
}
502526

503-
// The following functions are overrides required by Solidity.
527+
// ********** Internal functions to facilitate the ONFT operations **********
528+
529+
function _bridgeBurn(address owner, uint256 tokenId) internal {
530+
_isBridgeOperation = true;
531+
// Token should only be burned, but the state including ERC20 balances should be preserved
532+
_update(address(0), tokenId, owner);
533+
_isBridgeOperation = false;
534+
}
535+
536+
function _debit(
537+
address from,
538+
uint256 tokenId,
539+
uint32 /*dstEid*/
540+
)
541+
internal
542+
override
543+
{
544+
address owner = ownerOf(tokenId);
545+
546+
if (from != owner && !isApprovedForAll(owner, from) && getApproved(tokenId) != from) {
547+
revert ERC721InsufficientApproval(from, tokenId);
548+
}
549+
550+
_bridgeBurn(owner, tokenId);
551+
}
552+
553+
function _credit(
554+
address to,
555+
uint256 tokenId,
556+
uint32 /*srcEid*/
557+
)
558+
internal
559+
override
560+
{
561+
if (_ownerOf(tokenId) != address(0)) revert TokenAlreadyExists(tokenId);
562+
563+
_isBridgeOperation = true;
564+
_update(to, tokenId, address(0));
565+
_isBridgeOperation = false;
566+
}
567+
568+
function _buildMsgAndOptions(SendParam calldata sendParam)
569+
internal
570+
view
571+
override
572+
returns (bytes memory message, bytes memory options)
573+
{
574+
if (sendParam.to == bytes32(0)) revert InvalidReceiver();
575+
576+
string memory tokenUri = tokenURI(sendParam.tokenId);
577+
bool isSoulbound = isNonTransferrable[sendParam.tokenId];
578+
bytes memory composedMessage = abi.encode(tokenUri, isSoulbound);
579+
580+
if (isSoulbound && sendParam.to.bytes32ToAddress() != ownerOf(sendParam.tokenId)) {
581+
revert CannotTransferSoulboundToken();
582+
}
583+
584+
bool hasCompose;
585+
(message, hasCompose) = ONFT721MsgCodec.encode(sendParam.to, sendParam.tokenId, composedMessage);
586+
587+
uint16 msgType = hasCompose ? SEND_AND_COMPOSE : SEND;
588+
options = combineOptions(sendParam.dstEid, msgType, sendParam.extraOptions);
589+
590+
address inspector = msgInspector;
591+
if (inspector != address(0)) IOAppMsgInspector(inspector).inspect(message, options);
592+
}
593+
594+
function _lzReceive(Origin calldata origin, bytes32 guid, bytes calldata message, address, bytes calldata)
595+
internal
596+
override
597+
{
598+
address toAddress = message.sendTo().bytes32ToAddress();
599+
uint256 tokenId = message.tokenId();
600+
601+
if (!message.isComposed()) revert MissingComposedMessage();
602+
603+
bytes memory rawCompose = message.composeMsg();
604+
bytes memory rawMessage = rawCompose;
605+
uint256 len;
606+
assembly {
607+
len := mload(rawCompose)
608+
// shift pointer forward by 32 bytes (skip fromOApp word)
609+
rawMessage := add(rawMessage, 32)
610+
// set length = originalLength - 32
611+
mstore(rawMessage, sub(len, 32))
612+
}
613+
614+
(string memory tokenUri, bool isSoulbound) = abi.decode(rawMessage, (string, bool));
615+
616+
_credit(toAddress, tokenId, origin.srcEid);
617+
_setTokenURI(tokenId, tokenUri);
618+
isNonTransferrable[tokenId] = isSoulbound;
619+
620+
bytes32 composeFrom = ONFTComposeMsgCodec.addressToBytes32(address(this));
621+
bytes memory composeInnerMsg = abi.encode(tokenUri, isSoulbound);
622+
bytes memory composeMsg = abi.encodePacked(composeFrom, composeInnerMsg);
623+
624+
bytes memory composedMsgEncoded = ONFTComposeMsgCodec.encode(origin.nonce, origin.srcEid, composeMsg);
625+
endpoint.sendCompose(toAddress, guid, 0, composedMsgEncoded);
626+
627+
emit ONFTReceived(guid, origin.srcEid, toAddress, tokenId);
628+
}
629+
630+
// ********** The following functions are overrides required by Solidity. **********
504631

505632
function _update(address to, uint256 tokenId, address auth)
506633
internal
@@ -531,6 +658,7 @@ contract Fraxiversarry is
531658
returns (bool)
532659
{
533660
return super.supportsInterface(interfaceId) || interfaceId == type(IERC7590).interfaceId
534-
|| interfaceId == type(IERC6454).interfaceId || interfaceId == type(IERC4906).interfaceId;
661+
|| interfaceId == type(IERC6454).interfaceId || interfaceId == type(IERC4906).interfaceId
662+
|| interfaceId == type(IONFT721).interfaceId;
535663
}
536664
}

src/interfaces/IFraxiversarryErrors.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ contract IFraxiversarryErrors {
1212
error InvalidGiftMintPrice();
1313
error InvalidRange();
1414
error MintingLimitReached();
15+
error MissingComposedMessage();
1516
error OnlyTokenOwnerCanBurnTheToken();
1617
error OnlyTokenOwnerCanFuseTokens();
1718
error OnlyTokenOwnerCanUnfuseTokens();
1819
error OutOfBounds();
1920
error SameTokenUnderlyingAssets();
21+
error TokenAlreadyExists(uint256 tokenId);
2022
error TokenDoesNotExist();
2123
error TokensCanOnlyBeDepositedByNftMint();
2224
error TokensCanOnlyBeRetrievedByNftBurn();

0 commit comments

Comments
 (0)