From 02302e24d0a6f8f498710ef8948cabf1e9700d6b Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Sat, 13 Nov 2021 14:42:40 +0300 Subject: [PATCH] Pull and push updated token metadata --- contracts/interfaces/IAMB.sol | 9 ++ .../interfaces/IForeignNFTOmnibridge.sol | 5 + contracts/interfaces/IInformationReceiver.sol | 9 ++ contracts/interfaces/ITokenMetadata.sol | 9 ++ contracts/libraries/TokenReader.sol | 99 --------------- contracts/mocks/AMBMock.sol | 30 ++++- contracts/tokens/ERC1155BridgeToken.sol | 13 ++ contracts/tokens/ERC721BridgeToken.sol | 13 ++ contracts/upgradeable_contracts/Ownable.sol | 6 +- .../omnibridge_nft/BasicNFTOmnibridge.sol | 4 +- .../omnibridge_nft/ForeignNFTOmnibridge.sol | 28 ++++- .../omnibridge_nft/HomeNFTOmnibridge.sol | 13 +- .../components/bridged/ERC1155TokenProxy.sol | 4 + .../components/bridged/ERC721TokenProxy.sol | 2 + .../components/bridged/MetadataPuller.sol | 114 ++++++++++++++++++ .../components/native/MetadataPusher.sol | 57 +++++++++ .../components/native/MetadataReader.sol | 35 ++++-- .../native/NativeTokensRegistry.sol | 6 +- e2e-tests/docker-compose.yml | 8 +- e2e-tests/run-tests.sh | 2 +- e2e-tests/run.js | 45 +++++++ .../scenarios/erc1155/homePullMetadata.js | 41 +++++++ .../scenarios/erc1155/homePushMetadata.js | 36 ++++++ .../scenarios/erc721/homePullMetadata.js | 41 +++++++ .../scenarios/erc721/homePushMetadata.js | 36 ++++++ test/omnibridge_nft/common.test.js | 114 ++++++++++++++++++ truffle-config.js | 2 +- 27 files changed, 650 insertions(+), 131 deletions(-) create mode 100644 contracts/interfaces/IForeignNFTOmnibridge.sol create mode 100644 contracts/interfaces/IInformationReceiver.sol create mode 100644 contracts/interfaces/ITokenMetadata.sol delete mode 100644 contracts/libraries/TokenReader.sol create mode 100644 contracts/upgradeable_contracts/omnibridge_nft/components/bridged/MetadataPuller.sol create mode 100644 contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataPusher.sol create mode 100644 e2e-tests/scenarios/erc1155/homePullMetadata.js create mode 100644 e2e-tests/scenarios/erc1155/homePushMetadata.js create mode 100644 e2e-tests/scenarios/erc721/homePullMetadata.js create mode 100644 e2e-tests/scenarios/erc721/homePushMetadata.js diff --git a/contracts/interfaces/IAMB.sol b/contracts/interfaces/IAMB.sol index 3f404bc..7f1392e 100644 --- a/contracts/interfaces/IAMB.sol +++ b/contracts/interfaces/IAMB.sol @@ -3,6 +3,13 @@ pragma solidity 0.7.5; interface IAMB { event UserRequestForAffirmation(bytes32 indexed messageId, bytes encodedData); event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData); + event UserRequestForInformation( + bytes32 indexed messageId, + bytes32 indexed requestSelector, + address indexed sender, + bytes data + ); + event InformationRetrieved(bytes32 indexed messageId, bool status, bool callbackStatus); event CollectedSignatures( address authorityResponsibleForRelay, bytes32 messageHash, @@ -46,6 +53,8 @@ interface IAMB { uint256 _gas ) external returns (bytes32); + function requireToGetInformation(bytes32 _requestSelector, bytes calldata _data) external returns (bytes32); + function sourceChainId() external view returns (uint256); function destinationChainId() external view returns (uint256); diff --git a/contracts/interfaces/IForeignNFTOmnibridge.sol b/contracts/interfaces/IForeignNFTOmnibridge.sol new file mode 100644 index 0000000..35c92eb --- /dev/null +++ b/contracts/interfaces/IForeignNFTOmnibridge.sol @@ -0,0 +1,5 @@ +pragma solidity 0.7.5; + +interface IForeignNFTOmnibridge { + function updateBridgedTokenMetadata(address _token, bytes calldata _data) external; +} diff --git a/contracts/interfaces/IInformationReceiver.sol b/contracts/interfaces/IInformationReceiver.sol new file mode 100644 index 0000000..c373205 --- /dev/null +++ b/contracts/interfaces/IInformationReceiver.sol @@ -0,0 +1,9 @@ +pragma solidity 0.7.5; + +interface IInformationReceiver { + function onInformationReceived( + bytes32 _messageId, + bool _status, + bytes calldata _data + ) external; +} diff --git a/contracts/interfaces/ITokenMetadata.sol b/contracts/interfaces/ITokenMetadata.sol new file mode 100644 index 0000000..74945e0 --- /dev/null +++ b/contracts/interfaces/ITokenMetadata.sol @@ -0,0 +1,9 @@ +pragma solidity 0.7.5; + +interface ITokenMetadata { + function owner() external view returns (address); + + function setOwner(address _owner) external; + + function setTokenURI(uint256 _tokenId, string calldata _tokenURI) external; +} diff --git a/contracts/libraries/TokenReader.sol b/contracts/libraries/TokenReader.sol deleted file mode 100644 index c4236c7..0000000 --- a/contracts/libraries/TokenReader.sol +++ /dev/null @@ -1,99 +0,0 @@ -pragma solidity 0.7.5; - -// solhint-disable -interface ITokenDetails { - function name() external view; - function NAME() external view; - function symbol() external view; - function SYMBOL() external view; - function decimals() external view; - function DECIMALS() external view; -} -// solhint-enable - -/** - * @title TokenReader - * @dev Helper methods for reading name/symbol/decimals parameters from ERC20 token contracts. - */ -library TokenReader { - /** - * @dev Reads the name property of the provided token. - * Either name() or NAME() method is used. - * Both, string and bytes32 types are supported. - * @param _token address of the token contract. - * @return token name as a string or an empty string if none of the methods succeeded. - */ - function readName(address _token) internal view returns (string memory) { - (bool status, bytes memory data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.name.selector)); - if (!status) { - (status, data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.NAME.selector)); - if (!status) { - return ""; - } - } - return _convertToString(data); - } - - /** - * @dev Reads the symbol property of the provided token. - * Either symbol() or SYMBOL() method is used. - * Both, string and bytes32 types are supported. - * @param _token address of the token contract. - * @return token symbol as a string or an empty string if none of the methods succeeded. - */ - function readSymbol(address _token) internal view returns (string memory) { - (bool status, bytes memory data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.symbol.selector)); - if (!status) { - (status, data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.SYMBOL.selector)); - if (!status) { - return ""; - } - } - return _convertToString(data); - } - - /** - * @dev Reads the decimals property of the provided token. - * Either decimals() or DECIMALS() method is used. - * @param _token address of the token contract. - * @return token decimals or 0 if none of the methods succeeded. - */ - function readDecimals(address _token) internal view returns (uint256) { - (bool status, bytes memory data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.decimals.selector)); - if (!status) { - (status, data) = _token.staticcall(abi.encodeWithSelector(ITokenDetails.DECIMALS.selector)); - if (!status) { - return 0; - } - } - return abi.decode(data, (uint256)); - } - - /** - * @dev Internal function for converting returned value of name()/symbol() from bytes32/string to string. - * @param returnData data returned by the token contract. - * @return string with value obtained from returnData. - */ - function _convertToString(bytes memory returnData) private pure returns (string memory) { - if (returnData.length > 32) { - return abi.decode(returnData, (string)); - } else if (returnData.length == 32) { - bytes32 data = abi.decode(returnData, (bytes32)); - string memory res = new string(32); - assembly { - let len := 0 - mstore(add(res, 32), data) // save value in result string - - // solhint-disable - for { } gt(data, 0) { len := add(len, 1) } { // until string is empty - data := shl(8, data) // shift left by one symbol - } - // solhint-enable - mstore(res, len) // save result string length - } - return res; - } else { - return ""; - } - } -} diff --git a/contracts/mocks/AMBMock.sol b/contracts/mocks/AMBMock.sol index e732c35..16c4c55 100644 --- a/contracts/mocks/AMBMock.sol +++ b/contracts/mocks/AMBMock.sol @@ -1,7 +1,10 @@ pragma solidity 0.7.5; +import "../interfaces/IInformationReceiver.sol"; + contract AMBMock { event MockedEvent(bytes32 indexed messageId, address executor, uint8 dataType, bytes data, uint256 gas); + event MockedInformationRequest(bytes32 indexed messageId, bytes32 indexed selector, bytes data); address public messageSender; uint256 public immutable maxGasPerTx; @@ -43,6 +46,15 @@ contract AMBMock { } } + function executeInformationResponse( + address _sender, + bytes32 _messageId, + bool _status, + bytes calldata _data + ) external { + IInformationReceiver(_sender).onInformationReceived(_messageId, _status, _data); + } + function requireToPassMessage( address _contract, bytes calldata _data, @@ -59,22 +71,30 @@ contract AMBMock { return _sendMessage(_contract, _data, _gas, 0x80); } + function requireToGetInformation(bytes32 _requestSelector, bytes calldata _data) external returns (bytes32) { + bytes32 _messageId = _newMessageId(); + emit MockedInformationRequest(_messageId, _requestSelector, _data); + return _messageId; + } + function _sendMessage( address _contract, bytes calldata _data, uint256 _gas, uint256 _dataType ) internal returns (bytes32) { + bytes32 _messageId = _newMessageId(); + emit MockedEvent(_messageId, _contract, uint8(_dataType), _data, _gas); + return _messageId; + } + + function _newMessageId() internal returns (bytes32) { require(messageId == bytes32(0)); bytes32 bridgeId = keccak256(abi.encodePacked(uint16(1337), address(this))) & 0x00000000ffffffffffffffffffffffffffffffffffffffff0000000000000000; - bytes32 _messageId = bytes32(uint256(0x11223344 << 224)) | bridgeId | bytes32(nonce); - nonce += 1; - - emit MockedEvent(_messageId, _contract, uint8(_dataType), _data, _gas); - return _messageId; + return bytes32(uint256(0x11223344 << 224)) | bridgeId | bytes32(nonce++); } function sourceChainId() external pure returns (uint256) { diff --git a/contracts/tokens/ERC1155BridgeToken.sol b/contracts/tokens/ERC1155BridgeToken.sol index 6481b71..d7ad03b 100644 --- a/contracts/tokens/ERC1155BridgeToken.sol +++ b/contracts/tokens/ERC1155BridgeToken.sol @@ -21,6 +21,9 @@ contract ERC1155BridgeToken is ERC1155, IBurnableMintableERC1155Token { bool private hasAlreadyMinted; + // metadata field for Opensea, it is not a real owner + address public owner; + constructor( string memory _name, string memory _symbol, @@ -153,6 +156,16 @@ contract ERC1155BridgeToken is ERC1155, IBurnableMintableERC1155Token { tokenURIs[_tokenId] = _tokenURI; } + /** + * @dev Sets the owner for this particular token. + * Ownership of the bridged token does not allow to mint or change tokens on-chain. + * It is only needed for convenience metadata management on the Opensea marketplace. + * @param _owner new token owner. + */ + function setOwner(address _owner) external onlyOwner { + owner = _owner; + } + /** * @dev Tells the metadata URI for the particular tokenId. * @param _tokenId unique token id for which to return metadata URI. diff --git a/contracts/tokens/ERC721BridgeToken.sol b/contracts/tokens/ERC721BridgeToken.sol index 022629f..11a6954 100644 --- a/contracts/tokens/ERC721BridgeToken.sol +++ b/contracts/tokens/ERC721BridgeToken.sol @@ -11,6 +11,9 @@ import "../interfaces/IBurnableMintableERC721Token.sol"; contract ERC721BridgeToken is ERC721, IBurnableMintableERC721Token { address public bridgeContract; + // metadata field for Opensea, it is not a real owner + address public owner; + constructor( string memory _name, string memory _symbol, @@ -129,6 +132,16 @@ contract ERC721BridgeToken is ERC721, IBurnableMintableERC721Token { _setTokenURI(_tokenId, _tokenURI); } + /** + * @dev Sets the owner for this particular token. + * Ownership of the bridged token does not allow to mint or change tokens on-chain. + * It is only needed for convenience metadata management on the Opensea marketplace. + * @param _owner new token owner. + */ + function setOwner(address _owner) external onlyOwner { + owner = _owner; + } + /** * @dev Tells the current version of the ERC721 token interfaces. */ diff --git a/contracts/upgradeable_contracts/Ownable.sol b/contracts/upgradeable_contracts/Ownable.sol index 7f7edd9..5a29045 100644 --- a/contracts/upgradeable_contracts/Ownable.sol +++ b/contracts/upgradeable_contracts/Ownable.sol @@ -21,10 +21,14 @@ contract Ownable is EternalStorage { * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { - require(msg.sender == owner()); + _onlyOwner(); _; } + function _onlyOwner() internal { + require(msg.sender == owner()); + } + /** * @dev Throws if called through proxy by any account other than contract itself or an upgradeability owner. */ diff --git a/contracts/upgradeable_contracts/omnibridge_nft/BasicNFTOmnibridge.sol b/contracts/upgradeable_contracts/omnibridge_nft/BasicNFTOmnibridge.sol index b2e9f2a..711a179 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/BasicNFTOmnibridge.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/BasicNFTOmnibridge.sol @@ -22,7 +22,7 @@ import "../../tokens/ERC721BridgeToken.sol"; /** * @title BasicNFTOmnibridge - * @dev Commong functionality for multi-token mediator for ERC721 tokens intended to work on top of AMB bridge. + * @dev Common functionality for multi-token mediator for ERC721 tokens intended to work on top of AMB bridge. */ abstract contract BasicNFTOmnibridge is Initializable, @@ -337,7 +337,7 @@ abstract contract BasicNFTOmnibridge is uint256[] memory _tokenIds, uint256[] memory _values ) internal override { - _releaseTokens(_token, nativeTokenAddress(_token) == address(0), _recipient, _tokenIds, _values); + _releaseTokens(_token, isRegisteredAsNativeToken(_token), _recipient, _tokenIds, _values); } /** diff --git a/contracts/upgradeable_contracts/omnibridge_nft/ForeignNFTOmnibridge.sol b/contracts/upgradeable_contracts/omnibridge_nft/ForeignNFTOmnibridge.sol index 9b224ea..c64a4ed 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/ForeignNFTOmnibridge.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/ForeignNFTOmnibridge.sol @@ -4,6 +4,7 @@ pragma abicoder v2; import "./BasicNFTOmnibridge.sol"; import "./components/common/GasLimitManager.sol"; +import "../../tokens/ERC1155BridgeToken.sol"; /** * @title ForeignNFTOmnibridge @@ -29,7 +30,7 @@ contract ForeignNFTOmnibridge is BasicNFTOmnibridge, GasLimitManager { address _owner, address _imageERC721, address _imageERC1155 - ) external onlyRelevantSender returns (bool) { + ) external onlyRelevantSender { require(!isInitialized()); _setBridgeContract(_bridgeContract); @@ -40,8 +41,31 @@ contract ForeignNFTOmnibridge is BasicNFTOmnibridge, GasLimitManager { _setTokenImageERC1155(_imageERC1155); setInitialize(); + } - return isInitialized(); + /** + * @dev Function for updating metadata on the bridged token from the other side. + * Used for permission-less updates of owner() and token URIs. + * @param _token address of the native token from the other side of the bridge. + * @param _data calldata for executing on the token contract. + */ + function updateBridgedTokenMetadata(address _token, bytes memory _data) external onlyMediator { + require(_data.length >= 4); + bytes4 selector; + assembly { + selector := shl(224, mload(add(_data, 4))) + } + // we are using this method only for calling setOwner/setTokenURI on the underlying token contract + // this check is here to prevent unintentional calls of sensitive methods + require( + selector != IBurnableMintableERC721Token.mint.selector && + selector != IBurnableMintableERC721Token.burn.selector && + selector != IBurnableMintableERC1155Token.mint.selector && + selector != IBurnableMintableERC1155Token.burn.selector && + selector != ERC1155BridgeToken.setBridgeContract.selector + ); + (bool status, ) = bridgedTokenAddress(_token).call(_data); + require(status); } /** diff --git a/contracts/upgradeable_contracts/omnibridge_nft/HomeNFTOmnibridge.sol b/contracts/upgradeable_contracts/omnibridge_nft/HomeNFTOmnibridge.sol index 65de0e5..a489288 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/HomeNFTOmnibridge.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/HomeNFTOmnibridge.sol @@ -4,13 +4,20 @@ pragma abicoder v2; import "./modules/forwarding_rules/NFTForwardingRulesConnector.sol"; import "./modules/gas_limit/SelectorTokenGasLimitConnector.sol"; +import "./components/bridged/MetadataPuller.sol"; +import "./components/native/MetadataPusher.sol"; /** * @title HomeNFTOmnibridge * @dev Home side implementation for multi-token ERC721 mediator intended to work on top of AMB bridge. * It is designed to be used as an implementation contract of EternalStorageProxy contract. */ -contract HomeNFTOmnibridge is NFTForwardingRulesConnector, SelectorTokenGasLimitConnector { +contract HomeNFTOmnibridge is + NFTForwardingRulesConnector, + SelectorTokenGasLimitConnector, + MetadataPuller, + MetadataPusher +{ constructor(string memory _suffix) BasicNFTOmnibridge(_suffix) {} /** @@ -31,7 +38,7 @@ contract HomeNFTOmnibridge is NFTForwardingRulesConnector, SelectorTokenGasLimit address _imageERC721, address _imageERC1155, address _forwardingRulesManager - ) external onlyRelevantSender returns (bool) { + ) external onlyRelevantSender { require(!isInitialized()); _setBridgeContract(_bridgeContract); @@ -43,8 +50,6 @@ contract HomeNFTOmnibridge is NFTForwardingRulesConnector, SelectorTokenGasLimit _setForwardingRulesManager(_forwardingRulesManager); setInitialize(); - - return isInitialized(); } /** diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC1155TokenProxy.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC1155TokenProxy.sol index ad222a8..5e62eb6 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC1155TokenProxy.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC1155TokenProxy.sol @@ -27,6 +27,10 @@ contract ERC1155TokenProxy is Proxy { string private _baseURI; address private bridgeContract; + bool private hasAlreadyMinted; + + address private owner; + /** * @dev Creates an upgradeable token proxy for ERC1155BridgeToken.sol, initializes its eternalStorage. * @param _tokenImage address of the token image used for mirroring all functions. diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC721TokenProxy.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC721TokenProxy.sol index 3a966b9..da3216f 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC721TokenProxy.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/ERC721TokenProxy.sol @@ -25,6 +25,8 @@ contract ERC721TokenProxy is Proxy { string private _baseURI; address private bridgeContract; + address private owner; + /** * @dev Creates an upgradeable token proxy for ERC721BridgeToken.sol, initializes its eternalStorage. * @param _tokenImage address of the token image used for mirroring all functions. diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/MetadataPuller.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/MetadataPuller.sol new file mode 100644 index 0000000..f1640b0 --- /dev/null +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/bridged/MetadataPuller.sol @@ -0,0 +1,114 @@ +pragma solidity 0.7.5; + +import "@openzeppelin/contracts/token/ERC721/IERC721Metadata.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155MetadataURI.sol"; +import "../../../../interfaces/IBurnableMintableERC721Token.sol"; +import "../../../../interfaces/ITokenMetadata.sol"; +import "../../../BasicAMBMediator.sol"; +import "./BridgedTokensRegistry.sol"; + +/** + * @title MetadataPuller + * @dev Functionality for pull updates of the tokens metadata from the native tokens on the foreign side. + * Uses async AMB calls functionality for transferring data. + */ +abstract contract MetadataPuller is BasicAMBMediator, BridgedTokensRegistry { + /** + * @dev Makes an async information request for an updated token owner field from the other side native token. + * @param _token address of the bridged token contract, for which to pull the updated owner. + */ + function pullTokenOwnerUpdate(address _token) external { + _pullUpdate(_token, abi.encodeWithSelector(ITokenMetadata.owner.selector), 0); + } + + /** + * @dev Makes an async information request for an updated token URI field from the other side native token. + * @param _token address of the bridged token contract, for which to pull the updated URI. + * @param _id token id of the token. + * @param _isERC1155 true, if pulling using uri() field, will use tokenURI() otherwise. + */ + function pullTokenURIUpdate( + address _token, + uint256 _id, + bool _isERC1155 + ) external { + bytes4 selector = _isERC1155 ? IERC1155MetadataURI.uri.selector : IERC721Metadata.tokenURI.selector; + _pullUpdate(_token, abi.encodeWithSelector(selector, _id), _id); + } + + /** + * @dev Makes an async eth_call request on the other side native token. + * @param _token address of the bridged token contract, for which to make a request. + * @param _data encoded calldata. + * @param _id tokenId to record inside a request context. + */ + function _pullUpdate( + address _token, + bytes memory _data, + uint256 _id + ) internal { + address nativeToken = nativeTokenAddress(_token); + require(nativeToken != address(0)); + + bytes32 requestSelector = keccak256("eth_call(address,bytes)"); + bytes32 messageId = bridgeContract().requireToGetInformation(requestSelector, abi.encode(nativeToken, _data)); + + _setMetadataRequestParameters(messageId, _token, _id); + } + + /** + * @dev Information receiving callback. + * @param _messageId id of the message. + * @param _status request completion status. + * @param _data received request data. + */ + function onInformationReceived( + bytes32 _messageId, + bool _status, + bytes calldata _data + ) external { + require(msg.sender == address(bridgeContract())); + require(messageId() == bytes32(0)); + + require(_status); + bytes memory data = abi.decode(_data, (bytes)); + + (address token, uint256 id) = _getMetadataRequestParameters(_messageId); + + if (id == 0 && data.length == 32) { + ITokenMetadata(token).setOwner(abi.decode(data, (address))); + } else if (data.length > 64) { + ITokenMetadata(token).setTokenURI(id, abi.decode(data, (string))); + } else { + revert(); + } + } + + /** + * @dev Internal function for setting request context. + * @param _messageId id of the sent message. + * @param _token request token address. + * @param _id request token id. + */ + function _setMetadataRequestParameters( + bytes32 _messageId, + address _token, + uint256 _id + ) private { + bytes32 key = keccak256(abi.encodePacked("uriRequest", _messageId)); + addressStorage[key] = _token; + uintStorage[key] = _id; + } + + /** + * @dev Internal function for restoring request context. + * @param _messageId id of the sent message. + */ + function _getMetadataRequestParameters(bytes32 _messageId) private returns (address, uint256) { + bytes32 key = keccak256(abi.encodePacked("uriRequest", _messageId)); + (address token, uint256 id) = (addressStorage[key], uintStorage[key]); + delete addressStorage[key]; + delete uintStorage[key]; + return (token, id); + } +} diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataPusher.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataPusher.sol new file mode 100644 index 0000000..4f10a96 --- /dev/null +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataPusher.sol @@ -0,0 +1,57 @@ +pragma solidity 0.7.5; + +import "@openzeppelin/contracts/token/ERC721/IERC721Metadata.sol"; +import "@openzeppelin/contracts/token/ERC1155/IERC1155MetadataURI.sol"; +import "../../../../interfaces/IBurnableMintableERC721Token.sol"; +import "../../../../interfaces/IForeignNFTOmnibridge.sol"; +import "../../../../interfaces/ITokenMetadata.sol"; +import "../../../BasicAMBMediator.sol"; +import "./NativeTokensRegistry.sol"; +import "./MetadataReader.sol"; + +/** + * @title MetadataPusher + * @dev Functionality for push updates of the tokens metadata from the native tokens on the home side. + * Uses regular AMB requests for transferring data. + */ +abstract contract MetadataPusher is BasicAMBMediator, NativeTokensRegistry, MetadataReader { + /** + * @dev Send an AMB message with the token owner update to the other side. + * @param _token address of the native token contract. + */ + function pushTokenOwnerUpdate(address _token) external { + address owner = Ownable(_token).owner(); + + _pushUpdate(_token, abi.encodeWithSelector(ITokenMetadata.setOwner.selector, owner)); + } + + /** + * @dev Send an AMB message with the token owner update to the other side. + * @param _token address of the native token contract. + * @param _id token id of the token. + * @param _isERC1155 true, if reading using uri() field, will use tokenURI() otherwise. + */ + function pushTokenURIUpdate( + address _token, + uint256 _id, + bool _isERC1155 + ) external { + string memory uri = _isERC1155 ? _readERC1155TokenURI(_token, _id) : _readERC721TokenURI(_token, _id); + require(bytes(uri).length > 0); + + _pushUpdate(_token, abi.encodeWithSelector(ITokenMetadata.setTokenURI.selector, _id, uri)); + } + + /** + * @dev Internal function for pushing native token metadata field update to the other side. + * @param _token address of the native token contract. + * @param _data encoded data parameter for the updateBridgedTokenMetadata method on the other side. + */ + function _pushUpdate(address _token, bytes memory _data) internal { + require(isRegisteredAsNativeToken(_token)); + + bytes memory data = + abi.encodeWithSelector(IForeignNFTOmnibridge.updateBridgedTokenMetadata.selector, _token, _data); + _passMessage(data, false); + } +} diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataReader.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataReader.sol index c4af321..27b6087 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataReader.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/native/MetadataReader.sol @@ -34,8 +34,7 @@ contract MetadataReader is Ownable { * @return name for the token. */ function _readName(address _token) internal view returns (string memory) { - (bool status, bytes memory data) = _token.staticcall(abi.encodeWithSelector(IERC721Metadata.name.selector)); - return status ? abi.decode(data, (string)) : stringStorage[keccak256(abi.encodePacked("customName", _token))]; + return _readStringOptional(_token, abi.encodeWithSelector(IERC721Metadata.name.selector), "customName"); } /** @@ -45,8 +44,7 @@ contract MetadataReader is Ownable { * @return symbol for the token. */ function _readSymbol(address _token) internal view returns (string memory) { - (bool status, bytes memory data) = _token.staticcall(abi.encodeWithSelector(IERC721Metadata.symbol.selector)); - return status ? abi.decode(data, (string)) : stringStorage[keccak256(abi.encodePacked("customSymbol", _token))]; + return _readStringOptional(_token, abi.encodeWithSelector(IERC721Metadata.symbol.selector), "customSymbol"); } /** @@ -56,9 +54,7 @@ contract MetadataReader is Ownable { * @return token URI for the particular token, if any. */ function _readERC721TokenURI(address _token, uint256 _tokenId) internal view returns (string memory) { - (bool status, bytes memory data) = - _token.staticcall(abi.encodeWithSelector(IERC721Metadata.tokenURI.selector, _tokenId)); - return status ? abi.decode(data, (string)) : ""; + return _readStringOptional(_token, abi.encodeWithSelector(IERC721Metadata.tokenURI.selector, _tokenId), ""); } /** @@ -68,8 +64,27 @@ contract MetadataReader is Ownable { * @return token URI for the particular token, if any. */ function _readERC1155TokenURI(address _token, uint256 _tokenId) internal view returns (string memory) { - (bool status, bytes memory data) = - _token.staticcall(abi.encodeWithSelector(IERC1155MetadataURI.uri.selector, _tokenId)); - return status ? abi.decode(data, (string)) : ""; + return _readStringOptional(_token, abi.encodeWithSelector(IERC1155MetadataURI.uri.selector, _tokenId), ""); + } + + /** + * @dev Internal function for reading string field from some contract. + * @param _contract address of the contract to read string from. + * @param _data encoded calldata. + * @param _key fallback storage key associated with the default value. + */ + function _readStringOptional( + address _contract, + bytes memory _data, + string memory _key + ) internal view returns (string memory) { + (bool status, bytes memory data) = _contract.staticcall(_data); + if (status) { + return abi.decode(data, (string)); + } + if (bytes(_key).length == 0) { + return ""; + } + return stringStorage[keccak256(abi.encodePacked(_key, _contract))]; } } diff --git a/contracts/upgradeable_contracts/omnibridge_nft/components/native/NativeTokensRegistry.sol b/contracts/upgradeable_contracts/omnibridge_nft/components/native/NativeTokensRegistry.sol index f2be7ce..9d96f6f 100644 --- a/contracts/upgradeable_contracts/omnibridge_nft/components/native/NativeTokensRegistry.sol +++ b/contracts/upgradeable_contracts/omnibridge_nft/components/native/NativeTokensRegistry.sol @@ -22,7 +22,7 @@ contract NativeTokensRegistry is EternalStorage { /** * @dev Checks if a given token is a bridged token that is native to this side of the bridge. * @param _token address of token contract. - * @return message id of the send message. + * @return true, if token is registered in this contract. */ function isRegisteredAsNativeToken(address _token) public view returns (bool) { return uintStorage[keccak256(abi.encodePacked("tokenRegistered", _token))] > 0; @@ -34,8 +34,6 @@ contract NativeTokensRegistry is EternalStorage { * @param _state registration state. */ function _setNativeTokenIsRegistered(address _token, uint256 _state) internal { - if (uintStorage[keccak256(abi.encodePacked("tokenRegistered", _token))] != _state) { - uintStorage[keccak256(abi.encodePacked("tokenRegistered", _token))] = _state; - } + uintStorage[keccak256(abi.encodePacked("tokenRegistered", _token))] = _state; } } diff --git a/e2e-tests/docker-compose.yml b/e2e-tests/docker-compose.yml index 0698ca4..f4abd30 100644 --- a/e2e-tests/docker-compose.yml +++ b/e2e-tests/docker-compose.yml @@ -2,10 +2,10 @@ version: '3.8' services: home: image: trufflesuite/ganache-cli - command: --deterministic --chainId 1337 --blockTime 1 --gasLimit 10000000 + command: --deterministic --chainId 1337 --blockTime 1 --gasLimit 10000000 --noVMErrorsOnRPCResponse foreign: image: trufflesuite/ganache-cli - command: --deterministic --chainId 1338 --blockTime 1 --gasLimit 10000000 + command: --deterministic --chainId 1338 --blockTime 1 --gasLimit 10000000 --noVMErrorsOnRPCResponse deploy-amb: image: poanetwork/tokenbridge-contracts env_file: local-envs/deploy-amb.env @@ -40,6 +40,10 @@ services: image: poanetwork/tokenbridge-oracle:latest env_file: local-envs/oracle.env entrypoint: yarn watcher:affirmation-request + bridge_information: + image: poanetwork/tokenbridge-oracle:latest + env_file: local-envs/oracle.env + entrypoint: yarn watcher:information-request bridge_senderhome: image: poanetwork/tokenbridge-oracle:latest env_file: local-envs/oracle.env diff --git a/e2e-tests/run-tests.sh b/e2e-tests/run-tests.sh index 08763c8..cf2fc55 100755 --- a/e2e-tests/run-tests.sh +++ b/e2e-tests/run-tests.sh @@ -26,7 +26,7 @@ if [[ "$1" == 'local' ]]; then docker-compose run --rm deploy-amb docker-compose run --rm deploy-omni-nft - docker-compose up -d rabbit redis bridge_affirmation bridge_request bridge_collected bridge_senderhome bridge_senderforeign + docker-compose up -d rabbit redis bridge_affirmation bridge_request bridge_collected bridge_information bridge_senderhome bridge_senderforeign docker-compose run --rm e2e-tests rc=$? diff --git a/e2e-tests/run.js b/e2e-tests/run.js index d29c6c4..c03d531 100644 --- a/e2e-tests/run.js +++ b/e2e-tests/run.js @@ -39,6 +39,21 @@ const HOMEAMBABI = [ ], type: 'function', }, + { + name: 'enableAsyncRequestSelector', + inputs: [ + { + name: '', + type: 'bytes32', + }, + { + name: '', + type: 'bool', + }, + ], + outputs: [], + type: 'function', + }, ] const FOREIGNAMBABI = [ @@ -74,6 +89,8 @@ const scenarios = [ require('./scenarios/erc1155/bridgeNativeHomeTokens'), require('./scenarios/erc1155/bridgeNativeForeignTokensToOtherUser'), require('./scenarios/erc1155/bridgeNativeHomeTokensToOtherUser'), + require('./scenarios/erc1155/homePushMetadata'), + require('./scenarios/erc1155/homePullMetadata'), require('./scenarios/erc721/bridgeNativeForeignTokens'), require('./scenarios/erc721/bridgeNativeHomeTokens'), require('./scenarios/erc721/bridgeNativeForeignTokensToOtherUser'), @@ -82,6 +99,8 @@ const scenarios = [ require('./scenarios/erc721/fixHomeMediatorBalance'), require('./scenarios/erc721/homeRequestFailedMessageFix'), require('./scenarios/erc721/foreignRequestFailedMessageFix'), + require('./scenarios/erc721/homePushMetadata'), + require('./scenarios/erc721/homePullMetadata'), ] const { ZERO_ADDRESS, toAddress, addPendingTxLogger, signatureToVRS, packSignatures } = require('./utils') @@ -137,6 +156,9 @@ function makeWaitUntilProcessed(contract, finalizationEvent, blockNumber) { toBlock: 'latest', }) if (events.length > 0) { + if (finalizationEvent === 'InformationRetrieved') { + return events[0].returnValues.callbackStatus && events[0].transactionHash + } return events[0].returnValues.status && events[0].transactionHash } } @@ -266,6 +288,22 @@ function makeRelayToken(mediator, defaultFrom, isERC1155 = false) { } } +function makePullTokenOwner(mediator, from) { + return (token) => mediator.methods.pullTokenOwnerUpdate(toAddress(token)).send({ from }) +} + +function makePullTokenURI(mediator, from, isERC1155 = false) { + return (token, tokenId) => mediator.methods.pullTokenURIUpdate(toAddress(token), tokenId, isERC1155).send({ from }) +} + +function makePushTokenOwner(mediator, from) { + return (token) => mediator.methods.pushTokenOwnerUpdate(toAddress(token)).send({ from }) +} + +function makePushTokenURI(mediator, from, isERC1155 = false) { + return (token, tokenId) => mediator.methods.pushTokenURIUpdate(toAddress(token), tokenId, isERC1155).send({ from }) +} + async function createEnv(web3Home, web3Foreign) { console.log('Import accounts') const users = [] @@ -357,6 +395,7 @@ async function createEnv(web3Home, web3Foreign) { getBridgedTokenERC721: makeGetBridgedToken(web3Home, homeMediator, homeOptions, false), getBridgedTokenERC1155: makeGetBridgedToken(web3Home, homeMediator, homeOptions, true), waitUntilProcessed: makeWaitUntilProcessed(homeAMB, 'AffirmationCompleted', homeBlockNumber), + waitUntilInformationReceived: makeWaitUntilProcessed(homeAMB, 'InformationRetrieved', homeBlockNumber), withDisabledExecution: makeWithDisabledExecution(homeMediator, owner), checkTransferERC721: makeCheckTransfer(web3Home, false), checkTransferERC1155: makeCheckTransfer(web3Home, true, false), @@ -365,6 +404,12 @@ async function createEnv(web3Home, web3Foreign) { mintERC1155: makeMint(homeTokenERC1155, users[0], true), relayTokenERC721: makeRelayToken(homeMediator, users[0], false), relayTokenERC1155: makeRelayToken(homeMediator, users[0], true), + pullTokenOwnerUpdate: makePullTokenOwner(homeMediator, users[0]), + pullERC721URIUpdate: makePullTokenURI(homeMediator, users[0], false), + pullERC1155URIUpdate: makePullTokenURI(homeMediator, users[0], true), + pushTokenOwnerUpdate: makePushTokenOwner(homeMediator, users[0]), + pushERC721URIUpdate: makePushTokenURI(homeMediator, users[0], false), + pushERC1155URIUpdate: makePushTokenURI(homeMediator, users[0], true), }, foreign: { web3: web3Foreign, diff --git a/e2e-tests/scenarios/erc1155/homePullMetadata.js b/e2e-tests/scenarios/erc1155/homePullMetadata.js new file mode 100644 index 0000000..01a5bee --- /dev/null +++ b/e2e-tests/scenarios/erc1155/homePullMetadata.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const { sha3 } = require('web3').utils + +const NEW_EXAMPLE_URI = 'https://example.com' + +async function run({ home, foreign, users, owner }) { + console.log('Enable async AMB requests') + await home.amb.methods.enableAsyncRequestSelector(sha3('eth_call(address,bytes)'), true).send({ from: owner }) + + console.log('Bridging Native Foreign token to Home chain') + const id = await foreign.mintERC1155() + + console.log('Sending token to the Foreign Mediator') + await home.waitUntilProcessed(await foreign.relayTokenERC1155(foreign.erc1155Token, id)) + const bridgedToken = await home.getBridgedTokenERC1155(foreign.erc1155Token) + + await foreign.erc1155Token.methods.setOwner(users[0]).send() + await foreign.erc1155Token.methods.setTokenURI(id, NEW_EXAMPLE_URI).send() + + console.log('Pull metadata updates from Foreign chain') + const receipt1 = await home.pullTokenOwnerUpdate(bridgedToken) + const receipt2 = await home.pullERC721URIUpdate(bridgedToken, id) + const receipt3 = await home.pullERC1155URIUpdate(bridgedToken, id) + + const relayTxHash1 = await home.waitUntilInformationReceived(receipt1) + const relayTxHash2 = await home.waitUntilInformationReceived(receipt2) + const relayTxHash3 = await home.waitUntilInformationReceived(receipt3) + + assert.ok(!!relayTxHash1, 'Information request should have been executed') + assert.ok(!relayTxHash2, 'Information request should have been failed') + assert.ok(!!relayTxHash3, 'Information request should have been executed') + + assert.strictEqual(await bridgedToken.methods.owner().call(), users[0], 'Owner is not updated') + assert.strictEqual(await bridgedToken.methods.uri(id).call(), NEW_EXAMPLE_URI, 'Token URI is not updated') +} + +module.exports = { + name: 'Pulling updated metadata from the native foreign token', + shouldRun: ({ owner }) => !!owner, + run, +} diff --git a/e2e-tests/scenarios/erc1155/homePushMetadata.js b/e2e-tests/scenarios/erc1155/homePushMetadata.js new file mode 100644 index 0000000..e48d54d --- /dev/null +++ b/e2e-tests/scenarios/erc1155/homePushMetadata.js @@ -0,0 +1,36 @@ +const assert = require('assert') + +const NEW_EXAMPLE_URI = 'https://example.com' + +async function run({ home, foreign, users }) { + console.log('Bridging Native Home token to Foreign chain') + const id = await home.mintERC1155() + + console.log('Sending token to the Home Mediator') + await foreign.waitUntilProcessed(await home.relayTokenERC1155(home.erc1155Token, id)) + const bridgedToken = await foreign.getBridgedTokenERC1155(home.erc1155Token) + + await home.erc1155Token.methods.setOwner(users[0]).send() + await home.erc1155Token.methods.setTokenURI(id, NEW_EXAMPLE_URI).send() + + console.log('Push metadata updates to Foreign chain') + const receipt1 = await home.pushTokenOwnerUpdate(home.erc1155Token) + const receipt2 = await home.pushERC721URIUpdate(home.erc1155Token, id).catch(() => ({ status: false })) + const receipt3 = await home.pushERC1155URIUpdate(home.erc1155Token, id) + + const relayTxHash1 = await foreign.executeManually(receipt1) + const relayTxHash3 = await foreign.executeManually(receipt3) + + assert.ok(!!relayTxHash1, 'AMB request should have been executed') + assert.ok(!receipt2.status, 'AMB request should have been failed') + assert.ok(!!relayTxHash3, 'AMB request should have been executed') + + assert.strictEqual(await bridgedToken.methods.owner().call(), users[0], 'Owner is not updated') + assert.strictEqual(await bridgedToken.methods.uri(id).call(), NEW_EXAMPLE_URI, 'Token URI is not updated') +} + +module.exports = { + name: 'Pushing updated metadata to the bridged foreign token', + shouldRun: () => true, + run, +} diff --git a/e2e-tests/scenarios/erc721/homePullMetadata.js b/e2e-tests/scenarios/erc721/homePullMetadata.js new file mode 100644 index 0000000..ddabc71 --- /dev/null +++ b/e2e-tests/scenarios/erc721/homePullMetadata.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const { sha3 } = require('web3').utils + +const NEW_EXAMPLE_URI = 'https://example.com' + +async function run({ home, foreign, users, owner }) { + console.log('Enable async AMB requests') + await home.amb.methods.enableAsyncRequestSelector(sha3('eth_call(address,bytes)'), true).send({ from: owner }) + + console.log('Bridging Native Foreign token to Home chain') + const id = await foreign.mintERC721() + + console.log('Sending token to the Foreign Mediator') + await home.waitUntilProcessed(await foreign.relayTokenERC721(foreign.erc721Token, id)) + const bridgedToken = await home.getBridgedTokenERC721(foreign.erc721Token) + + await foreign.erc721Token.methods.setOwner(users[0]).send() + await foreign.erc721Token.methods.setTokenURI(id, NEW_EXAMPLE_URI).send() + + console.log('Pull metadata updates from Foreign chain') + const receipt1 = await home.pullTokenOwnerUpdate(bridgedToken) + const receipt2 = await home.pullERC721URIUpdate(bridgedToken, id) + const receipt3 = await home.pullERC1155URIUpdate(bridgedToken, id) + + const relayTxHash1 = await home.waitUntilInformationReceived(receipt1) + const relayTxHash2 = await home.waitUntilInformationReceived(receipt2) + const relayTxHash3 = await home.waitUntilInformationReceived(receipt3) + + assert.ok(!!relayTxHash1, 'Information request should have been executed') + assert.ok(!!relayTxHash2, 'Information request should have been executed') + assert.ok(!relayTxHash3, 'Information request should have been failed') + + assert.strictEqual(await bridgedToken.methods.owner().call(), users[0], 'Owner is not updated') + assert.strictEqual(await bridgedToken.methods.tokenURI(id).call(), NEW_EXAMPLE_URI, 'Token URI is not updated') +} + +module.exports = { + name: 'Pulling updated metadata from the native foreign token', + shouldRun: ({ owner }) => !!owner, + run, +} diff --git a/e2e-tests/scenarios/erc721/homePushMetadata.js b/e2e-tests/scenarios/erc721/homePushMetadata.js new file mode 100644 index 0000000..65c740c --- /dev/null +++ b/e2e-tests/scenarios/erc721/homePushMetadata.js @@ -0,0 +1,36 @@ +const assert = require('assert') + +const NEW_EXAMPLE_URI = 'https://example.com' + +async function run({ home, foreign, users }) { + console.log('Bridging Native Home token to Foreign chain') + const id = await home.mintERC721() + + console.log('Sending token to the Home Mediator') + await foreign.waitUntilProcessed(await home.relayTokenERC721(home.erc721Token, id)) + const bridgedToken = await foreign.getBridgedTokenERC721(home.erc721Token) + + await home.erc721Token.methods.setOwner(users[0]).send() + await home.erc721Token.methods.setTokenURI(id, NEW_EXAMPLE_URI).send() + + console.log('Push metadata updates to Foreign chain') + const receipt1 = await home.pushTokenOwnerUpdate(home.erc721Token) + const receipt2 = await home.pushERC721URIUpdate(home.erc721Token, id) + const receipt3 = await home.pushERC1155URIUpdate(home.erc721Token, id).catch(() => ({ status: false })) + + const relayTxHash1 = await foreign.executeManually(receipt1) + const relayTxHash2 = await foreign.executeManually(receipt2) + + assert.ok(!!relayTxHash1, 'AMB request should have been executed') + assert.ok(!!relayTxHash2, 'AMB request should have been executed') + assert.ok(!receipt3.status, 'AMB request should have been failed') + + assert.strictEqual(await bridgedToken.methods.owner().call(), users[0], 'Owner is not updated') + assert.strictEqual(await bridgedToken.methods.tokenURI(id).call(), NEW_EXAMPLE_URI, 'Token URI is not updated') +} + +module.exports = { + name: 'Pushing updated metadata to the bridged foreign token', + shouldRun: () => true, + run, +} diff --git a/test/omnibridge_nft/common.test.js b/test/omnibridge_nft/common.test.js index 0ba0f8c..6c76685 100644 --- a/test/omnibridge_nft/common.test.js +++ b/test/omnibridge_nft/common.test.js @@ -16,6 +16,7 @@ const selectors = { handleBridgedNFT: '0xb701e094', handleNativeNFT: '0x6ca48357', fixFailedMessage: '0x276fea8a', + updateBridgedTokenMetadata: '0xf4b7a41d', } const { expect } = require('chai') @@ -1904,6 +1905,119 @@ function runTests(accounts, isHome) { expect(events[2].returnValues.dataType).to.be.bignumber.equal('0') }) }) + + describe('token metadata update requests', () => { + describe('push token metadata updates', () => { + it('should send message with new token owner', async () => { + await token.setOwner(accounts[7]) + await contract.pushTokenOwnerUpdate(token.address).should.be.rejected + await sendFunctions[0]() + await contract.pushTokenOwnerUpdate(token.address) + + const data = token.contract.methods.setOwner(accounts[7]).encodeABI() + const updateData = + web3.eth.abi.encodeFunctionSignature('updateBridgedTokenMetadata(address,bytes)') + + web3.eth.abi.encodeParameters(['address', 'bytes'], [token.address, data]).slice(2) + + const events = await getEvents(ambBridgeContract, { event: 'MockedEvent' }) + expect(events.length).to.be.equal(2) + expect(events[1].returnValues.data).to.be.equal(updateData) + }) + + it('should send message with new token URI', async () => { + const tokenId = await mintNewERC721() + + await contract.pushTokenURIUpdate(token.address, tokenId, false).should.be.rejected + await sendFunctions[0](tokenId) + await contract.pushTokenURIUpdate(token.address, tokenId, false) + + const data = token.contract.methods.setTokenURI(tokenId, uriFor(tokenId)).encodeABI() + const updateData = + web3.eth.abi.encodeFunctionSignature('updateBridgedTokenMetadata(address,bytes)') + + web3.eth.abi.encodeParameters(['address', 'bytes'], [token.address, data]).slice(2) + + const events = await getEvents(ambBridgeContract, { event: 'MockedEvent' }) + expect(events.length).to.be.equal(2) + expect(events[1].returnValues.data).to.be.equal(updateData) + }) + }) + + describe('pull token metadata updates', () => { + beforeEach(async () => { + const deployData = deployAndHandleBridgedERC721({ tokenId: 1 }) + expect(await executeMessageCall(exampleMessageId, deployData)).to.be.equal(true) + token = await ERC721BridgeToken.at(await contract.bridgedTokenAddress(otherSideToken1)) + }) + + it('pull token owner update', async () => { + await contract.pullTokenOwnerUpdate(otherSideToken1).should.be.rejected + await contract.pullTokenOwnerUpdate(token.address) + + const events = await getEvents(ambBridgeContract, { event: 'MockedInformationRequest' }) + expect(events.length).to.be.equal(1) + expect(events[0].returnValues.selector).to.be.equal(web3.utils.sha3('eth_call(address,bytes)')) + const data = token.contract.methods.owner().encodeABI() + const requestData = web3.eth.abi.encodeParameters(['address', 'bytes'], [otherSideToken1, data]) + expect(events[0].returnValues.data).to.be.equal(requestData) + + expect(await token.owner()).to.be.equal(ZERO_ADDRESS) + let responseData = web3.eth.abi.encodeParameters(['address'], [accounts[7]]) + responseData = web3.eth.abi.encodeParameters(['bytes'], [responseData]) + const messageId = events[0].returnValues.messageId + await ambBridgeContract.executeInformationResponse(contract.address, messageId, true, responseData) + expect(await token.owner()).to.be.equal(accounts[7]) + }) + + it('pull token URI update', async () => { + await contract.pullTokenURIUpdate(otherSideToken1, 1, false).should.be.rejected + await contract.pullTokenURIUpdate(token.address, 1, false) + + const events = await getEvents(ambBridgeContract, { event: 'MockedInformationRequest' }) + expect(events.length).to.be.equal(1) + expect(events[0].returnValues.selector).to.be.equal(web3.utils.sha3('eth_call(address,bytes)')) + const data = token.contract.methods.tokenURI(1).encodeABI() + const requestData = web3.eth.abi.encodeParameters(['address', 'bytes'], [otherSideToken1, data]) + expect(events[0].returnValues.data).to.be.equal(requestData) + + expect(await token.tokenURI(1)).to.be.equal(uriFor(1)) + let responseData = web3.eth.abi.encodeParameters(['string'], [uriFor(123)]) + responseData = web3.eth.abi.encodeParameters(['bytes'], [responseData]) + const messageId = events[0].returnValues.messageId + await ambBridgeContract.executeInformationResponse(contract.address, messageId, true, responseData) + expect(await token.tokenURI(1)).to.be.equal(uriFor(123)) + }) + }) + }) + } else { + describe('token metadata updates', () => { + beforeEach(async () => { + const deployData = deployAndHandleBridgedERC721({ tokenId: 1 }) + expect(await executeMessageCall(exampleMessageId, deployData)).to.be.equal(true) + token = await ERC721BridgeToken.at(await contract.bridgedTokenAddress(otherSideToken1)) + }) + + it('should update owner', async () => { + expect(await token.owner()).to.be.equal(ZERO_ADDRESS) + const data = token.contract.methods.setOwner(accounts[7]).encodeABI() + const updateData = contract.contract.methods.updateBridgedTokenMetadata(otherSideToken1, data).encodeABI() + + expect(await executeMessageCall(failedMessageId, updateData, { messageSender: owner })).to.be.equal(false) + expect(await executeMessageCall(exampleMessageId, updateData)).to.be.equal(true) + + expect(await token.owner()).to.be.equal(accounts[7]) + }) + + it('should update token URI', async () => { + expect(await token.tokenURI(1)).to.be.equal(uriFor(1)) + const data = token.contract.methods.setTokenURI(1, uriFor(2)).encodeABI() + const updateData = contract.contract.methods.updateBridgedTokenMetadata(otherSideToken1, data).encodeABI() + + expect(await executeMessageCall(failedMessageId, updateData, { messageSender: owner })).to.be.equal(false) + expect(await executeMessageCall(exampleMessageId, updateData)).to.be.equal(true) + + expect(await token.tokenURI(1)).to.be.equal(uriFor(2)) + }) + }) } }) } diff --git a/truffle-config.js b/truffle-config.js index 99a96c6..a157dd7 100644 --- a/truffle-config.js +++ b/truffle-config.js @@ -20,7 +20,7 @@ module.exports = { settings: { optimizer: { enabled: true, - runs: 200, + runs: 10, }, evmVersion: 'istanbul', },