diff --git a/.changeset/hyp-native-weth-wrapper.md b/.changeset/hyp-native-weth-wrapper.md new file mode 100644 index 00000000000..7169fe301c5 --- /dev/null +++ b/.changeset/hyp-native-weth-wrapper.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +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()`. diff --git a/solidity/contracts/interfaces/ITokenBridge.sol b/solidity/contracts/interfaces/ITokenBridge.sol index 9fddf6f4960..1567ef27296 100644 --- a/solidity/contracts/interfaces/ITokenBridge.sol +++ b/solidity/contracts/interfaces/ITokenBridge.sol @@ -25,6 +25,12 @@ interface ITokenFee { } interface ITokenBridge is ITokenFee { + /** + * @notice Returns the ERC20 token managed by this bridge, or address(0) for native. + * @dev Callers use this to know which token to approve for `transferRemote`. + */ + function token() external view returns (address); + /** * @notice Transfer value to another domain * @param _destination The destination domain of the message diff --git a/solidity/contracts/mock/MockValueTransferBridge.sol b/solidity/contracts/mock/MockValueTransferBridge.sol index ee4998acc42..2759de9738e 100644 --- a/solidity/contracts/mock/MockValueTransferBridge.sol +++ b/solidity/contracts/mock/MockValueTransferBridge.sol @@ -30,6 +30,10 @@ contract MockValueTransferBridge is Router, ITokenBridge { _MailboxClient_initialize(_hook, _ism, _owner); } + function token() external view override returns (address) { + return collateral; + } + function quoteTransferRemote( uint32 _destinationDomain, bytes32 _recipient, diff --git a/solidity/contracts/mock/MockWETH.sol b/solidity/contracts/mock/MockWETH.sol index d95f4b909c1..f4ab7e8f0ea 100644 --- a/solidity/contracts/mock/MockWETH.sol +++ b/solidity/contracts/mock/MockWETH.sol @@ -3,40 +3,75 @@ pragma solidity ^0.8.22; import {IWETH} from "../token/interfaces/IWETH.sol"; +/// @dev Minimal WETH9-compatible mock for tests. contract MockWETH is IWETH { - function allowance( - address _owner, - address _spender - ) external view returns (uint256) {} + string public constant name = "Wrapped Ether"; + string public constant symbol = "WETH"; + uint8 public constant decimals = 18; - function approve( - address _spender, - uint256 _amount - ) external returns (bool) { - return true; - } + mapping(address => uint256) public override balanceOf; + mapping(address => mapping(address => uint256)) public override allowance; - function balanceOf(address _account) external view returns (uint256) { - return 0; + event Deposit(address indexed account, uint256 amount); + event Withdrawal(address indexed account, uint256 amount); + + function totalSupply() external view override returns (uint256) { + return address(this).balance; } - function deposit() external payable {} + function deposit() public payable override { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } - function totalSupply() external view returns (uint256) { - return address(this).balance; + function withdraw(uint256 amount) external override { + require(balanceOf[msg.sender] >= amount, "WETH: balance"); + balanceOf[msg.sender] -= amount; + (bool ok, ) = msg.sender.call{value: amount}(""); + require(ok, "WETH: send failed"); + emit Withdrawal(msg.sender, amount); } - function transfer(address to, uint256 amount) external returns (bool) { + function approve( + address spender, + uint256 amount + ) external override returns (bool) { + allowance[msg.sender][spender] = amount; return true; } + function transfer( + address to, + uint256 amount + ) external override returns (bool) { + return _transfer(msg.sender, to, amount); + } + function transferFrom( address from, address to, uint256 amount - ) external returns (bool) { + ) external override returns (bool) { + uint256 allowed = allowance[from][msg.sender]; + if (allowed != type(uint256).max) { + require(allowed >= amount, "WETH: allowance"); + allowance[from][msg.sender] = allowed - amount; + } + return _transfer(from, to, amount); + } + + function _transfer( + address from, + address to, + uint256 amount + ) internal returns (bool) { + require(balanceOf[from] >= amount, "WETH: balance"); + balanceOf[from] -= amount; + balanceOf[to] += amount; return true; } - function withdraw(uint256 amount) external {} + receive() external payable { + deposit(); + } } diff --git a/solidity/contracts/token/bridge/TokenBridgeDepositAddress.sol b/solidity/contracts/token/bridge/TokenBridgeDepositAddress.sol index 95b5a2704c9..1a806f31942 100644 --- a/solidity/contracts/token/bridge/TokenBridgeDepositAddress.sol +++ b/solidity/contracts/token/bridge/TokenBridgeDepositAddress.sol @@ -76,7 +76,7 @@ contract TokenBridgeDepositAddress is _transferOwnership(_owner); } - function token() public view returns (address) { + function token() public view override returns (address) { return address(wrappedToken); } diff --git a/solidity/contracts/token/extensions/HypNativeWethWrapper.sol b/solidity/contracts/token/extensions/HypNativeWethWrapper.sol new file mode 100644 index 00000000000..af7ef29d9f2 --- /dev/null +++ b/solidity/contracts/token/extensions/HypNativeWethWrapper.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {IWETH} from "../interfaces/IWETH.sol"; +import {HypNative} from "../HypNative.sol"; +import {ITokenBridge, ITokenFee, Quote} from "../../interfaces/ITokenBridge.sol"; +import {Quotes} from "../libs/Quotes.sol"; + +// ============ External Imports ============ +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +/** + * @title HypNativeWethWrapper + * @notice Entry point that pulls WETH from the sender, unwraps to native, and + * forwards the transfer to an existing HypNative router. + * @dev Caller approves WETH for the full amount reported by `quoteTransferRemote` + * (bridged amount + IGP fee + any external fees). Since the wrapper quotes + * and pulls the exact native-equivalent before dispatch, no refund path is + * needed and `msg.value` must be zero. + */ +contract HypNativeWethWrapper is ITokenBridge { + using SafeERC20 for IERC20; + using Quotes for Quote[]; + + IWETH private immutable weth; + HypNative private immutable hypNative; + + constructor(IWETH _weth, HypNative _hypNative) { + require( + _hypNative.token() == address(0), + "Wrapper: HypNative required" + ); + weth = _weth; + hypNative = _hypNative; + } + + /** + * @notice Returns the ERC20 token callers must approve for `transferRemote`. + * @dev Mirrors `TokenRouter.token()`; always the wrapper's canonical WETH. + */ + function token() external view override returns (address) { + return address(weth); + } + + /** + * @inheritdoc ITokenBridge + * @dev Pulls the full native-equivalent (bridged amount + all fees) as WETH, + * unwraps, and forwards to the underlying HypNative. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable override returns (bytes32 messageId) { + require(msg.value == 0, "Wrapper: msg.value must be 0"); + + uint256 total = hypNative + .quoteTransferRemote(_destination, _recipient, _amount) + .extract(address(0)); + + IERC20(address(weth)).safeTransferFrom( + msg.sender, + address(this), + total + ); + weth.withdraw(total); + + messageId = hypNative.transferRemote{value: total}( + _destination, + _recipient, + _amount + ); + } + + /** + * @inheritdoc ITokenFee + * @dev Mirrors the 3-quote shape of other collateral routers + * (index 0: gas payment, index 1: bridged amount + internal fee, + * index 2: external fee). Each native-denominated entry from the + * underlying HypNative is rewritten to WETH, since the caller pays + * entirely in WETH. + */ + function quoteTransferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external view override returns (Quote[] memory quotes) { + quotes = hypNative.quoteTransferRemote( + _destination, + _recipient, + _amount + ); + for (uint256 i = 0; i < quotes.length; i++) { + if (quotes[i].token == address(0)) { + quotes[i].token = address(weth); + } + } + } + + // Receive ETH from WETH.withdraw during transferRemote. + receive() external payable {} +} diff --git a/solidity/contracts/token/extensions/HypNativeWethWrapperFactory.sol b/solidity/contracts/token/extensions/HypNativeWethWrapperFactory.sol new file mode 100644 index 00000000000..31f57851ef1 --- /dev/null +++ b/solidity/contracts/token/extensions/HypNativeWethWrapperFactory.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +// ============ Internal Imports ============ +import {IWETH} from "../interfaces/IWETH.sol"; +import {HypNative} from "../HypNative.sol"; +import {HypNativeWethWrapper} from "./HypNativeWethWrapper.sol"; + +// ============ External Imports ============ +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +/** + * @title HypNativeWethWrapperFactory + * @notice Deploys CREATE2 `HypNativeWethWrapper` instances for a fixed WETH. + * @dev The factory commits to a single WETH address (canonical per chain) and + * produces one deterministic wrapper per `HypNative` router. Wrappers are + * immutable and non-ownable; the factory itself holds no state. + */ +contract HypNativeWethWrapperFactory { + IWETH public immutable weth; + + event WrapperDeployed( + HypNative indexed hypNative, + HypNativeWethWrapper wrapper + ); + + constructor(IWETH _weth) { + weth = _weth; + } + + /** + * @notice Deploys a wrapper for `_hypNative` if one does not exist. + * @param _hypNative The HypNative router to wrap. + * @return wrapper The deployed (or pre-existing) wrapper. + */ + function deploy( + HypNative _hypNative + ) external returns (HypNativeWethWrapper wrapper) { + wrapper = getAddress(_hypNative); + if (address(wrapper).code.length == 0) { + Create2.deploy(0, bytes32(0), _initCode(_hypNative)); + emit WrapperDeployed(_hypNative, wrapper); + } + } + + /** + * @notice Returns the deterministic wrapper address for `_hypNative`. + */ + function getAddress( + HypNative _hypNative + ) public view returns (HypNativeWethWrapper) { + return + HypNativeWethWrapper( + payable( + Create2.computeAddress( + bytes32(0), + keccak256(_initCode(_hypNative)) + ) + ) + ); + } + + function _initCode( + HypNative _hypNative + ) private view returns (bytes memory) { + return + abi.encodePacked( + type(HypNativeWethWrapper).creationCode, + abi.encode(weth, _hypNative) + ); + } +} diff --git a/solidity/contracts/token/extensions/PredicateCrossCollateralRouterWrapper.sol b/solidity/contracts/token/extensions/PredicateCrossCollateralRouterWrapper.sol index dfbc6a83f7b..0f59f26edcb 100644 --- a/solidity/contracts/token/extensions/PredicateCrossCollateralRouterWrapper.sol +++ b/solidity/contracts/token/extensions/PredicateCrossCollateralRouterWrapper.sol @@ -73,7 +73,7 @@ contract PredicateCrossCollateralRouterWrapper is string memory _policyID ) AbstractPredicateWrapper(_crossCollateralRouter, _registry, _policyID) { // CrossCollateralRouter always has a non-zero token (native not supported) - if (address(token) == address(0)) + if (token == address(0)) revert IPredicateWrapper .PredicateRouterWrapper__NativeTokenUnsupported(); } diff --git a/solidity/contracts/token/extensions/PredicateRouterWrapper.sol b/solidity/contracts/token/extensions/PredicateRouterWrapper.sol index bbbe51c90d0..7b22d148254 100644 --- a/solidity/contracts/token/extensions/PredicateRouterWrapper.sol +++ b/solidity/contracts/token/extensions/PredicateRouterWrapper.sol @@ -70,9 +70,8 @@ contract PredicateRouterWrapper is AbstractPredicateWrapper { // ============ Views ============ function tokenType() public view returns (TokenType) { - address tokenAddress = address(token); - if (tokenAddress == address(0)) return TokenType.Native; - if (tokenAddress == address(warpRoute)) return TokenType.Synthetic; + if (token == address(0)) return TokenType.Native; + if (token == address(warpRoute)) return TokenType.Synthetic; return TokenType.Collateral; } diff --git a/solidity/contracts/token/libs/AbstractPredicateWrapper.sol b/solidity/contracts/token/libs/AbstractPredicateWrapper.sol index faf3a1352ab..5df00de9eae 100644 --- a/solidity/contracts/token/libs/AbstractPredicateWrapper.sol +++ b/solidity/contracts/token/libs/AbstractPredicateWrapper.sol @@ -56,8 +56,8 @@ abstract contract AbstractPredicateWrapper is /// @notice The underlying warpRoute being wrapped TokenRouter public immutable warpRoute; - /// @notice The ERC20 token managed by the warpRoute - IERC20 public immutable token; + /// @notice The ERC20 token managed by the warpRoute (address(0) for native) + address public immutable token; /// @notice The local domain ID (cached from warpRoute during construction) uint32 public immutable localDomain; @@ -88,15 +88,14 @@ abstract contract AbstractPredicateWrapper is revert IPredicateWrapper.PredicateRouterWrapper__InvalidPolicy(); warpRoute = TokenRouter(_warpRoute); - address tokenAddress = warpRoute.token(); - token = IERC20(tokenAddress); + token = warpRoute.token(); localDomain = warpRoute.localDomain(); _initPredicateClient(_registry, _policyID); // Infinite approval to warpRoute for token transfers (skip for native) - if (tokenAddress != address(0)) { - IERC20(tokenAddress).forceApprove(_warpRoute, type(uint256).max); + if (token != address(0)) { + IERC20(token).forceApprove(_warpRoute, type(uint256).max); } } @@ -108,10 +107,10 @@ abstract contract AbstractPredicateWrapper is revert IPredicateWrapper .PredicateRouterWrapper__InsufficientValue(); - if (address(token) == address(0)) return totalNativeRequired; - uint256 totalTokenRequired = Quotes.extract(quotes, address(token)); + if (token == address(0)) return totalNativeRequired; + uint256 totalTokenRequired = Quotes.extract(quotes, token); if (totalTokenRequired > 0) { - token.safeTransferFrom( + IERC20(token).safeTransferFrom( msg.sender, address(this), totalTokenRequired diff --git a/solidity/contracts/token/libs/TokenRouter.sol b/solidity/contracts/token/libs/TokenRouter.sol index a1c8396eb52..05859f8c2eb 100644 --- a/solidity/contracts/token/libs/TokenRouter.sol +++ b/solidity/contracts/token/libs/TokenRouter.sol @@ -98,7 +98,7 @@ abstract contract TokenRouter is GasRouter, ITokenBridge { * @dev This function must be implemented by derived contracts to specify the token address. * @return The address of the token contract. */ - function token() public view virtual returns (address); + function token() public view virtual override returns (address); /** * @inheritdoc ITokenFee diff --git a/solidity/test/token/HypNativeWethWrapper.t.sol b/solidity/test/token/HypNativeWethWrapper.t.sol new file mode 100644 index 00000000000..0276c4b2ab5 --- /dev/null +++ b/solidity/test/token/HypNativeWethWrapper.t.sol @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {MockMailbox} from "../../contracts/mock/MockMailbox.sol"; +import {MockWETH} from "../../contracts/mock/MockWETH.sol"; +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; +import {HypNative} from "../../contracts/token/HypNative.sol"; +import {IWETH} from "../../contracts/token/interfaces/IWETH.sol"; +import {HypNativeWethWrapper} from "../../contracts/token/extensions/HypNativeWethWrapper.sol"; +import {HypNativeWethWrapperFactory} from "../../contracts/token/extensions/HypNativeWethWrapperFactory.sol"; +import {Quote} from "../../contracts/interfaces/ITokenBridge.sol"; +import {Quotes} from "../../contracts/token/libs/Quotes.sol"; + +contract HypNativeWethWrapperTest is Test { + using TypeCasts for address; + using Quotes for Quote[]; + + uint32 internal constant ORIGIN = 11; + uint32 internal constant DESTINATION = 12; + uint256 internal constant TRANSFER_AMT = 1 ether; + address internal constant ALICE = address(0xA11CE); + address internal constant BOB = address(0xB0B); + + MockMailbox internal localMailbox; + MockMailbox internal remoteMailbox; + TestPostDispatchHook internal noopHook; + HypNative internal localRouter; + HypNative internal remoteRouter; + MockWETH internal weth; + HypNativeWethWrapper internal wrapper; + + function setUp() public { + localMailbox = new MockMailbox(ORIGIN); + remoteMailbox = new MockMailbox(DESTINATION); + localMailbox.addRemoteMailbox(DESTINATION, remoteMailbox); + remoteMailbox.addRemoteMailbox(ORIGIN, localMailbox); + + noopHook = new TestPostDispatchHook(); + localMailbox.setDefaultHook(address(noopHook)); + localMailbox.setRequiredHook(address(noopHook)); + remoteMailbox.setDefaultHook(address(noopHook)); + remoteMailbox.setRequiredHook(address(noopHook)); + + localRouter = new HypNative(1, 1, address(localMailbox)); + localRouter.initialize(address(0), address(0), address(this)); + remoteRouter = new HypNative(1, 1, address(remoteMailbox)); + remoteRouter.initialize(address(0), address(0), address(this)); + + localRouter.enrollRemoteRouter( + DESTINATION, + address(remoteRouter).addressToBytes32() + ); + remoteRouter.enrollRemoteRouter( + ORIGIN, + address(localRouter).addressToBytes32() + ); + + weth = new MockWETH(); + wrapper = new HypNativeWethWrapper(IWETH(address(weth)), localRouter); + + vm.label(ALICE, "ALICE"); + vm.label(BOB, "BOB"); + vm.label(address(wrapper), "wrapper"); + vm.label(address(localRouter), "localRouter"); + vm.label(address(remoteRouter), "remoteRouter"); + } + + // ------------------------------------------------------------------------- + // Wrapper: constructor guard + // ------------------------------------------------------------------------- + + function test_constructor_revertsOnNonNativeRouter() public { + vm.expectRevert(); + new HypNativeWethWrapper( + IWETH(address(weth)), + HypNative(payable(address(weth))) + ); + } + + function test_constructor_setsImmutables() public view { + assertEq(wrapper.token(), address(weth)); + } + + // ------------------------------------------------------------------------- + // Wrapper: transferRemote + // ------------------------------------------------------------------------- + + function test_transferRemote_pullsWethAndDispatches() public { + uint256 total = _totalWethRequired(TRANSFER_AMT); + _fundAliceWeth(total); + + vm.prank(ALICE); + wrapper.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + + // Full amount pulled as WETH and unwrapped; wrapper ends with nothing. + assertEq(weth.balanceOf(ALICE), 0); + assertEq(weth.balanceOf(address(wrapper)), 0); + assertEq(address(wrapper).balance, 0); + assertEq(address(localRouter).balance, TRANSFER_AMT); + + // Deliver message and confirm BOB gets native on destination. + uint256 bobBefore = BOB.balance; + vm.deal(address(remoteRouter), TRANSFER_AMT); + remoteMailbox.processNextInboundMessage(); + assertEq(BOB.balance - bobBefore, TRANSFER_AMT); + } + + function test_transferRemote_revertsWithNonZeroMsgValue() public { + uint256 total = _totalWethRequired(TRANSFER_AMT); + _fundAliceWeth(total); + + vm.deal(ALICE, 1 wei); + vm.prank(ALICE); + vm.expectRevert(bytes("Wrapper: msg.value must be 0")); + wrapper.transferRemote{value: 1 wei}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + } + + function test_transferRemote_revertsWithoutApproval() public { + uint256 total = _totalWethRequired(TRANSFER_AMT); + vm.deal(ALICE, total); + vm.prank(ALICE); + weth.deposit{value: total}(); + // No approval. + vm.prank(ALICE); + vm.expectRevert(); + wrapper.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + } + + function test_transferRemote_revertsWithInsufficientWethBalance() public { + uint256 total = _totalWethRequired(TRANSFER_AMT); + vm.prank(ALICE); + weth.approve(address(wrapper), total); + vm.prank(ALICE); + vm.expectRevert(); + wrapper.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + } + + // ------------------------------------------------------------------------- + // Wrapper: quoteTransferRemote + // ------------------------------------------------------------------------- + + function test_quoteTransferRemote_mirrorsHypNativeShapeInWeth() + public + view + { + Quote[] memory nativeQuotes = localRouter.quoteTransferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + Quote[] memory wrapperQuotes = wrapper.quoteTransferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + + // 3-entry shape: [0] gas payment, [1] amount + internal fee, [2] external fee. + assertEq(wrapperQuotes.length, nativeQuotes.length); + for (uint256 i = 0; i < wrapperQuotes.length; i++) { + assertEq(wrapperQuotes[i].token, address(weth)); + assertEq(wrapperQuotes[i].amount, nativeQuotes[i].amount); + } + } + + function test_token_returnsWeth() public view { + assertEq(wrapper.token(), address(weth)); + } + + // ------------------------------------------------------------------------- + // Factory + // ------------------------------------------------------------------------- + + function test_factory_getAddressMatchesDeployedAddress() public { + HypNativeWethWrapperFactory factory = new HypNativeWethWrapperFactory( + IWETH(address(weth)) + ); + + HypNativeWethWrapper predicted = factory.getAddress(localRouter); + assertEq(address(predicted).code.length, 0); + + HypNativeWethWrapper deployed = factory.deploy(localRouter); + assertEq(address(deployed), address(predicted)); + assertGt(address(deployed).code.length, 0); + + assertEq(deployed.token(), address(weth)); + } + + function test_factory_deployIsIdempotent() public { + HypNativeWethWrapperFactory factory = new HypNativeWethWrapperFactory( + IWETH(address(weth)) + ); + + HypNativeWethWrapper first = factory.deploy(localRouter); + HypNativeWethWrapper second = factory.deploy(localRouter); + assertEq(address(first), address(second)); + } + + function test_factory_distinctRoutersYieldDistinctWrappers() public { + HypNativeWethWrapperFactory factory = new HypNativeWethWrapperFactory( + IWETH(address(weth)) + ); + + HypNative secondRouter = new HypNative(1, 1, address(localMailbox)); + secondRouter.initialize(address(0), address(0), address(this)); + + HypNativeWethWrapper a = factory.deploy(localRouter); + HypNativeWethWrapper b = factory.deploy(secondRouter); + assertTrue(address(a) != address(b)); + } + + function test_factory_deployedWrapperBridgesCorrectly() public { + HypNativeWethWrapperFactory factory = new HypNativeWethWrapperFactory( + IWETH(address(weth)) + ); + HypNativeWethWrapper deployed = factory.deploy(localRouter); + + uint256 total = _totalWethRequired(TRANSFER_AMT); + vm.deal(ALICE, total); + vm.prank(ALICE); + weth.deposit{value: total}(); + vm.prank(ALICE); + weth.approve(address(deployed), total); + + vm.prank(ALICE); + deployed.transferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + + assertEq(address(localRouter).balance, TRANSFER_AMT); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + function _totalWethRequired( + uint256 _amount + ) internal view returns (uint256 total) { + Quote[] memory quotes = wrapper.quoteTransferRemote( + DESTINATION, + BOB.addressToBytes32(), + _amount + ); + for (uint256 i = 0; i < quotes.length; i++) { + if (quotes[i].token == address(weth)) { + total += quotes[i].amount; + } + } + } + + function _fundAliceWeth(uint256 _amount) internal { + vm.deal(ALICE, _amount); + vm.prank(ALICE); + weth.deposit{value: _amount}(); + vm.prank(ALICE); + weth.approve(address(wrapper), _amount); + } +} diff --git a/solidity/test/token/HypnativeMovable.t.sol b/solidity/test/token/HypnativeMovable.t.sol index c564df5d764..eae015af252 100644 --- a/solidity/test/token/HypnativeMovable.t.sol +++ b/solidity/test/token/HypnativeMovable.t.sol @@ -30,6 +30,10 @@ contract MockITokenBridgeEth is ITokenBridge { quoteAmount = _amount; } + function token() external pure override returns (address) { + return address(0); + } + function transferRemote( uint32 destinationDomain, bytes32 recipient, diff --git a/solidity/test/token/MovableCollateralRouter.t.sol b/solidity/test/token/MovableCollateralRouter.t.sol index f2b74b013ea..4834727036d 100644 --- a/solidity/test/token/MovableCollateralRouter.t.sol +++ b/solidity/test/token/MovableCollateralRouter.t.sol @@ -35,12 +35,16 @@ contract MockMovableCollateralRouter is MovableCollateralRouter { contract MockITokenBridge is ITokenBridge { using TypeCasts for bytes32; - ERC20Test token; + ERC20Test internal _token; uint256 collateralFee; uint256 nativeFee; - constructor(ERC20Test _token) { - token = _token; + constructor(ERC20Test __token) { + _token = __token; + } + + function token() external view override returns (address) { + return address(_token); } function transferRemote( @@ -49,7 +53,7 @@ contract MockITokenBridge is ITokenBridge { uint256 amountOut ) external payable override returns (bytes32 transferId) { require(msg.value >= nativeFee); - token.transferFrom( + _token.transferFrom( msg.sender, address(this), amountOut + collateralFee @@ -72,7 +76,7 @@ contract MockITokenBridge is ITokenBridge { ) public view override returns (Quote[] memory) { Quote[] memory quotes = new Quote[](2); quotes[0] = Quote(address(0), nativeFee); - quotes[1] = Quote(address(token), amountOut + collateralFee); + quotes[1] = Quote(address(_token), amountOut + collateralFee); return quotes; } }