|
| 1 | +// SPDX-License-Identifier: MIT |
| 2 | +pragma solidity ^0.8.20; |
| 3 | + |
| 4 | +import { OFTAdapterUpgradeable } from "@layerzerolabs/oft-evm-upgradeable/contracts/oft/OFTAdapterUpgradeable.sol"; |
| 5 | +import { EnforcedOptionParam } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppOptionsType3.sol"; |
| 6 | +import { AccessControlUpgradeable } from "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; |
| 7 | +import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; |
| 8 | +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; |
| 9 | +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; |
| 10 | + |
| 11 | +import { TransferLimiterV2 } from "./TransferLimiterV2.sol"; |
| 12 | + |
| 13 | +/** |
| 14 | + * @title ListaOFTAdapterV2 |
| 15 | + * @notice Upgradeable LayerZero V2 OFT Adapter for the canonical Lista (LISTA) |
| 16 | + * ERC20 token that lives natively on BNB Chain. |
| 17 | + * |
| 18 | + * @dev Bridging model: |
| 19 | + * Native BSC LISTA --(lock)--> ListaOFTAdapterV2 --> LayerZero --> ListaOFTv2 (mint) @ ETH |
| 20 | + * and the reverse on the way back (burn @ ETH -> unlock @ BSC). |
| 21 | + * |
| 22 | + * This is a *lockbox* adapter: the canonical supply is escrowed inside this |
| 23 | + * contract and a 1:1 backed representation is minted on remote chains. Per |
| 24 | + * LayerZero rules ONLY ONE OFT Adapter may exist for a given token mesh. |
| 25 | + * |
| 26 | + * Access control: |
| 27 | + * - DEFAULT_ADMIN_ROLE : upgrade authority, role administration + setPeer |
| 28 | + * (the cross-chain trust anchor). |
| 29 | + * - MANAGER : LayerZero OApp configuration (enforced options, msg |
| 30 | + * inspector, precrime), transfer limiter |
| 31 | + * configuration and unpause(). |
| 32 | + * - PAUSER : pause() the bridge in an emergency. |
| 33 | + * |
| 34 | + * Upgradeability: |
| 35 | + * - UUPS; only DEFAULT_ADMIN_ROLE may authorize an implementation upgrade. |
| 36 | + * - `token` and `lzEndpoint` are immutables baked into the implementation |
| 37 | + * bytecode by the LayerZero base contracts, hence supplied to the |
| 38 | + * constructor and reused across upgrades. |
| 39 | + */ |
| 40 | +contract ListaOFTAdapterV2 is |
| 41 | + OFTAdapterUpgradeable, |
| 42 | + AccessControlUpgradeable, |
| 43 | + PausableUpgradeable, |
| 44 | + UUPSUpgradeable, |
| 45 | + TransferLimiterV2 |
| 46 | +{ |
| 47 | + // @notice can pause the bridge in an emergency |
| 48 | + bytes32 public constant PAUSER = keccak256("PAUSER"); |
| 49 | + // @notice can configure the OApp + transfer limiter and unpause the bridge |
| 50 | + bytes32 public constant MANAGER = keccak256("MANAGER"); |
| 51 | + |
| 52 | + /** |
| 53 | + * @param _token The canonical LISTA ERC20 token on BNB Chain. |
| 54 | + * @param _lzEndpoint The LayerZero V2 endpoint on BNB Chain. |
| 55 | + * @dev Disables initializers on the implementation so it can only be used |
| 56 | + * behind a proxy. |
| 57 | + */ |
| 58 | + /// @custom:oz-upgrades-unsafe-allow constructor state-variable-immutable |
| 59 | + constructor(address _token, address _lzEndpoint) OFTAdapterUpgradeable(_token, _lzEndpoint) { |
| 60 | + _disableInitializers(); |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * @notice Initializes the proxy. |
| 65 | + * @param _admin The DEFAULT_ADMIN_ROLE holder (upgrade + role admin). Also set |
| 66 | + * as the Ownable owner to satisfy the few non-virtual LayerZero |
| 67 | + * owner-gated hooks. |
| 68 | + * @param _manager The MANAGER holder and LayerZero endpoint delegate. |
| 69 | + * @param _pauser The PAUSER holder. |
| 70 | + * @param _transferLimitConfigs Initial transfer limit configurations. |
| 71 | + */ |
| 72 | + function initialize( |
| 73 | + address _admin, |
| 74 | + address _manager, |
| 75 | + address _pauser, |
| 76 | + TransferLimit[] memory _transferLimitConfigs |
| 77 | + ) external initializer { |
| 78 | + require(_admin != address(0), "admin cannot be zero address"); |
| 79 | + require(_manager != address(0), "manager cannot be zero address"); |
| 80 | + require(_pauser != address(0), "pauser cannot be zero address"); |
| 81 | + |
| 82 | + // sets the LayerZero endpoint delegate to the manager |
| 83 | + __OFTAdapter_init(_manager); |
| 84 | + __Ownable_init(); |
| 85 | + __AccessControl_init(); |
| 86 | + __Pausable_init(); |
| 87 | + __UUPSUpgradeable_init(); |
| 88 | + |
| 89 | + // owner is used only by the non-virtual LayerZero owner hooks (eg. setDelegate) |
| 90 | + _transferOwnership(_admin); |
| 91 | + |
| 92 | + _grantRole(DEFAULT_ADMIN_ROLE, _admin); |
| 93 | + _grantRole(MANAGER, _manager); |
| 94 | + _grantRole(PAUSER, _pauser); |
| 95 | + |
| 96 | + _setTransferLimitConfigs(_transferLimitConfigs); |
| 97 | + } |
| 98 | + |
| 99 | + // -------------------------------------------------------------------------- |
| 100 | + // Transfer Limiter |
| 101 | + // -------------------------------------------------------------------------- |
| 102 | + |
| 103 | + /** |
| 104 | + * @notice Sets the transfer limit configurations. |
| 105 | + * @param _transferLimitConfigs An array of TransferLimit structures. |
| 106 | + */ |
| 107 | + function setTransferLimitConfigs( |
| 108 | + TransferLimit[] calldata _transferLimitConfigs |
| 109 | + ) external onlyRole(MANAGER) { |
| 110 | + _setTransferLimitConfigs(_transferLimitConfigs); |
| 111 | + } |
| 112 | + |
| 113 | + // -------------------------------------------------------------------------- |
| 114 | + // Pause control |
| 115 | + // -------------------------------------------------------------------------- |
| 116 | + |
| 117 | + /// @notice Pause the bridge. Callable by PAUSER. |
| 118 | + function pause() external onlyRole(PAUSER) { |
| 119 | + _pause(); |
| 120 | + } |
| 121 | + |
| 122 | + /// @notice Unpause the bridge. Callable by MANAGER. |
| 123 | + function unpause() external onlyRole(MANAGER) { |
| 124 | + _unpause(); |
| 125 | + } |
| 126 | + |
| 127 | + // -------------------------------------------------------------------------- |
| 128 | + // OFT debit / credit hooks (lock / unlock with limiter + pause guard) |
| 129 | + // -------------------------------------------------------------------------- |
| 130 | + |
| 131 | + /** |
| 132 | + * @dev Locks tokens on the source (this) chain when sending cross-chain. |
| 133 | + * Enforces the transfer limiter and the pause guard before locking. |
| 134 | + */ |
| 135 | + function _debit( |
| 136 | + address _from, |
| 137 | + uint256 _amountLD, |
| 138 | + uint256 _minAmountLD, |
| 139 | + uint32 _dstEid |
| 140 | + ) internal virtual override whenNotPaused returns (uint256 amountSentLD, uint256 amountReceivedLD) { |
| 141 | + // remove dust before checking, mirroring the value that is actually bridged |
| 142 | + uint256 _amount = _removeDust(_amountLD); |
| 143 | + _checkAndUpdateTransferLimit(_dstEid, _amount, _from); |
| 144 | + return super._debit(_from, _amountLD, _minAmountLD, _dstEid); |
| 145 | + } |
| 146 | + |
| 147 | + /** |
| 148 | + * @dev Unlocks tokens to the recipient when receiving cross-chain. |
| 149 | + * Blocked while the contract is paused. |
| 150 | + */ |
| 151 | + function _credit( |
| 152 | + address _to, |
| 153 | + uint256 _amountLD, |
| 154 | + uint32 _srcEid |
| 155 | + ) internal virtual override whenNotPaused returns (uint256 amountReceivedLD) { |
| 156 | + return super._credit(_to, _amountLD, _srcEid); |
| 157 | + } |
| 158 | + |
| 159 | + // -------------------------------------------------------------------------- |
| 160 | + // LayerZero OApp configuration |
| 161 | + // - setPeer (the cross-chain trust anchor) is restricted to DEFAULT_ADMIN_ROLE |
| 162 | + // - enforced options / msg inspector / preCrime are restricted to MANAGER |
| 163 | + // -------------------------------------------------------------------------- |
| 164 | + |
| 165 | + /// @notice Sets the trusted peer for a destination endpoint. Restricted to DEFAULT_ADMIN_ROLE |
| 166 | + /// (the cross-chain trust anchor; emergency severing uses pause()). |
| 167 | + function setPeer(uint32 _eid, bytes32 _peer) public override onlyRole(DEFAULT_ADMIN_ROLE) { |
| 168 | + _getOAppCoreStorage().peers[_eid] = _peer; |
| 169 | + emit PeerSet(_eid, _peer); |
| 170 | + } |
| 171 | + |
| 172 | + /// @notice Sets enforced LayerZero options. Restricted to MANAGER. |
| 173 | + function setEnforcedOptions( |
| 174 | + EnforcedOptionParam[] calldata _enforcedOptions |
| 175 | + ) public override onlyRole(MANAGER) { |
| 176 | + OAppOptionsType3Storage storage $ = _getOAppOptionsType3Storage(); |
| 177 | + for (uint256 i = 0; i < _enforcedOptions.length; i++) { |
| 178 | + _assertOptionsType3(_enforcedOptions[i].options); |
| 179 | + $.enforcedOptions[_enforcedOptions[i].eid][_enforcedOptions[i].msgType] = _enforcedOptions[i].options; |
| 180 | + } |
| 181 | + emit EnforcedOptionSet(_enforcedOptions); |
| 182 | + } |
| 183 | + |
| 184 | + /// @notice Sets the message inspector. Restricted to MANAGER. |
| 185 | + function setMsgInspector(address _msgInspector) public override onlyRole(MANAGER) { |
| 186 | + _getOFTCoreStorage().msgInspector = _msgInspector; |
| 187 | + emit MsgInspectorSet(_msgInspector); |
| 188 | + } |
| 189 | + |
| 190 | + /// @notice Sets the preCrime contract. Restricted to MANAGER. |
| 191 | + function setPreCrime(address _preCrime) public override onlyRole(MANAGER) { |
| 192 | + _getOAppPreCrimeSimulatorStorage().preCrime = _preCrime; |
| 193 | + emit PreCrimeSet(_preCrime); |
| 194 | + } |
| 195 | + |
| 196 | + // -------------------------------------------------------------------------- |
| 197 | + // Upgrade authorization |
| 198 | + // -------------------------------------------------------------------------- |
| 199 | + |
| 200 | + /// @dev Only DEFAULT_ADMIN_ROLE may upgrade the implementation. |
| 201 | + function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) {} |
| 202 | + |
| 203 | + uint256[50] private __gap; |
| 204 | +} |
0 commit comments