Skip to content

Commit

Permalink
Merge pull request #12 from lukso-network/universal-receiver
Browse files Browse the repository at this point in the history
feat: add universalReceiver to HypLSP7Collateral
  • Loading branch information
CJ42 authored Mar 4, 2025
2 parents efc4ecb + 77507a5 commit 181d7d5
Show file tree
Hide file tree
Showing 9 changed files with 517 additions and 18 deletions.
2 changes: 2 additions & 0 deletions .solhint.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"compiler-version": ["error", ">=0.8.0"],
"func-name-mixedcase": "off",
"func-visibility": ["error", { "ignoreConstructors": true }],
"no-unused-import": "error",
"immutable-vars-naming": "error",
"named-parameters-mapping": "warn",
"no-console": "off",
"not-rely-on-time": "off",
Expand Down
70 changes: 70 additions & 0 deletions docs/ABI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# `HypERC20`

## Destination Gas

**Getter:**

- `destinationGas(uint32)` = param is the chain ID

**Setter:**

- `setDestinationGas(uint32,uint256)`
- `setDestinationGas(uint32[],uint256[])`

## Domains & Mailbox

**Getter:**

- `domains()`
- `localDomain()`
- `mailbox()`

## Routers

**Getter:**

- `routers(uint32)`

**Setter**

- `enrollRemoteRouter(uint32,bytes32)` (chain ID + destination of wrapper / collateral contract (abi-encoded))
- `enrollRemoteRouters(uint32[],bytes32[])`
- `unenrollRemoteRouter(uint32)`
- `unenrollRemoteRouters(uint32[])`

## Hooks

**Getter:**

- `hook()`

**Setter:**

- `setHook(address)`

## ISM

**Getter:**

- `interchainSecurityModule()`

**Setter:**

- `setInterchainSecurityModule(address)`

## Transferring functionalities

`quoteGasPayment(uint32)`

`transferRemote(uint32,bytes32,uint256)` = for bridging tokens, trigger the transfer from the source chain

- `uint32` = domain (chain ID)
- `bytes32` = recipient on destination chain
- `uint256` = amount

`transferRemote(unit32,bytes32,uint256,bytes,address)` = same as above with extra parameters

- `bytes` = hook metadata
- `address` = hook contract address

`handle(...)` = for receiving the bridged tokens on the destination chain
71 changes: 58 additions & 13 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,36 @@
## Bridging Flow Overview
# Bridging Flow Overview

The flow for bridging tokens is generally as follow. If the token is originally from:
The flow for bridging tokens is generally as follows.

### ETHEREUM -> LUKSO
```mermaid
graph LR;
User[User 👤] --> ERC20[ERC20 stablecoin 🪙];
subgraph Collateral_Chain[Ethereum]
ERC20 -- lock tokens into --> HypERC20Collateral[HypERC20Collateral 🏦🔒];
HypERC20Collateral -- informs --> Mailbox1[Hyperlane Mailbox 📭📥];
end
Mailbox1 -- to relay ✉️ bridge token --> Relayer[Hyperlane Relayer 🚚];
Relayer -- relaying ✉️ bridge tx to LUKSO chain --> Mailbox2[Hyperlane Mailbox 📬📤];
subgraph Synthetic_Chain[LUKSO]
Mailbox2 -- verify bridge transaction in source chain <--> ISM[Hashi ISM 👮🫡];
Mailbox2 --> HypLSP7[wrapped synthetic version of stablecoin as LSP7 🪙 📦];
end
HypLSP7 -- mint ⛏️ --> User2[User 👤]
Hashi[Hashi zk light client generating zk proofs 🔄⨐] <--> ISM
```

If the token is originally from:

**scenario 1:** the ERC20 token initially exists on Ethereum and was deployed there (_e.g: DAI, USDC, etc..._).
## ETHEREUM -> LUKSO

### Scenario 1: ERC20 on Ethereum (USDC) -> HypLSP7 on LUKSO

The ERC20 token initially exists on Ethereum and was deployed there (_e.g: DAI, USDC, etc..._).

