Last active
February 26, 2026 20:22
-
-
Save nazreen/bb89573f4e961b44c559d3274bebe163 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // SPDX-License-Identifier: BUSL-1.1 | |
| pragma solidity ^0.8.27; | |
| // OZ | |
| import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | |
| import { Ownable2Step } from "@openzeppelin/contracts/access/Ownable2Step.sol"; | |
| // LZ | |
| import { NativeOFTAdapter } from "./../lib/devtools/packages/oft-evm/contracts/NativeOFTAdapter.sol"; | |
| import { MessagingFee, MessagingReceipt } from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; | |
| import { SendParam, OFTReceipt } from "./../lib/devtools/packages/oft-evm/contracts/interfaces/IOFT.sol"; | |
| contract XNativeAdapter is NativeOFTAdapter, Ownable2Step { | |
| // XToken has a fixed total supply of 8_888_888_888 * 1e18 | |
| uint256 internal constant TOTAL_GLOBAL_SUPPLY = 8_888_888_888 ether; | |
| uint256 public lockedTokens; | |
| //--------------------------- Mappings --------------------------- | |
| // Outbound limits | |
| mapping(uint32 eid => uint256 outboundLimit) public outboundLimits; | |
| mapping(uint32 eid => uint256 sentTokenAmount) public sentTokenAmounts; | |
| mapping(uint32 eid => uint256 lastSentTimestamp) public lastSentTimestamps; | |
| // Inbound limits | |
| mapping(uint32 eid => uint256 inboundLimit) public inboundLimits; | |
| mapping(uint32 eid => uint256 receivedTokenAmount) public receivedTokenAmounts; | |
| mapping(uint32 eid => uint256 lastReceivedTimestamp) public lastReceivedTimestamps; | |
| // If an address is whitelisted, limit checks are skipped | |
| mapping(address addr => bool isWhitelisted) public whitelist; | |
| // if an address is an operator is can disconnect/connect bridges via setPeers | |
| mapping(address addr => bool isOperator) public operators; | |
| //--------------------------- Events --------------------------- | |
| event SetOutboundLimit(uint32 indexed eid, uint256 limit); | |
| event SetInboundLimit(uint32 indexed eid, uint256 limit); | |
| event SetWhitelist(address indexed addr, bool isWhitelist); | |
| event SetOperator(address indexed addr, bool isWhitelist); | |
| event TokensLocked(uint256 amount, uint256 totalLocked); | |
| event ReceivedTokenAmountReset(uint32 indexed eid); | |
| event SentTokenAmountReset(uint32 indexed eid); | |
| //--------------------------- Errors --------------------------- | |
| error ExceedInboundLimit(uint256 limit, uint256 amount); | |
| error ExceedOutboundLimit(uint256 limit, uint256 amount); | |
| error SendAndCallBlocked(); | |
| error ZeroValue(); | |
| error ExceedTotalGlobalSupply(); | |
| error NotOperator(); | |
| //--------------------------- Constructor --------------------------- | |
| // OAppCore inherits from Ownable | |
| /** | |
| * @dev decimals of token on the local chain is fixed to 18 (https://github.com/x/x/blob/main/src/x.sol#L36) | |
| * @param owner contract owner | |
| * @param lzEndpoint LayerZero endpoint address on the local chain | |
| * @param delegate The address capable of making OApp configurations inside of the endpoint. | |
| */ | |
| constructor(address owner, address lzEndpoint, address delegate) NativeOFTAdapter(18, lzEndpoint, delegate) Ownable(owner) { | |
| } | |
| //------------------------------------------- RATE LIMITS -------------------------------------------------------------- | |
| /** | |
| * @dev Owner to set the max daily limit on outbound x-chain transfers | |
| * @param eid Destination eid, as per LayerZero | |
| * @param limit Daily outbound limit | |
| */ | |
| function setOutboundLimit(uint32 eid, uint256 limit) external onlyOwner { | |
| outboundLimits[eid] = limit; | |
| emit SetOutboundLimit(eid, limit); | |
| } | |
| /** | |
| * @dev Owner to set the max daily limit on inbound x-chain transfers | |
| * @param eid Destination eid, as per LayerZero | |
| * @param limit Daily inbound limit | |
| */ | |
| function setInboundLimit(uint32 eid, uint256 limit) external onlyOwner { | |
| inboundLimits[eid] = limit; | |
| emit SetInboundLimit(eid, limit); | |
| } | |
| /** | |
| * @dev Owner to set whitelisted addresses - limits do not apply to these addresses | |
| * @param addr address | |
| * @param isWhitelisted true/false | |
| */ | |
| function setWhitelist(address addr, bool isWhitelisted) external onlyOwner { | |
| whitelist[addr] = isWhitelisted; | |
| emit SetWhitelist(addr, isWhitelisted); | |
| } | |
| /** | |
| * @notice Resets the accrued received amount for specified chain | |
| * @param eid The endpoint ID. | |
| * @dev Only owner of the OApp can call this function. | |
| */ | |
| function resetReceivedTokenAmount(uint32 eid) external onlyOwner { | |
| delete receivedTokenAmounts[eid]; | |
| emit ReceivedTokenAmountReset(eid); | |
| } | |
| /** | |
| * @notice Resets the accrued sent amount for specified chain | |
| * @param eid The endpoint ID. | |
| * @dev Only owner of the OApp can call this function. | |
| */ | |
| function resetSentTokenAmount(uint32 eid) external onlyOwner { | |
| delete sentTokenAmounts[eid]; | |
| emit SentTokenAmountReset(eid); | |
| } | |
| //------------------------------------------- OPERATORS -------------------------------------------------------------- | |
| /** | |
| * @dev Owner to set operator addresses - these addresses can call setPeers | |
| * @param addr address | |
| * @param isOperator true/false | |
| */ | |
| function setOperator(address addr, bool isOperator) external onlyOwner { | |
| operators[addr] = isOperator; | |
| emit SetOperator(addr, isOperator); | |
| } | |
| /** | |
| * @notice Resets the peer address (OApp instance) for a corresponding endpoint. | |
| * @param eid The endpoint ID. | |
| * @dev Only an operator or owner of the OApp can call this function. | |
| */ | |
| function resetPeer(uint32 eid) external { | |
| require(operators[msg.sender] || msg.sender == owner(), NotOperator()); | |
| peers[eid] = bytes32(0); | |
| emit PeerSet(eid, bytes32(0)); | |
| } | |
| //------------------------------------------- LZ Override -------------------------------------------------------------- | |
| /** | |
| * @dev Overwrite _debit to implement rate limits. | |
| * @dev Native tokens are not burned, but locked in the contract instead | |
| * @param from The address to debit 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. | |
| */ | |
| function _debit(address from, uint256 amountLD, uint256 minAmountLD, uint32 dstEid) internal override returns (uint256, uint256) { | |
| // Call parent logic (removes dust, checks fees, slippage, etc) | |
| (uint256 amountSentLD, uint256 amountReceivedLD) = super._debit(from, amountLD, minAmountLD, dstEid); | |
| // whitelisted addresses have no limits: proceed to send tokens | |
| if (whitelist[from]) return (amountSentLD, amountReceivedLD); | |
| // calculate sent token amount for this epoch | |
| uint256 sentTokenAmountsInThisEpoch; | |
| uint256 lastSentTimestamp = lastSentTimestamps[dstEid]; | |
| uint256 currTimestamp = block.timestamp; | |
| // Round down timestamps to the nearest day. | |
| if ((currTimestamp / (1 days)) > (lastSentTimestamp / (1 days))) { | |
| sentTokenAmountsInThisEpoch = amountSentLD; | |
| lastSentTimestamps[dstEid] = currTimestamp; | |
| } else { | |
| // sentTokenAmountsInThisEpoch = recentSentAmount + incomingSendAmount | |
| sentTokenAmountsInThisEpoch = sentTokenAmounts[dstEid] + amountSentLD; | |
| } | |
| // check against outboundLimit | |
| uint256 outboundLimit = outboundLimits[dstEid]; | |
| if (sentTokenAmountsInThisEpoch > outboundLimit) revert ExceedOutboundLimit(outboundLimit, sentTokenAmountsInThisEpoch); | |
| // update storage | |
| sentTokenAmounts[dstEid] = sentTokenAmountsInThisEpoch; | |
| return (amountSentLD, amountReceivedLD); | |
| } | |
| /** | |
| * @dev Credits tokens to the specified address. | |
| * @param to The address to credit the tokens to. | |
| * @param amountLD The amount of tokens to credit in local decimals. | |
| * @param srcEid The source chain ID. | |
| * @return amountReceivedLD The amount of tokens ACTUALLY received in local decimals. | |
| */ | |
| function _credit(address to, uint256 amountLD, uint32 srcEid) internal override returns (uint256) { | |
| uint256 amountReceivedLD = super._credit(to, amountLD, srcEid); | |
| // whiteslisted address have no limits | |
| if (whitelist[to]) return amountReceivedLD; | |
| uint256 receivedTokenAmountInThisEpoch; | |
| uint256 lastReceivedTimestamp = lastReceivedTimestamps[srcEid]; | |
| uint256 currTimestamp = block.timestamp; | |
| // Round down timestamps to the nearest day. | |
| if ((currTimestamp / (1 days)) > (lastReceivedTimestamp / (1 days))) { | |
| receivedTokenAmountInThisEpoch = amountReceivedLD; | |
| lastReceivedTimestamps[srcEid] = currTimestamp; | |
| } else { | |
| receivedTokenAmountInThisEpoch = receivedTokenAmounts[srcEid] + amountReceivedLD; | |
| } | |
| // ensure limit not exceeded | |
| uint256 inboundLimit = inboundLimits[srcEid]; | |
| if (receivedTokenAmountInThisEpoch > inboundLimit) revert ExceedInboundLimit(inboundLimit, receivedTokenAmountInThisEpoch); | |
| // update storage | |
| receivedTokenAmounts[srcEid] = receivedTokenAmountInThisEpoch; | |
| return amountReceivedLD; | |
| } | |
| /** Note: Override to block composed messages | |
| * @dev Executes the send operation. | |
| * @param _sendParam The parameters for the send operation. | |
| * @param _fee The calculated fee for the send() operation. | |
| * - nativeFee: The native fee. | |
| * - lzTokenFee: The lzToken fee. | |
| * @param _refundAddress The address to receive any excess funds. | |
| * @return msgReceipt The receipt for the send operation. | |
| * @return oftReceipt The OFT receipt information. | |
| * | |
| * @dev MessagingReceipt: LayerZero msg receipt | |
| * - guid: The unique identifier for the sent message. | |
| * - nonce: The nonce of the sent message. | |
| * - fee: The LayerZero fee incurred for the message. | |
| */ | |
| function _send(SendParam calldata _sendParam, MessagingFee calldata _fee, address _refundAddress) internal override returns (MessagingReceipt memory msgReceipt, OFTReceipt memory oftReceipt) { | |
| // @dev Applies the token transfers regarding this send() operation. | |
| // - amountSentLD is the amount in local decimals that was ACTUALLY sent from the sender. | |
| // - amountReceivedLD is the amount in local decimals that will be credited to the recipient on the remote OFT instance. | |
| (uint256 amountSentLD, uint256 amountReceivedLD) = _debit( | |
| msg.sender, | |
| _sendParam.amountLD, | |
| _sendParam.minAmountLD, | |
| _sendParam.dstEid | |
| ); | |
| // @dev Builds the options and OFT message to quote in the endpoint | |
| (bytes memory message, bytes memory options) = _buildMsgAndOptions(_sendParam, amountReceivedLD); | |
| // note: Block composed messages - custom logic | |
| if(isComposed(message)) revert SendAndCallBlocked(); | |
| // @dev Sends the message to the LayerZero endpoint and returns the LayerZero msg receipt. | |
| msgReceipt = _lzSend(_sendParam.dstEid, message, options, _fee, _refundAddress); | |
| // @dev Formulate the OFT receipt. | |
| oftReceipt = OFTReceipt(amountSentLD, amountReceivedLD); | |
| emit OFTSent(msgReceipt.guid, _sendParam.dstEid, msg.sender, amountSentLD, amountReceivedLD); | |
| } | |
| /** | |
| * @dev Checks if the OFT message is composed. Copied from OFTMsgCodec.sol | |
| * @param _msg The OFT message. | |
| * @return A boolean indicating whether the message is composed. | |
| */ | |
| function isComposed(bytes memory _msg) internal pure returns (bool) { | |
| // uint8 private constant SEND_AMOUNT_SD_OFFSET = 40; | |
| // return _msg.length > SEND_AMOUNT_SD_OFFSET | |
| return _msg.length > 40; | |
| } | |
| //------------------------------------------- OWNABLE2STEP -------------------------------------------------------------- | |
| /** | |
| * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. | |
| * Can only be called by the current owner. | |
| */ | |
| function transferOwnership(address newOwner) public override(Ownable, Ownable2Step) onlyOwner { | |
| Ownable2Step.transferOwnership(newOwner); | |
| } | |
| /** | |
| * @dev Transfers ownership of the contract to a new account (`newOwner`) and deletes any pending owner. | |
| * Internal function without access restriction. | |
| */ | |
| function _transferOwnership(address newOwner) internal override(Ownable, Ownable2Step) { | |
| Ownable2Step._transferOwnership(newOwner); | |
| } | |
| //--------------------------- Special Function --------------------------- | |
| /** | |
| * @notice For initial setup of the bridge. | |
| * @dev Only owner can lock native tokens; from the owner's balance | |
| * Reverts if the total locked tokens exceed the total global supply | |
| * Expects msg.value to be greater than 0. | |
| */ | |
| function lockTokens() external payable onlyOwner { | |
| require(msg.value > 0, ZeroValue()); | |
| require(lockedTokens + msg.value <= TOTAL_GLOBAL_SUPPLY, ExceedTotalGlobalSupply()); | |
| lockedTokens += msg.value; | |
| emit TokensLocked(msg.value, lockedTokens); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment