-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from lukso-network/hypLSP8
feat: HypLSP8 implementation
- Loading branch information
Showing
7 changed files
with
431 additions
and
1 deletion.
There are no files selected for viewing
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity >=0.8.19; | ||
|
||
import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol"; | ||
|
||
import { LSP8IdentifiableDigitalAssetInitAbstract } from | ||
"@lukso/lsp8-contracts/contracts/LSP8IdentifiableDigitalAssetInitAbstract.sol"; | ||
|
||
import { _LSP4_TOKEN_TYPE_TOKEN } from "@lukso/lsp4-contracts/contracts/LSP4Constants.sol"; | ||
|
||
import { _LSP8_TOKENID_FORMAT_NUMBER } from "@lukso/lsp8-contracts/contracts/LSP8Constants.sol"; | ||
|
||
/** | ||
* @title LSP8 version of the Hyperlane ERC721 Token Router | ||
* @dev https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/contracts/token/HypERC721.sol | ||
*/ | ||
contract HypLSP8 is LSP8IdentifiableDigitalAssetInitAbstract, TokenRouter { | ||
constructor(address _mailbox) TokenRouter(_mailbox) { } | ||
|
||
/** | ||
* @notice Initializes the Hyperlane router, LSP8 metadata, and mints initial supply to deployer. | ||
* @param _mintAmount The amount of NFTs to mint to `msg.sender`. | ||
* @param _name The name of the token. | ||
* @param _symbol The symbol of the token. | ||
*/ | ||
function initialize( | ||
uint256 _mintAmount, | ||
address _hook, | ||
address _interchainSecurityModule, | ||
address _owner, | ||
string memory _name, | ||
string memory _symbol | ||
) | ||
external | ||
initializer | ||
{ | ||
_MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); | ||
address owner = msg.sender; | ||
_transferOwnership(owner); | ||
|
||
LSP8IdentifiableDigitalAssetInitAbstract._initialize( | ||
_name, _symbol, owner, _LSP4_TOKEN_TYPE_TOKEN, _LSP8_TOKENID_FORMAT_NUMBER | ||
); | ||
|
||
for (uint256 i = 0; i < _mintAmount; i++) { | ||
_mint(owner, bytes32(i), true, ""); | ||
} | ||
} | ||
|
||
function balanceOf(address _account) | ||
public | ||
view | ||
virtual | ||
override(TokenRouter, LSP8IdentifiableDigitalAssetInitAbstract) | ||
returns (uint256) | ||
{ | ||
return LSP8IdentifiableDigitalAssetInitAbstract.balanceOf(_account); | ||
} | ||
|
||
/** | ||
* @dev Asserts `msg.sender` is owner and burns `_tokenId`. | ||
* @inheritdoc TokenRouter | ||
*/ | ||
function _transferFromSender(uint256 _tokenId) internal virtual override returns (bytes memory) { | ||
bytes32 tokenIdAsBytes32 = bytes32(_tokenId); | ||
require(tokenOwnerOf(tokenIdAsBytes32) == msg.sender, "!owner"); | ||
_burn(tokenIdAsBytes32, ""); | ||
return bytes(""); // no metadata | ||
} | ||
|
||
/** | ||
* @dev Mints `_tokenId` to `_recipient`. | ||
* @inheritdoc TokenRouter | ||
*/ | ||
function _transferTo( | ||
address _recipient, | ||
uint256 _tokenId, | ||
bytes calldata // no metadata | ||
) | ||
internal | ||
virtual | ||
override | ||
{ | ||
_mint(_recipient, bytes32(_tokenId), true, ""); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
pragma solidity >=0.8.19; | ||
|
||
import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol"; | ||
|
||
import { TokenMessage } from "@hyperlane-xyz/core/contracts/token/libs/TokenMessage.sol"; | ||
|
||
import { ILSP8IdentifiableDigitalAsset as ILSP8 } from | ||
"@lukso/lsp8-contracts/contracts/ILSP8IdentifiableDigitalAsset.sol"; | ||
|
||
/** | ||
* @title Hyperlane LSP8 Token Collateral that wraps an existing LSP8 with remote transfer functionality. | ||
* @author Abacus Works | ||
*/ | ||
contract HypLSP8Collateral is TokenRouter { | ||
ILSP8 public immutable wrappedToken; | ||
|
||
/** | ||
* @notice Constructor | ||
* @param lsp8 Address of the token to keep as collateral | ||
*/ | ||
constructor(address lsp8, address _mailbox) TokenRouter(_mailbox) { | ||
wrappedToken = ILSP8(lsp8); | ||
} | ||
|
||
/** | ||
* @notice Initializes the Hyperlane router | ||
* @param _hook The post-dispatch hook contract. | ||
* @param _interchainSecurityModule The interchain security module contract. | ||
* @param _owner The this contract. | ||
*/ | ||
function initialize(address _hook, address _interchainSecurityModule, address _owner) public virtual initializer { | ||
_MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); | ||
} | ||
|
||
function ownerOf(uint256 _tokenId) external view returns (address) { | ||
return ILSP8(wrappedToken).tokenOwnerOf(bytes32(_tokenId)); | ||
} | ||
|
||
/** | ||
* @dev Returns the balance of `_account` for `wrappedToken`. | ||
* @inheritdoc TokenRouter | ||
*/ | ||
function balanceOf(address _account) external view override returns (uint256) { | ||
return ILSP8(wrappedToken).balanceOf(_account); | ||
} | ||
|
||
/** | ||
* @dev Transfers `_tokenId` of `wrappedToken` from `msg.sender` to this contract. | ||
* @inheritdoc TokenRouter | ||
*/ | ||
function _transferFromSender(uint256 _tokenId) internal virtual override returns (bytes memory) { | ||
wrappedToken.transfer(msg.sender, address(this), bytes32(_tokenId), true, ""); | ||
return bytes(""); // no metadata | ||
} | ||
|
||
/** | ||
* @dev Transfers `_tokenId` of `wrappedToken` from this contract to `_recipient`. | ||
* @inheritdoc TokenRouter | ||
*/ | ||
function _transferTo( | ||
address _recipient, | ||
uint256 _tokenId, | ||
bytes calldata // no metadata | ||
) | ||
internal | ||
override | ||
{ | ||
wrappedToken.transfer(address(this), _recipient, bytes32(_tokenId), true, ""); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
// SPDX-License-Identifier: MIT OR Apache-2.0 | ||
pragma solidity ^0.8.13; | ||
|
||
import { Test } from "forge-std/src/Test.sol"; | ||
|
||
import { TypeCasts } from "@hyperlane-xyz/core/contracts/libs/TypeCasts.sol"; | ||
import { TestMailbox } from "@hyperlane-xyz/core/contracts/test/TestMailbox.sol"; | ||
import { TestPostDispatchHook } from "@hyperlane-xyz/core/contracts/test/TestPostDispatchHook.sol"; | ||
import { TestInterchainGasPaymaster } from "@hyperlane-xyz/core/contracts/test/TestInterchainGasPaymaster.sol"; | ||
import { GasRouter } from "@hyperlane-xyz/core/contracts/client/GasRouter.sol"; | ||
import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol"; | ||
|
||
// Mock + contracts to test | ||
import { HypLSP8 } from "../src/HypLSP8.sol"; | ||
import { HypLSP8Collateral } from "../src/HypLSP8Collateral.sol"; | ||
import { LSP8Mock } from "./LSP8Mock.sol"; | ||
|
||
abstract contract HypTokenTest is Test { | ||
using TypeCasts for address; | ||
|
||
uint256 internal constant INITIAL_SUPPLY = 10; | ||
string internal constant NAME = "Hyperlane NFTs"; | ||
string internal constant SYMBOL = "HNFT"; | ||
|
||
address internal ALICE = makeAddr("alice"); | ||
address internal BOB = makeAddr("bob"); | ||
address internal OWNER = makeAddr("owner"); | ||
uint32 internal constant ORIGIN = 11; | ||
uint32 internal constant DESTINATION = 22; | ||
bytes32 internal constant TOKEN_ID = bytes32(uint256(1)); | ||
string internal constant URI = "http://example.com/token/"; | ||
|
||
LSP8Mock internal localPrimaryToken; | ||
LSP8Mock internal remotePrimaryToken; | ||
TestMailbox internal localMailbox; | ||
TestMailbox internal remoteMailbox; | ||
TokenRouter internal localToken; | ||
HypLSP8 internal remoteToken; | ||
TestPostDispatchHook internal noopHook; | ||
|
||
function setUp() public virtual { | ||
localMailbox = new TestMailbox(ORIGIN); | ||
remoteMailbox = new TestMailbox(DESTINATION); | ||
|
||
localPrimaryToken = new LSP8Mock(NAME, SYMBOL, OWNER); | ||
|
||
noopHook = new TestPostDispatchHook(); | ||
localMailbox.setDefaultHook(address(noopHook)); | ||
localMailbox.setRequiredHook(address(noopHook)); | ||
|
||
vm.deal(ALICE, 1 ether); | ||
} | ||
|
||
function _deployRemoteToken() internal { | ||
remoteToken = new HypLSP8(address(remoteMailbox)); | ||
vm.prank(OWNER); | ||
remoteToken.initialize(0, address(noopHook), address(0), OWNER, NAME, SYMBOL); | ||
vm.prank(OWNER); | ||
remoteToken.enrollRemoteRouter(ORIGIN, address(localToken).addressToBytes32()); | ||
} | ||
|
||
function _processTransfers(address _recipient, bytes32 _tokenId) internal { | ||
vm.prank(address(remoteMailbox)); | ||
remoteToken.handle( | ||
ORIGIN, address(localToken).addressToBytes32(), abi.encodePacked(_recipient.addressToBytes32(), _tokenId) | ||
); | ||
} | ||
|
||
function _performRemoteTransfer(uint256 _msgValue, bytes32 _tokenId) internal { | ||
vm.prank(ALICE); | ||
localToken.transferRemote{ value: _msgValue }(DESTINATION, BOB.addressToBytes32(), uint256(_tokenId)); | ||
|
||
_processTransfers(BOB, _tokenId); | ||
assertEq(remoteToken.balanceOf(BOB), 1); | ||
} | ||
} | ||
|
||
contract HypLSP8Test is HypTokenTest { | ||
using TypeCasts for address; | ||
|
||
HypLSP8 internal lsp8Token; | ||
|
||
function setUp() public override { | ||
super.setUp(); | ||
|
||
localToken = new HypLSP8(address(localMailbox)); | ||
lsp8Token = HypLSP8(payable(address(localToken))); | ||
|
||
vm.prank(OWNER); | ||
lsp8Token.initialize(INITIAL_SUPPLY, address(noopHook), address(0), OWNER, NAME, SYMBOL); | ||
|
||
vm.prank(OWNER); | ||
lsp8Token.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); | ||
|
||
// Give accounts some ETH for gas | ||
vm.deal(OWNER, 1 ether); | ||
vm.deal(ALICE, 1 ether); | ||
vm.deal(BOB, 1 ether); | ||
|
||
// Transfer some tokens to ALICE for testing | ||
vm.prank(OWNER); | ||
lsp8Token.transfer(OWNER, ALICE, TOKEN_ID, true, ""); | ||
|
||
_deployRemoteToken(); | ||
} | ||
|
||
function testInitialize_revert_ifAlreadyInitialized() public { | ||
vm.expectRevert("Initializable: contract is already initialized"); | ||
lsp8Token.initialize(INITIAL_SUPPLY, address(noopHook), address(0), OWNER, NAME, SYMBOL); | ||
} | ||
|
||
function testTotalSupply() public { | ||
assertEq(lsp8Token.totalSupply(), INITIAL_SUPPLY); | ||
} | ||
|
||
function testTokenOwnerOf() public { | ||
assertEq(lsp8Token.tokenOwnerOf(TOKEN_ID), ALICE); | ||
} | ||
|
||
function testLocalTransfer() public { | ||
vm.prank(ALICE); | ||
lsp8Token.transfer(ALICE, BOB, TOKEN_ID, true, ""); | ||
assertEq(lsp8Token.tokenOwnerOf(TOKEN_ID), BOB); | ||
assertEq(lsp8Token.balanceOf(ALICE), 0); | ||
assertEq(lsp8Token.balanceOf(BOB), 1); | ||
} | ||
|
||
function testRemoteTransferHere() public { | ||
vm.prank(OWNER); | ||
remoteToken.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); | ||
|
||
_performRemoteTransfer(25_000, TOKEN_ID); | ||
assertEq(lsp8Token.balanceOf(ALICE), 0); | ||
} | ||
|
||
function testRemoteTransfer_revert_unauthorizedOperator() public { | ||
vm.prank(OWNER); | ||
vm.expectRevert("!owner"); | ||
localToken.transferRemote{ value: 25_000 }(DESTINATION, BOB.addressToBytes32(), uint256(TOKEN_ID)); | ||
} | ||
|
||
function testRemoteTransfer_revert_invalidTokenId() public { | ||
bytes32 invalidTokenId = bytes32(uint256(999)); | ||
vm.expectRevert(abi.encodeWithSignature("LSP8NonExistentTokenId(bytes32)", invalidTokenId)); | ||
_performRemoteTransfer(25_000, invalidTokenId); | ||
} | ||
} | ||
|
||
contract HypLSP8CollateralTest is HypTokenTest { | ||
using TypeCasts for address; | ||
|
||
HypLSP8Collateral internal lsp8Collateral; | ||
|
||
function setUp() public override { | ||
super.setUp(); | ||
|
||
localToken = new HypLSP8Collateral(address(localPrimaryToken), address(localMailbox)); | ||
lsp8Collateral = HypLSP8Collateral(address(localToken)); | ||
|
||
vm.prank(OWNER); | ||
lsp8Collateral.initialize(address(noopHook), address(0), OWNER); | ||
|
||
// Give accounts some ETH for gas | ||
vm.deal(OWNER, 1 ether); | ||
vm.deal(ALICE, 1 ether); | ||
vm.deal(BOB, 1 ether); | ||
|
||
// Mint test tokens | ||
vm.startPrank(OWNER); | ||
localPrimaryToken.mint(OWNER, TOKEN_ID, true, ""); | ||
localPrimaryToken.transfer(OWNER, ALICE, TOKEN_ID, true, ""); | ||
vm.stopPrank(); | ||
|
||
_deployRemoteToken(); | ||
|
||
// Enroll routers for both chains | ||
vm.prank(OWNER); | ||
lsp8Collateral.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32()); | ||
} | ||
|
||
function testRemoteTransfer() public { | ||
vm.prank(ALICE); | ||
localPrimaryToken.authorizeOperator(address(lsp8Collateral), TOKEN_ID, ""); | ||
_performRemoteTransfer(25_000, TOKEN_ID); | ||
|
||
assertEq(localPrimaryToken.tokenOwnerOf(TOKEN_ID), address(lsp8Collateral)); | ||
} | ||
|
||
function testRemoteTransfer_revert_unauthorized() public { | ||
vm.expectRevert( | ||
abi.encodeWithSignature("LSP8NotTokenOperator(bytes32,address)", TOKEN_ID, address(lsp8Collateral)) | ||
); | ||
vm.prank(BOB); | ||
localToken.transferRemote{ value: 25_000 }(DESTINATION, BOB.addressToBytes32(), uint256(TOKEN_ID)); | ||
} | ||
|
||
function testRemoteTransfer_revert_invalidTokenId() public { | ||
bytes32 invalidTokenId = bytes32(uint256(999)); | ||
vm.expectRevert(abi.encodeWithSignature("LSP8NonExistentTokenId(bytes32)", invalidTokenId)); | ||
_performRemoteTransfer(25_000, invalidTokenId); | ||
} | ||
} |
Oops, something went wrong.