From 46440b3bf9261bd212a20db1906563f537bcaf5c Mon Sep 17 00:00:00 2001 From: valentin Date: Mon, 20 Sep 2021 12:25:09 +0200 Subject: [PATCH] nft: add special case for spl naming, update tests Change-Id: Ifbe9eeaad5d5604d52d75fdac6b018e94afa9d75 --- ethereum/contracts/nft/NFTBridge.sol | 42 +++++++-- ethereum/contracts/nft/NFTBridgeGetters.sol | 4 + ethereum/contracts/nft/NFTBridgeSetters.sol | 8 ++ ethereum/contracts/nft/NFTBridgeState.sol | 8 ++ ethereum/test/nft.js | 97 ++++++++++++++++++++- 5 files changed, 149 insertions(+), 10 deletions(-) diff --git a/ethereum/contracts/nft/NFTBridge.sol b/ethereum/contracts/nft/NFTBridge.sol index 515a1104..f2f171bf 100644 --- a/ethereum/contracts/nft/NFTBridge.sol +++ b/ethereum/contracts/nft/NFTBridge.sol @@ -39,21 +39,31 @@ contract NFTBridge is NFTBridgeGovernance { string memory nameString; string memory uriString; { - (,bytes memory queriedSymbol) = token.staticcall(abi.encodeWithSignature("symbol()")); - (,bytes memory queriedName) = token.staticcall(abi.encodeWithSignature("name()")); - (,bytes memory queriedURI) = token.staticcall(abi.encodeWithSignature("tokenURI(uint256)", tokenID)); + 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)); + } - 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; - assembly { + 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 - symbol := mload(add(symbolString, 32)) - name := mload(add(nameString, 32)) + symbol := mload(add(symbolString, 32)) + name := mload(add(nameString, 32)) + } } if (tokenChain == chainId()) { @@ -118,6 +128,14 @@ contract NFTBridge is NFTBridgeGovernance { 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 { @@ -130,6 +148,14 @@ contract NFTBridge is NFTBridgeGovernance { 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, diff --git a/ethereum/contracts/nft/NFTBridgeGetters.sol b/ethereum/contracts/nft/NFTBridgeGetters.sol index 8391809d..37816384 100644 --- a/ethereum/contracts/nft/NFTBridgeGetters.sol +++ b/ethereum/contracts/nft/NFTBridgeGetters.sol @@ -53,4 +53,8 @@ contract NFTBridgeGetters is NFTBridgeState { function isWrappedAsset(address token) public view returns (bool){ return _state.isWrappedAsset[token]; } + + function splCache(uint256 tokenId) public view returns (NFTBridgeStorage.SPLCache memory) { + return _state.splCache[tokenId]; + } } \ No newline at end of file diff --git a/ethereum/contracts/nft/NFTBridgeSetters.sol b/ethereum/contracts/nft/NFTBridgeSetters.sol index 17235186..ec2dde5b 100644 --- a/ethereum/contracts/nft/NFTBridgeSetters.sol +++ b/ethereum/contracts/nft/NFTBridgeSetters.sol @@ -46,4 +46,12 @@ contract NFTBridgeSetters is NFTBridgeState { _state.wrappedAssets[tokenChainId][tokenAddress] = wrapper; _state.isWrappedAsset[wrapper] = true; } + + function setSplCache(uint256 tokenId, NFTBridgeStorage.SPLCache memory cache) internal { + _state.splCache[tokenId] = cache; + } + + function clearSplCache(uint256 tokenId) internal { + delete _state.splCache[tokenId]; + } } \ No newline at end of file diff --git a/ethereum/contracts/nft/NFTBridgeState.sol b/ethereum/contracts/nft/NFTBridgeState.sol index b3f53159..52bd6206 100644 --- a/ethereum/contracts/nft/NFTBridgeState.sol +++ b/ethereum/contracts/nft/NFTBridgeState.sol @@ -17,6 +17,11 @@ contract NFTBridgeStorage { bytes32 assetAddress; } + struct SPLCache { + bytes32 name; + bytes32 symbol; + } + struct State { address payable wormhole; address tokenImplementation; @@ -40,6 +45,9 @@ contract NFTBridgeStorage { // Mapping of bridge contracts on other chains mapping(uint16 => bytes32) bridgeImplementations; + + // Mapping of spl token info caches (chainID => nativeAddress => SPLCache) + mapping(uint256 => SPLCache) splCache; } } diff --git a/ethereum/test/nft.js b/ethereum/test/nft.js index b70a005c..6cfaccae 100644 --- a/ethereum/test/nft.js +++ b/ethereum/test/nft.js @@ -24,7 +24,7 @@ contract("NFT", function () { let WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"; const testForeignChainId = "1"; const testForeignBridgeContract = "0x000000000000000000000000000000000000000000000000000000000000ffff"; - const testBridgedAssetChain = "0001"; + const testBridgedAssetChain = "0003"; const testBridgedAssetAddress = "000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"; @@ -406,7 +406,7 @@ contract("NFT", function () { assert.equal(name, "Foreign Chain NFT"); const chainId = await wrappedAsset.methods.chainId().call(); - assert.equal(chainId, 1); + assert.equal(chainId, Number(testBridgedAssetChain)); const nativeContract = await wrappedAsset.methods.nativeContract().call(); assert.equal(nativeContract, "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e"); @@ -458,6 +458,99 @@ contract("NFT", function () { assert.equal(ownerAfter, accounts[0]); }) + it("should mint bridged assets from solana under unified name, caching the original", async function () { + const accounts = await web3.eth.getAccounts(); + let tokenId = "1000000000000000001"; + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address); + + // we are using the asset where we created a wrapper in the previous test + let data = "0x" + + "01" + + // tokenaddress + testBridgedAssetAddress + + // tokenchain + "0001" + + // symbol + "464f520000000000000000000000000000000000000000000000000000000000" + + // name + "466f726569676e20436861696e204e4654000000000000000000000000000000" + + // tokenID + web3.eth.abi.encodeParameter("uint256", new BigNumber(tokenId).toString()).substring(2) + + // url length + "00" + + // no URL + "" + + // receiver + web3.eth.abi.encodeParameter("address", accounts[0]).substr(2) + + // receiving chain + web3.eth.abi.encodeParameter("uint16", testChainId).substring(2 + (64 - 4)); + + let vm = await signAndEncodeVM( + 0, + 0, + testForeignChainId, + testForeignBridgeContract, + 0, + data, + [ + testSigner1PK + ], + 0, + 0 + ); + + await initialized.methods.completeTransfer("0x" + vm).send({ + value: 0, + from: accounts[1], + gasLimit: 2000000 + }); + + const cache = await initialized.methods.splCache(tokenId).call() + assert.equal(cache.symbol, "0x464f520000000000000000000000000000000000000000000000000000000000"); + assert.equal(cache.name, "0x466f726569676e20436861696e204e4654000000000000000000000000000000"); + + const wrappedAddress = await initialized.methods.wrappedAsset("0x0001", "0x" + testBridgedAssetAddress).call(); + const wrappedAsset = new web3.eth.Contract(NFTImplementation.abi, wrappedAddress); + + const symbol = await wrappedAsset.methods.symbol().call(); + assert.equal(symbol, "WORMSPLNFT"); + + const name = await wrappedAsset.methods.name().call(); + assert.equal(name, "Wormhole Bridged Solana-NFT"); + }) + + it("cached SPL names are loaded when transferring out, cache is cleared", async function () { + const accounts = await web3.eth.getAccounts(); + let tokenId = "1000000000000000001"; + + const initialized = new web3.eth.Contract(BridgeImplementationFullABI, NFTBridge.address); + + const wrappedAddress = await initialized.methods.wrappedAsset("0x0001", "0x" + testBridgedAssetAddress).call(); + + const transfer = await initialized.methods.transferNFT( + wrappedAddress, + tokenId, + "10", + "0x000000000000000000000000b7a2211e8165943192ad04f5dd21bedc29ff003e", + "2345" + ).send({ + value: 0, + from: accounts[0], + gasLimit: 2000000 + }); + + // symbol + assert.ok(transfer.events[2].raw.data.includes('464f520000000000000000000000000000000000000000000000000000000000')) + // name + assert.ok(transfer.events[2].raw.data.includes('466f726569676e20436861696e204e4654000000000000000000000000000000')) + + // check if cache is cleared + const cache = await initialized.methods.splCache(tokenId).call() + assert.equal(cache.symbol, "0x0000000000000000000000000000000000000000000000000000000000000000"); + assert.equal(cache.name, "0x0000000000000000000000000000000000000000000000000000000000000000"); + }) + it("should burn bridged assets wrappers on transfer to another chain", async function () { const accounts = await web3.eth.getAccounts();