Skip to content

Commit 2c19d29

Browse files
committed
feat(core): HypNativeWethWrapper adapter and CREATE2 factory
Lets WETH holders bridge through an existing HypNative route by pulling WETH, unwrapping, and forwarding. Extends ITokenBridge with token(); retypes AbstractPredicateWrapper.token to address and updates mocks accordingly.
1 parent 37455fb commit 2c19d29

13 files changed

Lines changed: 568 additions & 20 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/core': minor
3+
---
4+
5+
A `HypNativeWethWrapper` adapter and CREATE2-based factory were added, letting WETH holders bridge through an existing `HypNative` route by pulling WETH, unwrapping, and forwarding. `ITokenBridge` was extended with a `token()` accessor (already present on all concrete implementations); `AbstractPredicateWrapper`'s `token` was retyped from `IERC20` to `address` and mocks were updated to expose `token()`.

solidity/contracts/interfaces/ITokenBridge.sol

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ interface ITokenFee {
2525
}
2626

2727
interface ITokenBridge is ITokenFee {
28+
/**
29+
* @notice Returns the ERC20 token managed by this bridge, or address(0) for native.
30+
* @dev Callers use this to know which token to approve for `transferRemote`.
31+
*/
32+
function token() external view returns (address);
33+
2834
/**
2935
* @notice Transfer value to another domain
3036
* @param _destination The destination domain of the message

solidity/contracts/mock/MockValueTransferBridge.sol

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ contract MockValueTransferBridge is Router, ITokenBridge {
3030
_MailboxClient_initialize(_hook, _ism, _owner);
3131
}
3232

33+
function token() external view override returns (address) {
34+
return collateral;
35+
}
36+
3337
function quoteTransferRemote(
3438
uint32 _destinationDomain,
3539
bytes32 _recipient,

solidity/contracts/token/bridge/TokenBridgeDepositAddress.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ contract TokenBridgeDepositAddress is
7676
_transferOwnership(_owner);
7777
}
7878

79-
function token() public view returns (address) {
79+
function token() public view override returns (address) {
8080
return address(wrappedToken);
8181
}
8282

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity >=0.8.0;
3+
4+
// ============ Internal Imports ============
5+
import {IWETH} from "../interfaces/IWETH.sol";
6+
import {HypNative} from "../HypNative.sol";
7+
import {ITokenBridge, ITokenFee, Quote} from "../../interfaces/ITokenBridge.sol";
8+
import {Quotes} from "../libs/Quotes.sol";
9+
10+
// ============ External Imports ============
11+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
12+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
13+
14+
/**
15+
* @title HypNativeWethWrapper
16+
* @notice Entry point that pulls WETH from the sender, unwraps to native, and
17+
* forwards the transfer to an existing HypNative router.
18+
* @dev Caller approves WETH for the full amount reported by `quoteTransferRemote`
19+
* (bridged amount + IGP fee + any external fees). Since the wrapper quotes
20+
* and pulls the exact native-equivalent before dispatch, no refund path is
21+
* needed and `msg.value` must be zero.
22+
*/
23+
contract HypNativeWethWrapper is ITokenBridge {
24+
using SafeERC20 for IERC20;
25+
using Quotes for Quote[];
26+
27+
IWETH private immutable weth;
28+
HypNative private immutable hypNative;
29+
30+
constructor(IWETH _weth, HypNative _hypNative) {
31+
require(
32+
_hypNative.token() == address(0),
33+
"Wrapper: HypNative required"
34+
);
35+
weth = _weth;
36+
hypNative = _hypNative;
37+
}
38+
39+
/**
40+
* @notice Returns the ERC20 token callers must approve for `transferRemote`.
41+
* @dev Mirrors `TokenRouter.token()`; always the wrapper's canonical WETH.
42+
*/
43+
function token() external view override returns (address) {
44+
return address(weth);
45+
}
46+
47+
/**
48+
* @inheritdoc ITokenBridge
49+
* @dev Pulls the full native-equivalent (bridged amount + all fees) as WETH,
50+
* unwraps, and forwards to the underlying HypNative.
51+
*/
52+
function transferRemote(
53+
uint32 _destination,
54+
bytes32 _recipient,
55+
uint256 _amount
56+
) external payable override returns (bytes32 messageId) {
57+
require(msg.value == 0, "Wrapper: msg.value must be 0");
58+
59+
uint256 total = hypNative
60+
.quoteTransferRemote(_destination, _recipient, _amount)
61+
.extract(address(0));
62+
63+
IERC20(address(weth)).safeTransferFrom(
64+
msg.sender,
65+
address(this),
66+
total
67+
);
68+
weth.withdraw(total);
69+
70+
messageId = hypNative.transferRemote{value: total}(
71+
_destination,
72+
_recipient,
73+
_amount
74+
);
75+
}
76+
77+
/**
78+
* @inheritdoc ITokenFee
79+
* @dev Mirrors the 3-quote shape of other collateral routers
80+
* (index 0: gas payment, index 1: bridged amount + internal fee,
81+
* index 2: external fee). Each native-denominated entry from the
82+
* underlying HypNative is rewritten to WETH, since the caller pays
83+
* entirely in WETH.
84+
*/
85+
function quoteTransferRemote(
86+
uint32 _destination,
87+
bytes32 _recipient,
88+
uint256 _amount
89+
) external view override returns (Quote[] memory quotes) {
90+
quotes = hypNative.quoteTransferRemote(
91+
_destination,
92+
_recipient,
93+
_amount
94+
);
95+
for (uint256 i = 0; i < quotes.length; i++) {
96+
if (quotes[i].token == address(0)) {
97+
quotes[i].token = address(weth);
98+
}
99+
}
100+
}
101+
102+
// Receive ETH from WETH.withdraw during transferRemote.
103+
receive() external payable {}
104+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
pragma solidity >=0.8.0;
3+
4+
// ============ Internal Imports ============
5+
import {IWETH} from "../interfaces/IWETH.sol";
6+
import {HypNative} from "../HypNative.sol";
7+
import {HypNativeWethWrapper} from "./HypNativeWethWrapper.sol";
8+
9+
// ============ External Imports ============
10+
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
11+
12+
/**
13+
* @title HypNativeWethWrapperFactory
14+
* @notice Deploys CREATE2 `HypNativeWethWrapper` instances for a fixed WETH.
15+
* @dev The factory commits to a single WETH address (canonical per chain) and
16+
* produces one deterministic wrapper per `HypNative` router. Wrappers are
17+
* immutable and non-ownable; the factory itself holds no state.
18+
*/
19+
contract HypNativeWethWrapperFactory {
20+
IWETH public immutable weth;
21+
22+
event WrapperDeployed(
23+
HypNative indexed hypNative,
24+
HypNativeWethWrapper wrapper
25+
);
26+
27+
constructor(IWETH _weth) {
28+
weth = _weth;
29+
}
30+
31+
/**
32+
* @notice Deploys a wrapper for `_hypNative` if one does not exist.
33+
* @param _hypNative The HypNative router to wrap.
34+
* @return wrapper The deployed (or pre-existing) wrapper.
35+
*/
36+
function deploy(
37+
HypNative _hypNative
38+
) external returns (HypNativeWethWrapper wrapper) {
39+
wrapper = getAddress(_hypNative);
40+
if (address(wrapper).code.length == 0) {
41+
Create2.deploy(0, bytes32(0), _initCode(_hypNative));
42+
emit WrapperDeployed(_hypNative, wrapper);
43+
}
44+
}
45+
46+
/**
47+
* @notice Returns the deterministic wrapper address for `_hypNative`.
48+
*/
49+
function getAddress(
50+
HypNative _hypNative
51+
) public view returns (HypNativeWethWrapper) {
52+
return
53+
HypNativeWethWrapper(
54+
payable(
55+
Create2.computeAddress(
56+
bytes32(0),
57+
keccak256(_initCode(_hypNative))
58+
)
59+
)
60+
);
61+
}
62+
63+
function _initCode(
64+
HypNative _hypNative
65+
) private view returns (bytes memory) {
66+
return
67+
abi.encodePacked(
68+
type(HypNativeWethWrapper).creationCode,
69+
abi.encode(weth, _hypNative)
70+
);
71+
}
72+
}

solidity/contracts/token/extensions/PredicateCrossCollateralRouterWrapper.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ contract PredicateCrossCollateralRouterWrapper is
7373
string memory _policyID
7474
) AbstractPredicateWrapper(_crossCollateralRouter, _registry, _policyID) {
7575
// CrossCollateralRouter always has a non-zero token (native not supported)
76-
if (address(token) == address(0))
76+
if (token == address(0))
7777
revert IPredicateWrapper
7878
.PredicateRouterWrapper__NativeTokenUnsupported();
7979
}

solidity/contracts/token/extensions/PredicateRouterWrapper.sol

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ contract PredicateRouterWrapper is AbstractPredicateWrapper {
7070
// ============ Views ============
7171

7272
function tokenType() public view returns (TokenType) {
73-
address tokenAddress = address(token);
74-
if (tokenAddress == address(0)) return TokenType.Native;
75-
if (tokenAddress == address(warpRoute)) return TokenType.Synthetic;
73+
if (token == address(0)) return TokenType.Native;
74+
if (token == address(warpRoute)) return TokenType.Synthetic;
7675
return TokenType.Collateral;
7776
}
7877

solidity/contracts/token/libs/AbstractPredicateWrapper.sol

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ abstract contract AbstractPredicateWrapper is
5656
/// @notice The underlying warpRoute being wrapped
5757
TokenRouter public immutable warpRoute;
5858

59-
/// @notice The ERC20 token managed by the warpRoute
60-
IERC20 public immutable token;
59+
/// @notice The ERC20 token managed by the warpRoute (address(0) for native)
60+
address public immutable token;
6161

6262
/// @notice The local domain ID (cached from warpRoute during construction)
6363
uint32 public immutable localDomain;
@@ -88,15 +88,14 @@ abstract contract AbstractPredicateWrapper is
8888
revert IPredicateWrapper.PredicateRouterWrapper__InvalidPolicy();
8989

9090
warpRoute = TokenRouter(_warpRoute);
91-
address tokenAddress = warpRoute.token();
92-
token = IERC20(tokenAddress);
91+
token = warpRoute.token();
9392
localDomain = warpRoute.localDomain();
9493

9594
_initPredicateClient(_registry, _policyID);
9695

9796
// Infinite approval to warpRoute for token transfers (skip for native)
98-
if (tokenAddress != address(0)) {
99-
IERC20(tokenAddress).forceApprove(_warpRoute, type(uint256).max);
97+
if (token != address(0)) {
98+
IERC20(token).forceApprove(_warpRoute, type(uint256).max);
10099
}
101100
}
102101

@@ -108,10 +107,10 @@ abstract contract AbstractPredicateWrapper is
108107
revert IPredicateWrapper
109108
.PredicateRouterWrapper__InsufficientValue();
110109

111-
if (address(token) == address(0)) return totalNativeRequired;
112-
uint256 totalTokenRequired = Quotes.extract(quotes, address(token));
110+
if (token == address(0)) return totalNativeRequired;
111+
uint256 totalTokenRequired = Quotes.extract(quotes, token);
113112
if (totalTokenRequired > 0) {
114-
token.safeTransferFrom(
113+
IERC20(token).safeTransferFrom(
115114
msg.sender,
116115
address(this),
117116
totalTokenRequired

solidity/contracts/token/libs/TokenRouter.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ abstract contract TokenRouter is GasRouter, ITokenBridge {
9898
* @dev This function must be implemented by derived contracts to specify the token address.
9999
* @return The address of the token contract.
100100
*/
101-
function token() public view virtual returns (address);
101+
function token() public view virtual override returns (address);
102102

103103
/**
104104
* @inheritdoc ITokenFee

0 commit comments

Comments
 (0)