|
| 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