| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282 |
- // contracts/Bridge.sol
- // SPDX-License-Identifier: Apache 2
- pragma solidity ^0.8.0;
- import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
- import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
- import "../libraries/external/BytesLib.sol";
- import "./NFTBridgeGetters.sol";
- import "./NFTBridgeSetters.sol";
- import "./NFTBridgeStructs.sol";
- import "./NFTBridgeGovernance.sol";
- import "./token/NFT.sol";
- import "./token/NFTImplementation.sol";
- contract NFTBridge is NFTBridgeGovernance {
- using BytesLib for bytes;
- // Initiate a Transfer
- function transferNFT(address token, uint256 tokenID, uint16 recipientChain, bytes32 recipient, uint32 nonce) public payable returns (uint64 sequence) {
- // determine token parameters
- uint16 tokenChain;
- bytes32 tokenAddress;
- if (isWrappedAsset(token)) {
- tokenChain = NFTImplementation(token).chainId();
- tokenAddress = NFTImplementation(token).nativeContract();
- } else {
- tokenChain = chainId();
- tokenAddress = bytes32(uint256(uint160(token)));
- // Verify that the correct interfaces are implemented
- require(ERC165(token).supportsInterface(type(IERC721).interfaceId), "must support the ERC721 interface");
- require(ERC165(token).supportsInterface(type(IERC721Metadata).interfaceId), "must support the ERC721-Metadata extension");
- }
- string memory symbolString;
- string memory nameString;
- string memory uriString;
- {
- if (tokenChain != 1) { // SPL tokens use cache
- (,bytes memory queriedSymbol) = token.staticcall(abi.encodeWithSignature("symbol()"));
- (,bytes memory queriedName) = token.staticcall(abi.encodeWithSignature("name()"));
- symbolString = abi.decode(queriedSymbol, (string));
- nameString = abi.decode(queriedName, (string));
- }
- (,bytes memory queriedURI) = token.staticcall(abi.encodeWithSignature("tokenURI(uint256)", tokenID));
- uriString = abi.decode(queriedURI, (string));
- }
- bytes32 symbol;
- bytes32 name;
- if (tokenChain == 1) {
- // use cached SPL token info, as the contracts uses unified values
- NFTBridgeStorage.SPLCache memory cache = splCache(tokenID);
- symbol = cache.symbol;
- name = cache.name;
- clearSplCache(tokenID);
- } else {
- assembly {
- // first 32 bytes hold string length
- // mload then loads the next word, i.e. the first 32 bytes of the strings
- // NOTE: this means that we might end up with an
- // invalid utf8 string (e.g. if we slice an emoji in half). The VAA
- // payload specification doesn't require that these are valid utf8
- // strings, and it's cheaper to do any validation off-chain for
- // presentation purposes
- symbol := mload(add(symbolString, 32))
- name := mload(add(nameString, 32))
- }
- }
- if (tokenChain == chainId()) {
- IERC721(token).safeTransferFrom(msg.sender, address(this), tokenID);
- } else {
- NFTImplementation(token).burn(tokenID);
- }
- sequence = logTransfer(NFTBridgeStructs.Transfer({
- tokenAddress : tokenAddress,
- tokenChain : tokenChain,
- name : name,
- symbol : symbol,
- tokenID : tokenID,
- uri : uriString,
- to : recipient,
- toChain : recipientChain
- }), msg.value, nonce);
- }
- function logTransfer(NFTBridgeStructs.Transfer memory transfer, uint256 callValue, uint32 nonce) internal returns (uint64 sequence) {
- bytes memory encoded = encodeTransfer(transfer);
- sequence = wormhole().publishMessage{
- value : callValue
- }(nonce, encoded, 15);
- }
- function completeTransfer(bytes memory encodedVm) public {
- _completeTransfer(encodedVm);
- }
- // Execute a Transfer message
- function _completeTransfer(bytes memory encodedVm) internal {
- (IWormhole.VM memory vm, bool valid, string memory reason) = wormhole().parseAndVerifyVM(encodedVm);
- require(valid, reason);
- require(verifyBridgeVM(vm), "invalid emitter");
- NFTBridgeStructs.Transfer memory transfer = parseTransfer(vm.payload);
- require(!isTransferCompleted(vm.hash), "transfer already completed");
- setTransferCompleted(vm.hash);
- require(transfer.toChain == chainId(), "invalid target chain");
- IERC721 transferToken;
- if (transfer.tokenChain == chainId()) {
- transferToken = IERC721(address(uint160(uint256(transfer.tokenAddress))));
- } else {
- address wrapped = wrappedAsset(transfer.tokenChain, transfer.tokenAddress);
- // If the wrapped asset does not exist yet, create it
- if (wrapped == address(0)) {
- wrapped = _createWrapped(transfer.tokenChain, transfer.tokenAddress, transfer.name, transfer.symbol);
- }
- transferToken = IERC721(wrapped);
- }
- // transfer bridged NFT to recipient
- address transferRecipient = address(uint160(uint256(transfer.to)));
- if (transfer.tokenChain != chainId()) {
- if (transfer.tokenChain == 1) {
- // Cache SPL token info which otherwise would get lost
- setSplCache(transfer.tokenID, NFTBridgeStorage.SPLCache({
- name : transfer.name,
- symbol : transfer.symbol
- }));
- }
- // mint wrapped asset
- NFTImplementation(address(transferToken)).mint(transferRecipient, transfer.tokenID, transfer.uri);
- } else {
- transferToken.safeTransferFrom(address(this), transferRecipient, transfer.tokenID);
- }
- }
- // Creates a wrapped asset using AssetMeta
- function _createWrapped(uint16 tokenChain, bytes32 tokenAddress, bytes32 name, bytes32 symbol) internal returns (address token) {
- require(tokenChain != chainId(), "can only wrap tokens from foreign chains");
- require(wrappedAsset(tokenChain, tokenAddress) == address(0), "wrapped asset already exists");
- // SPL NFTs all use the same NFT contract, so unify the name
- if (tokenChain == 1) {
- // "Wormhole Bridged Solana-NFT" - right-padded
- name = 0x576f726d686f6c65204272696467656420536f6c616e612d4e46540000000000;
- // "WORMSPLNFT" - right-padded
- symbol = 0x574f524d53504c4e465400000000000000000000000000000000000000000000;
- }
- // initialize the NFTImplementation
- bytes memory initialisationArgs = abi.encodeWithSelector(
- NFTImplementation.initialize.selector,
- bytes32ToString(name),
- bytes32ToString(symbol),
- address(this),
- tokenChain,
- tokenAddress
- );
- // initialize the BeaconProxy
- bytes memory constructorArgs = abi.encode(address(this), initialisationArgs);
- // deployment code
- bytes memory bytecode = abi.encodePacked(type(BridgeNFT).creationCode, constructorArgs);
- bytes32 salt = keccak256(abi.encodePacked(tokenChain, tokenAddress));
- assembly {
- token := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
- if iszero(extcodesize(token)) {
- revert(0, 0)
- }
- }
- setWrappedAsset(tokenChain, tokenAddress, token);
- }
- function verifyBridgeVM(IWormhole.VM memory vm) internal view returns (bool){
- if (bridgeContracts(vm.emitterChainId) == vm.emitterAddress) {
- return true;
- }
- return false;
- }
- function encodeTransfer(NFTBridgeStructs.Transfer memory transfer) public pure returns (bytes memory encoded) {
- // There is a global limit on 200 bytes of tokenURI in Wormhole due to Solana
- require(bytes(transfer.uri).length <= 200, "tokenURI must not exceed 200 bytes");
- encoded = abi.encodePacked(
- uint8(1),
- transfer.tokenAddress,
- transfer.tokenChain,
- transfer.symbol,
- transfer.name,
- transfer.tokenID,
- uint8(bytes(transfer.uri).length),
- transfer.uri,
- transfer.to,
- transfer.toChain
- );
- }
- function parseTransfer(bytes memory encoded) public pure returns (NFTBridgeStructs.Transfer memory transfer) {
- uint index = 0;
- uint8 payloadID = encoded.toUint8(index);
- index += 1;
- require(payloadID == 1, "invalid Transfer");
- transfer.tokenAddress = encoded.toBytes32(index);
- index += 32;
- transfer.tokenChain = encoded.toUint16(index);
- index += 2;
- transfer.symbol = encoded.toBytes32(index);
- index += 32;
- transfer.name = encoded.toBytes32(index);
- index += 32;
- transfer.tokenID = encoded.toUint256(index);
- index += 32;
-
- // Ignore length due to malformatted payload
- index += 1;
- transfer.uri = string(encoded.slice(index, encoded.length - index - 34));
- // From here we read backwards due malformatted package
- index = encoded.length;
- index -= 2;
- transfer.toChain = encoded.toUint16(index);
- index -= 32;
- transfer.to = encoded.toBytes32(index);
- //require(encoded.length == index, "invalid Transfer");
- }
- function onERC721Received(
- address operator,
- address,
- uint256,
- bytes calldata
- ) external view returns (bytes4){
- require(operator == address(this), "can only bridge tokens via transferNFT method");
- return type(IERC721Receiver).interfaceId;
- }
- function bytes32ToString(bytes32 input) internal pure returns (string memory) {
- uint256 i;
- while (i < 32 && input[i] != 0) {
- i++;
- }
- bytes memory array = new bytes(i);
- for (uint c = 0; c < i; c++) {
- array[c] = input[c];
- }
- return string(array);
- }
- }
|