|
| 1 | +// SPDX-License-Identifier: Apache 2 |
| 2 | +pragma solidity ^0.8.19; |
| 3 | + |
| 4 | +import "wormhole-solidity-sdk/libraries/BytesParsing.sol"; |
| 5 | +import "wormhole-solidity-sdk/interfaces/IWormhole.sol"; |
| 6 | +import "wormhole-solidity-sdk/Utils.sol"; |
| 7 | + |
| 8 | +import "../../interfaces/IMsgReceiver.sol"; |
| 9 | +import "../../interfaces/ISharedWormholeTransceiver.sol"; |
| 10 | + |
| 11 | +string constant sharedWormholeTransceiverVersionString = "SharedWormholeTransceiver-0.0.1"; |
| 12 | + |
| 13 | +/// @title SharedWormholeTransceiver |
| 14 | +/// |
| 15 | +/// @author Wormhole Project Contributors. |
| 16 | +/// |
| 17 | +/// @notice The SharedWormholeTransceiver is a Wormhole transceiver implementation that can |
| 18 | +/// be shared between multiple managers. It implements the ITransceiver interface. |
| 19 | +/// |
| 20 | +/// The SharedWormholeTransceiver assumes the use of the Executor at the manager level, |
| 21 | +/// so it has no internal relayer support. It currently requires no transceiver instructions, |
| 22 | +/// so the parameter required by the interface is not used. |
| 23 | +/// |
| 24 | +/// Since the ITransceiver interface requires NttManager and token addresses, this transceiver |
| 25 | +/// implements those interfaces, but they are not used, and the values returned are zero. |
| 26 | +/// |
| 27 | +/// Since this transceiver is not owned by a specific manager, some of the interface |
| 28 | +/// functions are stubbed off (transferring ownership, for instance). Additionally, |
| 29 | +/// this transceiver is immutable, so the upgrade function reverts. |
| 30 | +/// |
| 31 | +/// This transceiver has an admin who is responsible for provisioning peers. There are |
| 32 | +/// a number of admin functions for provisioning peers and transferring the admin. |
| 33 | +/// Additionally, there is a function to discard the admin, making the contract |
| 34 | +/// truly immutable. |
| 35 | +/// |
| 36 | +/// This transceiver maintains the Wormhole transceiver wire format, so in theory |
| 37 | +/// it should be able to peer with instances of the standard `WormholeTransceiver`. |
| 38 | +/// |
| 39 | +contract SharedWormholeTransceiver is ISharedWormholeTransceiver { |
| 40 | + using BytesParsing for bytes; // Used by _decodePayload |
| 41 | + |
| 42 | + // ==================== Constants ================================================ |
| 43 | + // TODO: These are in `WormholeTranceiverState.sol` but I can't access them for some reason. |
| 44 | + // TODO: Do we need to publish `WH_TRANSCEIVER_INIT_PREFIX` and `WH_PEER_REGISTRATION_PREFIX`? |
| 45 | + |
| 46 | + /// @dev Prefix for all TransceiverMessage payloads |
| 47 | + /// @notice Magic string (constant value set by messaging provider) that idenfies the payload as an transceiver-emitted payload. |
| 48 | + /// Note that this is not a security critical field. It's meant to be used by messaging providers to identify which messages are Transceiver-related. |
| 49 | + bytes4 public constant WH_TRANSCEIVER_PAYLOAD_PREFIX = 0x9945FF10; |
| 50 | + |
| 51 | + /// @dev Prefix for all Wormhole transceiver initialisation payloads |
| 52 | + /// This is bytes4(keccak256("WormholeTransceiverInit")) |
| 53 | + bytes4 constant WH_TRANSCEIVER_INIT_PREFIX = 0x9c23bd3b; |
| 54 | + |
| 55 | + /// @dev Prefix for all Wormhole peer registration payloads |
| 56 | + /// This is bytes4(keccak256("WormholePeerRegistration")) |
| 57 | + bytes4 constant WH_PEER_REGISTRATION_PREFIX = 0x18fc67c2; |
| 58 | + |
| 59 | + // ==================== Immutables =============================================== |
| 60 | + |
| 61 | + address public admin; |
| 62 | + address public pendingAdmin; |
| 63 | + uint16 public immutable ourChain; |
| 64 | + IWormhole public immutable wormhole; |
| 65 | + uint8 public immutable consistencyLevel; |
| 66 | + |
| 67 | + // ==================== Constructor ============================================== |
| 68 | + |
| 69 | + constructor(uint16 _ourChain, address _admin, address _wormhole, uint8 _consistencyLevel) { |
| 70 | + assert(_ourChain != 0); |
| 71 | + assert(_admin != address(0)); |
| 72 | + assert(_wormhole != address(0)); |
| 73 | + // Not checking consistency level since maybe zero is valid? |
| 74 | + ourChain = _ourChain; |
| 75 | + admin = _admin; |
| 76 | + wormhole = IWormhole(_wormhole); |
| 77 | + consistencyLevel = _consistencyLevel; |
| 78 | + } |
| 79 | + |
| 80 | + // =============== Storage Keys ============================================= |
| 81 | + |
| 82 | + bytes32 private constant WORMHOLE_PEERS_SLOT = bytes32(uint256(keccak256("swt.peers")) - 1); |
| 83 | + bytes32 private constant CHAINS_SLOT = bytes32(uint256(keccak256("swt.chains")) - 1); |
| 84 | + |
| 85 | + // =============== Storage Accessors ======================================== |
| 86 | + |
| 87 | + function _getPeersStorage() internal pure returns (mapping(uint16 => bytes32) storage $) { |
| 88 | + uint256 slot = uint256(WORMHOLE_PEERS_SLOT); |
| 89 | + assembly ("memory-safe") { |
| 90 | + $.slot := slot |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + function _getChainsStorage() internal pure returns (uint16[] storage $) { |
| 95 | + uint256 slot = uint256(CHAINS_SLOT); |
| 96 | + assembly ("memory-safe") { |
| 97 | + $.slot := slot |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + // =============== Public Getters ====================================================== |
| 102 | + |
| 103 | + /// @inheritdoc ISharedWormholeTransceiver |
| 104 | + function getPeer( |
| 105 | + uint16 chainId |
| 106 | + ) public view returns (bytes32 peerContract) { |
| 107 | + peerContract = _getPeersStorage()[chainId]; |
| 108 | + if (peerContract == bytes32(0)) { |
| 109 | + revert UnregisteredPeer(chainId); |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + /// @inheritdoc ISharedWormholeTransceiver |
| 114 | + function getPeers() public view returns (PeerEntry[] memory results) { |
| 115 | + uint16[] storage chains = _getChainsStorage(); |
| 116 | + uint256 len = chains.length; |
| 117 | + results = new PeerEntry[](len); |
| 118 | + for (uint256 idx = 0; idx < len;) { |
| 119 | + results[idx].chain = chains[idx]; |
| 120 | + results[idx].addr = getPeer(chains[idx]); |
| 121 | + unchecked { |
| 122 | + ++idx; |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // =============== Admin =============================================================== |
| 128 | + |
| 129 | + /// @inheritdoc ISharedWormholeTransceiver |
| 130 | + function updateAdmin( |
| 131 | + address newAdmin |
| 132 | + ) external onlyAdmin { |
| 133 | + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. |
| 134 | + // - This is handled by onlyAdmin. |
| 135 | + |
| 136 | + // SPEC: If possible, MUST NOT allow the admin to discard admin via this command (e.g. newAdmin != address(0) on EVM) |
| 137 | + if (newAdmin == address(0)) { |
| 138 | + revert InvalidAdminZeroAddress(); |
| 139 | + } |
| 140 | + |
| 141 | + // SPEC: Immediately sets newAdmin as the admin of the integrator. |
| 142 | + admin = newAdmin; |
| 143 | + emit AdminUpdated(msg.sender, newAdmin); |
| 144 | + } |
| 145 | + |
| 146 | + /// @inheritdoc ISharedWormholeTransceiver |
| 147 | + function transferAdmin( |
| 148 | + address newAdmin |
| 149 | + ) external onlyAdmin { |
| 150 | + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. |
| 151 | + // - This is handled by onlyAdmin. |
| 152 | + |
| 153 | + // SPEC: If possible, MUST NOT allow the admin to discard admin via this command (e.g. `newAdmin != address(0)` on EVM). |
| 154 | + if (newAdmin == address(0)) { |
| 155 | + revert InvalidAdminZeroAddress(); |
| 156 | + } |
| 157 | + |
| 158 | + // SPEC: Initiates the first step of a two-step process in which the current admin (to cancel) or new admin must claim. |
| 159 | + pendingAdmin = newAdmin; |
| 160 | + emit AdminUpdateRequested(msg.sender, newAdmin); |
| 161 | + } |
| 162 | + |
| 163 | + /// @inheritdoc ISharedWormholeTransceiver |
| 164 | + function claimAdmin() external { |
| 165 | + // This doesn't use onlyAdmin because the pending admin must be non-zero. |
| 166 | + |
| 167 | + // SPEC: MUST check that the caller is the current admin OR the pending admin. |
| 168 | + if ((admin != msg.sender) && (pendingAdmin != msg.sender)) { |
| 169 | + revert CallerNotAdmin(msg.sender); |
| 170 | + } |
| 171 | + |
| 172 | + // SPEC: MUST check that there is an admin transfer pending (e. g. pendingAdmin != address(0) on EVM). |
| 173 | + if (pendingAdmin == address(0)) { |
| 174 | + revert NoAdminUpdatePending(); |
| 175 | + } |
| 176 | + |
| 177 | + // SPEC: Cancels / Completes the second step of the two-step transfer. Sets the admin to the caller and clears the pending admin. |
| 178 | + address oldAdmin = admin; |
| 179 | + admin = msg.sender; |
| 180 | + pendingAdmin = address(0); |
| 181 | + emit AdminUpdated(oldAdmin, msg.sender); |
| 182 | + } |
| 183 | + |
| 184 | + /// @inheritdoc ISharedWormholeTransceiver |
| 185 | + function discardAdmin() external onlyAdmin { |
| 186 | + // SPEC: MUST check that the caller is the current admin and there is not a pending transfer. |
| 187 | + // - This is handled by onlyAdmin. |
| 188 | + |
| 189 | + // SPEC: Clears the current admin. THIS IS NOT REVERSIBLE. This ensures that the Integrator configuration becomes immutable. |
| 190 | + admin = address(0); |
| 191 | + emit AdminDiscarded(msg.sender); |
| 192 | + } |
| 193 | + |
| 194 | + /// @inheritdoc ISharedWormholeTransceiver |
| 195 | + function setPeer(uint16 peerChain, bytes32 peerContract) external onlyAdmin { |
| 196 | + if (peerChain == 0 || peerChain == ourChain) { |
| 197 | + revert InvalidChain(peerChain); |
| 198 | + } |
| 199 | + if (peerContract == bytes32(0)) { |
| 200 | + revert InvalidPeerZeroAddress(); |
| 201 | + } |
| 202 | + |
| 203 | + bytes32 oldPeerContract = _getPeersStorage()[peerChain]; |
| 204 | + |
| 205 | + // SPEC: MUST not set the peer if it is already set. |
| 206 | + if (oldPeerContract != bytes32(0)) { |
| 207 | + revert PeerAlreadySet(peerChain, oldPeerContract); |
| 208 | + } |
| 209 | + |
| 210 | + _getPeersStorage()[peerChain] = peerContract; |
| 211 | + _getChainsStorage().push(peerChain); |
| 212 | + emit PeerAdded(peerChain, peerContract); |
| 213 | + } |
| 214 | + |
| 215 | + // =============== ITransceiver Interface ============================================== |
| 216 | + |
| 217 | + /// @inheritdoc ITransceiver |
| 218 | + function getTransceiverType() external pure virtual returns (string memory) { |
| 219 | + return sharedWormholeTransceiverVersionString; |
| 220 | + } |
| 221 | + |
| 222 | + /// @inheritdoc ITransceiver |
| 223 | + function quoteDeliveryPrice( |
| 224 | + uint16, // recipientChain |
| 225 | + TransceiverStructs.TransceiverInstruction calldata // instruction |
| 226 | + ) external view virtual returns (uint256) { |
| 227 | + return wormhole.messageFee(); |
| 228 | + } |
| 229 | + |
| 230 | + /// @inheritdoc ITransceiver |
| 231 | + /// @dev The caller should set the delivery price in msg.value. |
| 232 | + /// @dev This transceiver does not use instructions, so that parameter is ignored. |
| 233 | + /// @dev This transceiver does not use refundAddress, so that parameter is ignored. |
| 234 | + function sendMessage( |
| 235 | + uint16 recipientChain, |
| 236 | + TransceiverStructs.TransceiverInstruction memory, // instruction, |
| 237 | + bytes memory nttManagerMessage, |
| 238 | + bytes32 recipientNttManagerAddress, |
| 239 | + bytes32 // refundAddress |
| 240 | + ) external payable virtual { |
| 241 | + ( |
| 242 | + TransceiverStructs.TransceiverMessage memory transceiverMessage, |
| 243 | + bytes memory encodedTransceiverPayload |
| 244 | + ) = TransceiverStructs.buildAndEncodeTransceiverMessage( |
| 245 | + WH_TRANSCEIVER_PAYLOAD_PREFIX, |
| 246 | + toWormholeFormat(msg.sender), |
| 247 | + recipientNttManagerAddress, |
| 248 | + nttManagerMessage, |
| 249 | + new bytes(0) |
| 250 | + ); |
| 251 | + |
| 252 | + wormhole.publishMessage{value: msg.value}(0, encodedTransceiverPayload, consistencyLevel); |
| 253 | + emit SendTransceiverMessage(recipientChain, transceiverMessage); |
| 254 | + } |
| 255 | + |
| 256 | + /// @inheritdoc ITransceiver |
| 257 | + /// @dev This transciever does not have a specific NttManager, so this function just reverts. |
| 258 | + function getNttManagerOwner() external pure returns (address) { |
| 259 | + revert NotImplemented(); |
| 260 | + } |
| 261 | + |
| 262 | + /// @inheritdoc ITransceiver |
| 263 | + /// @dev This transciever does not have a specific NttManager or token, so this function just reverts. |
| 264 | + function getNttManagerToken() external pure returns (address) { |
| 265 | + revert NotImplemented(); |
| 266 | + } |
| 267 | + |
| 268 | + /// @inheritdoc ITransceiver |
| 269 | + /// @dev Since shared transceivers are not owned by the manager, this function does nothing. |
| 270 | + /// We don't want to revert because a manager may have both shared and unshared transceivers. |
| 271 | + /// It should be able to call this without without worrying about the transceiver type. |
| 272 | + function transferTransceiverOwnership( |
| 273 | + address newOwner |
| 274 | + ) external {} |
| 275 | + |
| 276 | + /// @inheritdoc ITransceiver |
| 277 | + /// @dev Since shared transceivers are immutable, this just reverts. |
| 278 | + function upgrade( |
| 279 | + address // newImplementation |
| 280 | + ) external pure { |
| 281 | + revert NotUpgradable(); |
| 282 | + } |
| 283 | + |
| 284 | + // =============== ISharedWormholeTransceiver Interface ================================ |
| 285 | + |
| 286 | + /// @inheritdoc ISharedWormholeTransceiver |
| 287 | + function receiveMessage( |
| 288 | + bytes calldata encodedMessage |
| 289 | + ) external { |
| 290 | + // Verify the wormhole message and extract the source chain and payload. |
| 291 | + (uint16 sourceChainId, bytes memory payload) = _verifyMessage(encodedMessage); |
| 292 | + |
| 293 | + // TODO: There is no check that this message is intended for this chain, and I don't see how to do it! |
| 294 | + |
| 295 | + // Parse the encoded Transceiver payload and the encapsulated manager message. |
| 296 | + TransceiverStructs.TransceiverMessage memory parsedTransceiverMessage; |
| 297 | + TransceiverStructs.NttManagerMessage memory parsedNttManagerMessage; |
| 298 | + (parsedTransceiverMessage, parsedNttManagerMessage) = TransceiverStructs |
| 299 | + .parseTransceiverAndNttManagerMessage(WH_TRANSCEIVER_PAYLOAD_PREFIX, payload); |
| 300 | + |
| 301 | + // We use recipientNttManagerAddress to deliver the message so it must be set. |
| 302 | + if (parsedTransceiverMessage.recipientNttManagerAddress == bytes32(0)) { |
| 303 | + revert RecipientManagerAddressIsZero(); |
| 304 | + } |
| 305 | + |
| 306 | + // Forward the message to the specified manager. |
| 307 | + IMsgReceiver(fromWormholeFormat(parsedTransceiverMessage.recipientNttManagerAddress)) |
| 308 | + .attestationReceived( |
| 309 | + sourceChainId, parsedTransceiverMessage.sourceNttManagerAddress, parsedNttManagerMessage |
| 310 | + ); |
| 311 | + |
| 312 | + // We don't need to emit an event here because _verifyMessage already did. |
| 313 | + } |
| 314 | + |
| 315 | + // ============= Internal =============================================================== |
| 316 | + |
| 317 | + function _verifyMessage( |
| 318 | + bytes memory encodedMessage |
| 319 | + ) internal returns (uint16, bytes memory) { |
| 320 | + // Verify VAA against Wormhole Core Bridge contract. |
| 321 | + (IWormhole.VM memory vm, bool valid, string memory reason) = |
| 322 | + wormhole.parseAndVerifyVM(encodedMessage); |
| 323 | + if (!valid) { |
| 324 | + revert InvalidVaa(reason); |
| 325 | + } |
| 326 | + |
| 327 | + // Ensure that the message came from the registered peer contract. |
| 328 | + if (getPeer(vm.emitterChainId) != vm.emitterAddress) { |
| 329 | + revert InvalidPeer(vm.emitterChainId, vm.emitterAddress); |
| 330 | + } |
| 331 | + |
| 332 | + emit ReceivedMessage(vm.hash, vm.emitterChainId, vm.emitterAddress, vm.sequence); |
| 333 | + return (vm.emitterChainId, vm.payload); |
| 334 | + } |
| 335 | + |
| 336 | + // =============== MODIFIERS =============================================== |
| 337 | + |
| 338 | + modifier onlyAdmin() { |
| 339 | + if (admin != msg.sender) { |
| 340 | + revert CallerNotAdmin(msg.sender); |
| 341 | + } |
| 342 | + if (pendingAdmin != address(0)) { |
| 343 | + revert AdminTransferPending(); |
| 344 | + } |
| 345 | + _; |
| 346 | + } |
| 347 | +} |
0 commit comments