The ERC20 token is locked on ETHEREUM, an HypLSP7 token is minted on LUKSO.

Expand All @@ -23,8 +49,10 @@ graph TD
end
```

**scenario 2:** the token was migrated from LUKSO to Ethereum and an HypERC20 token contract was created as a wrapper on
the Ethereum side (_e.g: wrapped Chillwhale or wrapped FABS as HypERC20_).
### Scenario 2: HypERC20 from Ethereum (wCHILL) -> LSP7 on LUKSO

The token was migrated from LUKSO to Ethereum and an HypERC20 token contract was created as a wrapper on the Ethereum
side (_e.g: wrapped Chillwhale or wrapped FABS as HypERC20_).

The user burns the wrapped token `HypERC20` on Ethereum, and the tokens are unlocked on the LUKSO side and transferred
to the user.
Expand All @@ -43,9 +71,11 @@ graph TD
end
```

### LUKSO -> ETHEREUM
## LUKSO -> ETHEREUM

- **scenario 3:** the LSP7 token was originally created and deployed on LUKSO (_e.g: Chillwhale, FABS, etc..._).
### Scenario 3: LSP7 on LUKSO (CHILL) -> HypERC20 on Ethereum

The LSP7 token was originally created and deployed on LUKSO (_e.g: Chillwhale, FABS, etc..._).

