Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HypLSP8 implementation #5

Merged
merged 8 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
74 changes: 74 additions & 0 deletions src/HypLSP8.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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 Hyperlane LSP8 Token Router that extends LSP8 with remote transfer functionality.
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
* @author Abacus Works
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
*/
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, string memory _name, string memory _symbol) external initializer {
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
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) {
require(tokenOwnerOf(bytes32(_tokenId)) == msg.sender, "!owner");
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
_burn(bytes32(_tokenId), "");
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, "");
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
}
}
70 changes: 70 additions & 0 deletions src/HypLSP8Collateral.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 } from "@lukso/lsp8-contracts/contracts/ILSP8IdentifiableDigitalAsset.sol";
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved

/**
* @title Hyperlane LSP8 Token Collateral that wraps an existing LSP8 with remote transfer functionality.
* @author Abacus Works
*/
contract HypLSP8Collateral is TokenRouter {
ILSP8IdentifiableDigitalAsset public immutable wrappedToken;

/**
* @notice Constructor
* @param lsp8 Address of the token to keep as collateral
*/
constructor(address lsp8, address _mailbox) TokenRouter(_mailbox) {
wrappedToken = ILSP8IdentifiableDigitalAsset(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 ILSP8IdentifiableDigitalAsset(wrappedToken).tokenOwnerOf(bytes32(_tokenId));
}

/**
* @dev Returns the balance of `_account` for `wrappedToken`.
* @inheritdoc TokenRouter
*/
function balanceOf(address _account) external view override returns (uint256) {
return ILSP8IdentifiableDigitalAsset(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, "");
}
}
218 changes: 218 additions & 0 deletions test/HypLSP8.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// 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));

remoteToken = new HypLSP8(address(remoteMailbox));

vm.prank(OWNER);
remoteToken.initialize(0, NAME, SYMBOL);

vm.deal(ALICE, 1 ether);
}

function _deployRemoteToken(bool isCollateral) internal {
if (isCollateral) {
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
remoteToken = new HypLSP8(address(remoteMailbox));
vm.prank(OWNER);
remoteToken.initialize(0, NAME, SYMBOL);
} else {
remoteToken = new HypLSP8(address(remoteMailbox));
vm.prank(OWNER);
remoteToken.initialize(0, NAME, SYMBOL);
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
}
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));
CJ42 marked this conversation as resolved.
Show resolved Hide resolved
lsp8Token = HypLSP8(payable(address(localToken)));

vm.prank(OWNER);
lsp8Token.initialize(INITIAL_SUPPLY, NAME, SYMBOL);

vm.prank(OWNER);
lsp8Token.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32());

vm.deal(OWNER, 1 ether);
skimaharvey marked this conversation as resolved.
Show resolved Hide resolved
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(false);
}

function testInitialize_revert_ifAlreadyInitialized() public {
vm.expectRevert("Initializable: contract is already initialized");
lsp8Token.initialize(INITIAL_SUPPLY, 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();

// Deploy remote token
remoteToken = new HypLSP8(address(remoteMailbox));
vm.prank(OWNER);
remoteToken.initialize(0, NAME, SYMBOL);

// Enroll routers for both chains
vm.prank(OWNER);
lsp8Collateral.enrollRemoteRouter(DESTINATION, address(remoteToken).addressToBytes32());

vm.prank(OWNER);
remoteToken.enrollRemoteRouter(ORIGIN, address(localToken).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);
CJ42 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading