Skip to content

Commit

Permalink
Merge pull request #5 from lukso-network/hypLSP8
Browse files Browse the repository at this point in the history
feat: HypLSP8 implementation
  • Loading branch information
skimaharvey authored Nov 7, 2024
2 parents 6e2d323 + ad78c3b commit ddd8e09
Show file tree
Hide file tree
Showing 7 changed files with 431 additions and 1 deletion.
Binary file not shown.
Binary file added lukso-lsp8-contracts-0.16.0-rc-0.tgz
Binary file not shown.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"@erc725/smart-contracts-v8": "erc725-smart-contracts-v8-rc0.tgz",
"@hyperlane-xyz/core": "^5.3.0",
"@lukso/lsp4-contracts": "lukso-lsp4-contracts-0.16.0-rc.0.tgz",
"@lukso/lsp7-contracts": "lukso-lsp7-contracts-0.16.0-rc.0.tgz"
"@lukso/lsp7-contracts": "lukso-lsp7-contracts-0.16.0-rc.0.tgz",
"@lukso/lsp8-contracts": "lukso-lsp8-contracts-0.16.0-rc-0.tgz",
"@lukso/lsp17contractextension-contracts": "lukso-lsp17contractextension-contracts-0.16.0-rc.0.tgz"
},
"devDependencies": {
"forge-std": "github:foundry-rs/forge-std#v1.8.1",
Expand Down
86 changes: 86 additions & 0 deletions src/HypLSP8.sol
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, "");
}
}
71 changes: 71 additions & 0 deletions src/HypLSP8Collateral.sol
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, "");
}
}
202 changes: 202 additions & 0 deletions test/HypLSP8.t.sol
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);
}
}
Loading

0 comments on commit ddd8e09

Please sign in to comment.