283 lines
10 KiB
Solidity
283 lines
10 KiB
Solidity
// 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))
|
|
}
|
|
}
|
|
|
|
IERC721(token).safeTransferFrom(msg.sender, address(this), tokenID);
|
|
if (tokenChain != chainId()) {
|
|
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, finality());
|
|
}
|
|
|
|
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){
|
|
require(!isFork(), "invalid fork");
|
|
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);
|
|
}
|
|
}
|