Skip to content

Commit 4b0b5bc

Browse files
Add Ethereum side smart contract
1 parent 7b161ca commit 4b0b5bc

File tree

4 files changed

+964
-4
lines changed

4 files changed

+964
-4
lines changed

assets/metadata/gift.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
},
1919
{
2020
"trait_type": "Backing Asset Count",
21-
"value": 0
21+
"value": 1
2222
},
2323
{
2424
"trait_type": "Backing Asset",

src/FraxiversarryEthereum.sol

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
/**
5+
* ====================================================================
6+
* | ______ _______ |
7+
* | / _____________ __ __ / ____(_____ ____ _____ ________ |
8+
* | / /_ / ___/ __ `| |/_/ / /_ / / __ \/ __ `/ __ \/ ___/ _ \ |
9+
* | / __/ / / / /_/ _> < / __/ / / / / / /_/ / / / / /__/ __/ |
10+
* | /_/ /_/ \__,_/_/|_| /_/ /_/_/ /_/\__,_/_/ /_/\___/\___/ |
11+
* | |
12+
* ====================================================================
13+
* ========================= Fraxiversarry ============================
14+
* ====================================================================
15+
* Fraxiversarry NFT contract for the 5th anniversary of Frax Finance
16+
* Frax Finance: https://github.com/FraxFinance
17+
*/
18+
19+
import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";
20+
import {ERC721Enumerable} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
21+
import {ERC721Pausable} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721Pausable.sol";
22+
import {ERC721URIStorage} from "openzeppelin-contracts/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
23+
24+
import {IFraxiversarryErrors} from "./interfaces/IFraxiversarryErrors.sol";
25+
import {IERC6454} from "./interfaces/IERC6454.sol";
26+
import {IERC4906} from "openzeppelin-contracts/contracts/interfaces/IERC4906.sol";
27+
28+
import {ONFT721Core} from "@layerzerolabs/onft-evm/contracts/onft721/ONFT721Core.sol";
29+
import {IONFT721, SendParam} from "@layerzerolabs/onft-evm/contracts/onft721/interfaces/IONFT721.sol";
30+
import {ONFT721MsgCodec} from "@layerzerolabs/onft-evm/contracts/onft721/libs/ONFT721MsgCodec.sol";
31+
import {ONFTComposeMsgCodec} from "@layerzerolabs/onft-evm/contracts/libs/ONFTComposeMsgCodec.sol";
32+
import {IOAppMsgInspector} from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMsgInspector.sol";
33+
import {Origin} from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
34+
35+
/**
36+
* @title Fraxiversarry
37+
* @author Frax Finance
38+
* @notice Fraxiversarry Ethereum mirror smart contract to support cross-chain movement
39+
* @dev Soulbound restrictions are enforced via _update with a bridge-aware bypass used during ONFT operations
40+
* @dev Frax Reviewer(s) / Contributor(s)
41+
* Jan Turk: https://github.com/ThunderDeliverer
42+
* Sam Kazemian: https://github.com/samkazemian
43+
* Bjirke (honorary mention for the original idea)
44+
*/
45+
contract Fraxiversarry is
46+
ERC721,
47+
ERC721Enumerable,
48+
ERC721URIStorage,
49+
ERC721Pausable,
50+
IERC6454,
51+
IFraxiversarryErrors,
52+
ONFT721Core
53+
{
54+
using ONFT721MsgCodec for bytes;
55+
using ONFT721MsgCodec for bytes32;
56+
57+
/// @notice Marks whether a tokenId is non-transferable under IERC6454 rules
58+
/// @dev tokenId Fraxiversarry token ID to check
59+
/// @dev nonTransferable True if the token is soulbound
60+
mapping(uint256 tokenId => bool nonTransferable) public isNonTransferrable;
61+
62+
/// @dev Flag that disables soulbound checks during bridge operations
63+
bool private _isBridgeOperation;
64+
65+
/**
66+
* @notice Initializes Fraxiversarry with supply caps, fee settings, and ONFT configuration
67+
* @dev The mintingCutoffBlock is calculated assuming a fixed 2 second Fraxtal block time
68+
* @dev nextGiftTokenId starts immediately after the BASE tokenId range
69+
* @dev nextPremiumTokenId starts immediately after the GIFT tokenId range
70+
* @param _initialOwner Address that will own the contract and control admin functions
71+
* @param _lzEndpoint LayerZero endpoint used by ONFT721Core
72+
*/
73+
constructor(address _initialOwner, address _lzEndpoint)
74+
ERC721("Fraxiversarry", "FRAX5Y")
75+
ONFT721Core(_lzEndpoint, _initialOwner)
76+
{}
77+
78+
/**
79+
* @notice Returns the base URI for token metadata resolution
80+
* @dev The contract relies on per-token URIs for all token types
81+
* @return Empty base URI string
82+
*/
83+
function _baseURI() internal pure override returns (string memory) {
84+
return "";
85+
}
86+
87+
/**
88+
* @notice Pauses all transfers and minting that rely on ERC721Pausable checks
89+
* @dev Only the contract owner can pause the contract
90+
*/
91+
function pause() public onlyOwner {
92+
_pause();
93+
}
94+
95+
/**
96+
* @notice Unpauses the contract so transfers and minting can resume
97+
* @dev Only the contract owner can unpause the contract
98+
*/
99+
function unpause() public onlyOwner {
100+
_unpause();
101+
}
102+
103+
/**
104+
* @notice Updates the metadata URI for a specific existing token
105+
* @dev Only the contract owner can update token URIs
106+
* @dev Reverts if the token has been burned or never existed
107+
* @param _tokenId Token ID whose metadata URI will be updated
108+
* @param _uri New metadata URI for the _tokenId
109+
*/
110+
function updateSpecificTokenUri(uint256 _tokenId, string memory _uri) public onlyOwner {
111+
if (_ownerOf(_tokenId) == address(0)) revert TokenDoesNotExist();
112+
113+
_setTokenURI(_tokenId, _uri);
114+
}
115+
116+
/**
117+
* @notice Returns whether a tokenId is transferable under IERC6454 semantics
118+
* @dev This is a lightweight view that reflects the isNonTransferrable flag
119+
* @dev This function preserves the interface of IERC6454 even though _from and _to are unused
120+
* @param _tokenId Token ID to check
121+
* @param _from Current owner address provided for interface compliance
122+
* @param _to Intended recipient address provided for interface compliance
123+
* @return True if the token is not marked as non-transferable
124+
*/
125+
function isTransferable(uint256 _tokenId, address _from, address _to) public view override returns (bool) {
126+
return !isNonTransferrable[_tokenId];
127+
}
128+
129+
// ********** ONFT functional overrides **********
130+
131+
/**
132+
* @notice Returns the token address used by the ONFT interface
133+
* @dev For ONFT721 this must be the address of the NFT contract itself
134+
* @return Address of this contract
135+
*/
136+
function token() external view override returns (address) {
137+
return address(this);
138+
}
139+
140+
/**
141+
* @notice Indicates whether explicit approvals are required for bridging operations
142+
* @dev The contract opts into a no-approval bridging model in ONFT721Core
143+
* @return False indicating approvals are not required
144+
*/
145+
function approvalRequired() public view override returns (bool) {
146+
return false;
147+
}
148+
149+
// ********** Internal functions to facilitate the ERC6454 functionality **********
150+
151+
/**
152+
* @notice Enforces soulbound restrictions during standard transfers and burns
153+
* @dev The check is bypassed during bridge operations to allow _debit and _credit flows
154+
* @param _tokenId Token ID to validate for transferability
155+
*/
156+
function _soulboundCheck(uint256 _tokenId) internal view {
157+
if (!_isBridgeOperation && isNonTransferrable[_tokenId]) revert CannotTransferSoulboundToken();
158+
}
159+
160+
// ********** Internal functions to facilitate the ONFT operations **********
161+
162+
/**
163+
* @notice Performs a bridge-aware burn that preserves token-linked ERC20 state
164+
* @dev This is not a full burn of storage and is used by _debit to represent
165+
* a token leaving the source chain
166+
* @param _owner Current owner of the token being bridged out
167+
* @param _tokenId Token ID to bridge-burn
168+
*/
169+
function _bridgeBurn(address _owner, uint256 _tokenId) internal {
170+
_isBridgeOperation = true;
171+
// Token should only be burned, but the state including ERC20 balances should be preserved
172+
_update(address(0), _tokenId, _owner);
173+
_isBridgeOperation = false;
174+
}
175+
176+
/**
177+
* @notice Debits a token from the source chain during an ONFT send
178+
* @dev Validates approval when the caller is not the owner
179+
* @dev Uses _bridgeBurn to preserve ERC20 state for later credit on the destination chain
180+
* @param _from Address initiating the debit which must be owner or approved
181+
* @param _tokenId Token ID to debit from the source chain
182+
* @param _dstEid Destination endpoint ID provided by ONFT721Core (unused)
183+
*/
184+
function _debit(address _from, uint256 _tokenId, uint32 _dstEid) internal override {
185+
address owner = ownerOf(_tokenId);
186+
187+
if (_from != owner && !isApprovedForAll(owner, _from) && getApproved(_tokenId) != _from) {
188+
revert ERC721InsufficientApproval(_from, _tokenId);
189+
}
190+
191+
_bridgeBurn(owner, _tokenId);
192+
}
193+
194+
/**
195+
* @notice Credits a token on the destination chain during an ONFT receive
196+
* @dev Reverts if the token already exists on the destination chain
197+
* @dev Uses a bridge-aware update that bypasses soulbound checks
198+
* @param _to Address that will receive ownership on the destination chain
199+
* @param _tokenId Token ID to credit on the destination chain
200+
* @param _srcEid Source endpoint ID provided by ONFT721Core
201+
*/
202+
function _credit(address _to, uint256 _tokenId, uint32 _srcEid) internal override {
203+
if (_ownerOf(_tokenId) != address(0)) revert TokenAlreadyExists(_tokenId);
204+
205+
_isBridgeOperation = true;
206+
_update(_to, _tokenId, address(0));
207+
_isBridgeOperation = false;
208+
}
209+
210+
/**
211+
* @notice Builds the ONFT message and options payload for cross-chain sends
212+
* @dev Encodes tokenURI and soulbound flag into the composed message
213+
* @dev Reverts if the receiver is zero or if a soulbound token is sent to a non-owner address
214+
* @param _sendParam SendParam struct containing destination and token data
215+
* @return _message Encoded ONFT message to be dispatched
216+
* @return _options Encoded LayerZero options for the send
217+
*/
218+
function _buildMsgAndOptions(SendParam calldata _sendParam)
219+
internal
220+
view
221+
override
222+
returns (bytes memory _message, bytes memory _options)
223+
{
224+
if (_sendParam.to == bytes32(0)) revert InvalidReceiver();
225+
226+
string memory tokenUri = tokenURI(_sendParam.tokenId);
227+
bool isSoulbound = isNonTransferrable[_sendParam.tokenId];
228+
bytes memory composedMessage = abi.encode(tokenUri, isSoulbound);
229+
230+
if (isSoulbound && _sendParam.to.bytes32ToAddress() != ownerOf(_sendParam.tokenId)) {
231+
revert CannotTransferSoulboundToken();
232+
}
233+
234+
bool hasCompose;
235+
(_message, hasCompose) = ONFT721MsgCodec.encode(_sendParam.to, _sendParam.tokenId, composedMessage);
236+
237+
uint16 msgType = hasCompose ? SEND_AND_COMPOSE : SEND;
238+
_options = combineOptions(_sendParam.dstEid, msgType, _sendParam.extraOptions);
239+
240+
address inspector = msgInspector;
241+
if (inspector != address(0)) IOAppMsgInspector(inspector).inspect(_message, _options);
242+
}
243+
244+
/**
245+
* @notice Receives a composed ONFT message and reconstructs token state on the destination chain
246+
* @dev Expects a composed message that includes tokenURI and soulbound flag
247+
* @dev Calls _credit before applying the token URI and soulbound flag locally
248+
* @param _origin Origin struct containing srcEid and nonce data
249+
* @param _guid Global unique identifier for the LayerZero message
250+
* @param _message Encoded ONFT message containing composed payload
251+
* @param _executor Unused executor parameter for LayerZero interface compatibility
252+
* @param _executorData Unused executor data parameter for LayerZero interface compatibility
253+
*/
254+
function _lzReceive(
255+
Origin calldata _origin,
256+
bytes32 _guid,
257+
bytes calldata _message,
258+
address _executor,
259+
bytes calldata _executorData
260+
) internal override {
261+
address toAddress = _message.sendTo().bytes32ToAddress();
262+
uint256 tokenId = _message.tokenId();
263+
264+
if (!_message.isComposed()) revert MissingComposedMessage();
265+
266+
bytes memory rawCompose = _message.composeMsg();
267+
bytes memory rawMessage = rawCompose;
268+
uint256 len;
269+
assembly {
270+
len := mload(rawCompose)
271+
// shift pointer forward by 32 bytes (skip fromOApp word)
272+
rawMessage := add(rawMessage, 32)
273+
// set length = originalLength - 32
274+
mstore(rawMessage, sub(len, 32))
275+
}
276+
277+
(string memory tokenUri, bool isSoulbound) = abi.decode(rawMessage, (string, bool));
278+
279+
_credit(toAddress, tokenId, _origin.srcEid);
280+
_setTokenURI(tokenId, tokenUri);
281+
isNonTransferrable[tokenId] = isSoulbound;
282+
283+
bytes32 composeFrom = ONFTComposeMsgCodec.addressToBytes32(address(this));
284+
bytes memory composeInnerMsg = abi.encode(tokenUri, isSoulbound);
285+
bytes memory composeMsg = abi.encodePacked(composeFrom, composeInnerMsg);
286+
287+
bytes memory composedMsgEncoded = ONFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, composeMsg);
288+
endpoint.sendCompose(toAddress, _guid, 0, composedMsgEncoded);
289+
290+
emit ONFTReceived(_guid, _origin.srcEid, toAddress, tokenId);
291+
}
292+
293+
// ********** The following functions are overrides required by Solidity. **********
294+
295+
/**
296+
* @notice Central transfer hook used by ERC721, Enumerable, and Pausable logic
297+
* @dev Enforces soulbound rules via _soulboundCheck before delegating to OZ logic
298+
* @param _to Address receiving the token
299+
* @param _tokenId Token ID being updated
300+
* @param _auth Address attempting to authorize the update
301+
* @return Previous owner address returned by the parent implementation
302+
*/
303+
function _update(address _to, uint256 _tokenId, address _auth)
304+
internal
305+
override(ERC721, ERC721Enumerable, ERC721Pausable)
306+
returns (address)
307+
{
308+
_soulboundCheck(_tokenId);
309+
return super._update(_to, _tokenId, _auth);
310+
}
311+
312+
/**
313+
* @notice Resolves the multiple inheritance requirement for _increaseBalance
314+
* @dev Delegates to the OZ implementation to preserve Enumerable invariants
315+
* @param _account Address whose balance is increased
316+
* @param _value Amount of balance increase
317+
*/
318+
function _increaseBalance(address _account, uint128 _value) internal override(ERC721, ERC721Enumerable) {
319+
super._increaseBalance(_account, _value);
320+
}
321+
322+
/**
323+
* @notice Sets a token URI and emits an IERC4906 MetadataUpdate event
324+
* @dev This override ensures metadata refresh signals are emitted for indexers
325+
* @param _tokenId Token ID whose URI is being updated
326+
* @param _tokenUri New token URI to assign
327+
*/
328+
function _setTokenURI(uint256 _tokenId, string memory _tokenUri) internal override {
329+
super._setTokenURI(_tokenId, _tokenUri);
330+
emit MetadataUpdate(_tokenId);
331+
}
332+
333+
/**
334+
* @notice Returns the token URI for a given tokenId
335+
* @dev Resolves the multiple inheritance between ERC721 and ERC721URIStorage
336+
* @param _tokenId Token ID whose URI will be returned
337+
* @return Token URI string
338+
*/
339+
function tokenURI(uint256 _tokenId) public view override(ERC721, ERC721URIStorage) returns (string memory) {
340+
return super.tokenURI(_tokenId);
341+
}
342+
343+
/**
344+
* @notice Declares supported interfaces across ERC721 extensions and custom standards
345+
* @dev Includes IERC7590, IERC6454, IERC4906, and IONFT721 support
346+
* @param _interfaceId Interface identifier to check
347+
* @return True if the interface is supported
348+
*/
349+
function supportsInterface(bytes4 _interfaceId)
350+
public
351+
view
352+
override(ERC721, ERC721Enumerable, ERC721URIStorage)
353+
returns (bool)
354+
{
355+
return super.supportsInterface(_interfaceId) || _interfaceId == type(IERC6454).interfaceId
356+
|| _interfaceId == type(IERC4906).interfaceId || _interfaceId == type(IONFT721).interfaceId;
357+
}
358+
}

0 commit comments

Comments
 (0)