The user transfers the LSP7 token to its `HypLSP7Collateral` contract on LUKSO where it is locked. The HypERC20 token on
Ethereum is then minted for the user.
Expand All @@ -64,8 +94,23 @@ graph TD
end
```

- **scenario 4:** an ERC20 token was bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g:
wrapped DAI as HypLSP7_).
The flow of functions being called is as follow:

1. user approves the `HypLSP7Collateral` to spend x amount of CHILL tokens
2. user calls `transferRemote(uint32,address,uint256)` on `HypLSP7Collateral`

```solidity
transferRemote(
uint32 destination, // chain ID
address recipient,
uint256 amount
)
```

### Scenario 4: HypLSP7 on LUKSO (wUSDC) -> ERC20 on Ethereum

An ERC20 token was bridged from Ethereum to LUKSO and we want to bridge back to Ethereum (_e.g: wrapped DAI as
HypLSP7_).

This HypLSP7 token is burnt on LUKSO, on Ethereum it is unlocked.

Expand Down Expand Up @@ -164,9 +209,9 @@ graph TD
- [Cross Chain Alliance - Hashi](https://crosschain-alliance.gitbook.io/hashi)
- [Hyperlane smart contracts monorepo](https://github.com/hyperlane-xyz/hyperlane-monorepo)

[`HypERC20Collateral`]:
[`hyperc20collateral`]:
https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20Collateral.sol
[`HypERC20`]:
[`hyperc20`]:
https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/token/HypERC20.sol
[`Mailbox`]:
[`mailbox`]:
https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/%40hyperlane-xyz/core%405.2.0/solidity/contracts/Mailbox.sol
2 changes: 1 addition & 1 deletion src/HypLSP7.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pragma solidity >=0.8.19;
// modules
import { LSP7DigitalAssetInitAbstract } from "@lukso/lsp7-contracts/contracts/LSP7DigitalAssetInitAbstract.sol";
import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol";
import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

// constants
import { _LSP4_TOKEN_TYPE_TOKEN } from "@lukso/lsp4-contracts/contracts/LSP4Constants.sol";
Expand All @@ -14,6 +13,7 @@ import { _LSP4_TOKEN_TYPE_TOKEN } from "@lukso/lsp4-contracts/contracts/LSP4Cons
* @dev https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/solidity/contracts/token/HypERC20.sol
*/
contract HypLSP7 is LSP7DigitalAssetInitAbstract, TokenRouter {
// solhint-disable-next-line immutable-vars-naming
uint8 private immutable _decimals;

constructor(uint8 __decimals, address _mailbox) TokenRouter(_mailbox) {
Expand Down
2 changes: 2 additions & 0 deletions src/HypLSP7Collateral.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRoute
import { Address } from "@openzeppelin/contracts/utils/Address.sol";

contract HypLSP7Collateral is TokenRouter {
// solhint-disable-next-line immutable-vars-naming
ILSP7 public immutable wrappedToken;

/**
* @notice Constructor
* @param lsp7_ Address of the token to keep as collateral
*/
constructor(address lsp7_, address mailbox_) TokenRouter(mailbox_) {
// solhint-disable-next-line custom-errors
require(Address.isContract(lsp7_), "HypLSP7Collateral: invalid token");
wrappedToken = ILSP7(lsp7_);
}
Expand Down
177 changes: 177 additions & 0 deletions src/HypLSP7CollateralWithLSP1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19;

import { console } from "forge-std/src/console.sol";

// Interfaces
import { ILSP7DigitalAsset as ILSP7 } from "@lukso/lsp7-contracts/contracts/ILSP7DigitalAsset.sol";

// Modules
import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import { TokenRouter } from "@hyperlane-xyz/core/contracts/token/libs/TokenRouter.sol";

// Libraries
import { Address } from "@openzeppelin/contracts/utils/Address.sol";
import { TokenMessage } from "@hyperlane-xyz/core/contracts/token/libs/TokenMessage.sol";

// Constants
import { _TYPEID_LSP7_TOKENOPERATOR } from "@lukso/lsp7-contracts/contracts/LSP7Constants.sol";
import { _INTERFACEID_LSP1 } from "@lukso/lsp1-contracts/contracts/LSP1Constants.sol";

contract HypLSP7CollateralWithLSP1 is ERC165, TokenRouter {
// solhint-disable-next-line immutable-vars-naming
ILSP7 public immutable wrappedToken;

/**
* @notice Constructor
* @param lsp7_ Address of the token to keep as collateral
*/
constructor(address lsp7_, address mailbox_) TokenRouter(mailbox_) {
require(Address.isContract(lsp7_), "HypLSP7Collateral: invalid token");
wrappedToken = ILSP7(lsp7_);
}

function initialize(address _hook, address _interchainSecurityModule, address _owner) public virtual initializer {
_MailboxClient_initialize(_hook, _interchainSecurityModule, _owner);
}

/**
* @param typeId TypeId related to performing a bridge operation
* @param data The `lsp1Data` sent by the function `authorizeOperator(address,uint256,bytes)` when the internal hook
* below was triggered:
*
* User --> calls `authorizeOperator(...)` on LSP7 token to bridge with parameters:
* | address: router contract
* | uint256: amount to bridge
* | bytes: operatorNotificationData -> abi-encoded function call of `transferRemote(uint32 _destination, bytes32
* _recipient, uint256 _amountOrId)`
* V
*
* Triggered internally by the function `_notifyTokenOperator(...)` with lsp1Data
*
* ```
* abi.encode(address msg.sender (user), uint256 amount, bytes memory operatorNotificationData)
* ```
*
* transferRemote(uint32,bytes32,uint256) selector -> 0x81b4e8b4
*
* Tokens that authorize and dont call the universalReceiver on authorization, will get front-runned
*/
function universalReceiver(bytes32 typeId, bytes calldata data) public payable returns (bytes memory) {
if (typeId == _TYPEID_LSP7_TOKENOPERATOR) {
// 0x000000000000000000000000328809bc894f92807417d2dad6b7c998c1afdac6 -> msg.sender
// 0000000000000000000000000000000000000000000000056bc75e2d63100000 -> authorized amount (100 with 18
// decimals in hex)
// 0000000000000000000000000000000000000000000000000000000000000060 -> operatorNotificationData
// 0000000000000000000000000000000000000000000000000000000000000064
// 81b4e8b4 ->
// transferRemote(uint32,bytes32,uint256) selector
// 000000000000000000000000000000000000000000000000000000000000000c -> destination (= chainId, here 12 in
// hex)
// 0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e -> recipient address
// 0000000000000000000000000000000000000000000000056bc75e2d63100000 -> amount to transfer (100 with 18
// decimals in hex)
// 00000000000000000000000000000000000000000000000000000000 -> remaining padded to do 32 bytes
// words

// `authorizeOperator(address,uint256,bytes)` calldata (example)
// --------------------
// The `lsp1Data` sent by `authorizeOperator(...)` contains 3 arguments:
// - address: msg.sender (user) -> 32 bytes
// - uint256: amount authorized -> 32 bytes
// - bytes: operatorNotificationData -> which contains the encoded transferRemote(...) parameters
address from = address(uint160(uint256(bytes32(data[:32]))));

// if no data then revert
if (uint256(bytes32(data[96:128])) == 0) revert("Authorization and Bridge must happen in the same tx");

// Get the function selector (first 4 bytes after the offset + length)
bytes4 executeSelectorToRun = bytes4(data[128:132]);

// For transferRemote, we expect the following parameters (32 bytes each as abi-encoded:
uint32 destination = uint32(uint256(bytes32(data[132:164])));
bytes32 recipient = bytes32(data[164:196]);
uint256 amount = uint256(bytes32(data[196:228]));

// Check if it's a transferRemote call (0x81b4e8b4)
if (executeSelectorToRun == 0x81b4e8b4) {
require(msg.sender == address(wrappedToken), "transferRemote only possible from wrappedToken");

// Normally we should use:
// _transferRemote(
// destination,
// recipient,
// amount,
// 0 // default value for _gasAmount
// );

// But `_transferRemote(...)` uses `msg.sender` as the `from` to transfer tokens.
// Since we are dealing with a `universalReceiver(...)` callback on the HypLSP7Collateral contract
// triggered via the `<LSP7 token>.authorizeOperator(...)`, the `msg.sender` is the token contract,
// which shouldn't be.
// Therefore, we need to re-write the logic of the `_transferRemote(...)` to use the `from` extracted
// from the received `operatorNotificationData`
wrappedToken.transfer(from, address(this), amount, true, "");

bytes memory _tokenMessage = TokenMessage.format(recipient, amount, ""); // no token metadata

// normally `_transferRemote(...)` returns the message ID. We don't return it here (could be a problem
// for
// external contracts that interact with it and need it)
_Router_dispatch(
destination, msg.value, _tokenMessage, _GasRouter_hookMetadata(destination), address(hook)
);

emit SentTransferRemote(destination, recipient, amount);
} else {
revert("Invalid selector");
}

// making sure that there are no authorized amount left over and send it back to owner if that is the case
uint256 remainingAuthorizedAmount = ILSP7(msg.sender).authorizedAmountFor(address(this), from);
console.log("remainingAuthorizedAmount: ", remainingAuthorizedAmount);
if (remainingAuthorizedAmount != 0) {
ILSP7(msg.sender).transfer(from, address(this), remainingAuthorizedAmount, true, "");
uint256 remainingBalance = ILSP7(msg.sender).balanceOf(address(this));
ILSP7(msg.sender).transfer(address(this), from, remainingBalance, true, "");
}
}
return abi.encodePacked(true);
}

/**
* @dev See {IERC165-supportsInterface}.
*/
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == _INTERFACEID_LSP1 || super.supportsInterface(interfaceId);
}

function balanceOf(address _account) external view override returns (uint256) {
return wrappedToken.balanceOf(_account);
}

/**
* @dev Transfers `_amount` of `wrappedToken` from `msg.sender` to this contract.
* @inheritdoc TokenRouter
*/
function _transferFromSender(uint256 _amount) internal virtual override returns (bytes memory) {
wrappedToken.transfer(msg.sender, address(this), _amount, true, "");
return bytes(""); // no metadata
}

/**
* @dev Transfers `_amount` of `wrappedToken` from this contract to `_recipient`.
* @inheritdoc TokenRouter
*/
function _transferTo(
address _recipient,
uint256 _amount,
bytes calldata // no metadata
)
internal
virtual
override
{
wrappedToken.transfer(address(this), _recipient, _amount, true, "");
}
}
Loading

0 comments on commit 181d7d5

Please # to comment.