From 6c63ef9fd2d14eec23de715c5355b6f078622426 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 11 Dec 2025 17:53:11 +0300 Subject: [PATCH 01/15] add converter parameter to TokenWrapper for WETH9-compatible interface support --- script/deploy/facets/DeployTokenWrapper.s.sol | 20 +++++++++- .../tron/deploy-and-register-periphery.ts | 38 ++++++++++++++++++- .../zksync/DeployTokenWrapper.zksync.s.sol | 20 +++++++++- src/Periphery/TokenWrapper.sol | 22 +++++++++-- test/solidity/Periphery/TokenWrapper.t.sol | 6 ++- 5 files changed, 97 insertions(+), 9 deletions(-) diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index d29966559..ef681efe1 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -29,6 +29,19 @@ contract DeployScript is DeployScriptBase { string.concat(".", network, ".wrappedNativeAddress") ); + // try to get converter address, default to address(0) if not found + address converterAddress; + try + vm.parseJsonAddress( + vm.readFile(path), + string.concat(".", network, ".converterAddress") + ) + returns (address addr) { + converterAddress = addr; + } catch { + converterAddress = address(0); + } + // get path of global config file string memory globalConfigPath = string.concat( root, @@ -43,6 +56,11 @@ contract DeployScript is DeployScriptBase { ".refundWallet" ); - return abi.encode(wrappedNativeAddress, refundWalletAddress); + return + abi.encode( + wrappedNativeAddress, + converterAddress, + refundWalletAddress + ); } } diff --git a/script/deploy/tron/deploy-and-register-periphery.ts b/script/deploy/tron/deploy-and-register-periphery.ts index bc781cc30..a790243d3 100755 --- a/script/deploy/tron/deploy-and-register-periphery.ts +++ b/script/deploy/tron/deploy-and-register-periphery.ts @@ -802,11 +802,28 @@ async function deployAndRegisterPeripheryImpl(options: { const refundWalletHex = globalConfig.refundWallet - const constructorArgs = [wrappedNativeHex, refundWalletHex] + // Try to get converter address, default to zero address if not found + const converterHex = tronConfig.converterAddress + ? '0x' + + tronWeb.address.toHex(tronConfig.converterAddress).substring(2) + : '0x0000000000000000000000000000000000000000' + + const constructorArgs = [ + wrappedNativeHex, + converterHex, + refundWalletHex, + ] consola.info( ` Using wrappedNative: ${wrappedNativeBase58} (hex: ${wrappedNativeHex})` ) + consola.info( + `Using converter: ${ + converterHex === '0x0000000000000000000000000000000000000000' + ? 'None (zero address)' + : converterHex + }` + ) consola.info(`Using refundWallet: ${refundWalletHex}`) consola.info(`Version: ${version}`) @@ -871,11 +888,28 @@ async function deployAndRegisterPeripheryImpl(options: { const refundWalletHex = globalConfig.refundWallet - const constructorArgs = [wrappedNativeHex, refundWalletHex] + // Try to get converter address, default to zero address if not found + const converterHex = tronConfig.converterAddress + ? '0x' + + tronWeb.address.toHex(tronConfig.converterAddress).substring(2) + : '0x0000000000000000000000000000000000000000' + + const constructorArgs = [ + wrappedNativeHex, + converterHex, + refundWalletHex, + ] consola.info( ` Using wrappedNative: ${wrappedNativeBase58} (hex: ${wrappedNativeHex})` ) + consola.info( + `Using converter: ${ + converterHex === '0x0000000000000000000000000000000000000000' + ? 'None (zero address)' + : converterHex + }` + ) consola.info(`Using refundWallet: ${refundWalletHex}`) consola.info(`Version: ${version}`) diff --git a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol index d29966559..ef681efe1 100644 --- a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol +++ b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol @@ -29,6 +29,19 @@ contract DeployScript is DeployScriptBase { string.concat(".", network, ".wrappedNativeAddress") ); + // try to get converter address, default to address(0) if not found + address converterAddress; + try + vm.parseJsonAddress( + vm.readFile(path), + string.concat(".", network, ".converterAddress") + ) + returns (address addr) { + converterAddress = addr; + } catch { + converterAddress = address(0); + } + // get path of global config file string memory globalConfigPath = string.concat( root, @@ -43,6 +56,11 @@ contract DeployScript is DeployScriptBase { ".refundWallet" ); - return abi.encode(wrappedNativeAddress, refundWalletAddress); + return + abi.encode( + wrappedNativeAddress, + converterAddress, + refundWalletAddress + ); } } diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 043978067..b5934f9b8 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -18,10 +18,11 @@ interface IWrapper { /// @title TokenWrapper /// @author LI.FI (https://li.fi) /// @notice Provides functionality for wrapping and unwrapping tokens -/// @custom:version 1.1.0 +/// @custom:version 1.2.0 contract TokenWrapper is WithdrawablePeriphery { uint256 private constant MAX_INT = 2 ** 256 - 1; address public wrappedToken; + address public converter; /// Errors /// error WithdrawFailure(); @@ -30,17 +31,21 @@ contract TokenWrapper is WithdrawablePeriphery { // solhint-disable-next-line no-empty-blocks constructor( address _wrappedToken, + address _converter, address _owner ) WithdrawablePeriphery(_owner) { wrappedToken = _wrappedToken; - IERC20(wrappedToken).approve(address(this), MAX_INT); + converter = _converter; } /// External Methods /// /// @notice Wraps the native token function deposit() external payable { - IWrapper(wrappedToken).deposit{ value: msg.value }(); + address wrapperAddress = converter != address(0) + ? converter + : wrappedToken; + IWrapper(wrapperAddress).deposit{ value: msg.value }(); IERC20(wrappedToken).transfer(msg.sender, msg.value); } @@ -52,7 +57,16 @@ contract TokenWrapper is WithdrawablePeriphery { // nearly MAX_UINT256. Using the balance only is a gas optimisation. uint256 wad = IERC20(wrappedToken).balanceOf(msg.sender); IERC20(wrappedToken).transferFrom(msg.sender, address(this), wad); - IWrapper(wrappedToken).withdraw(wad); + address wrapperAddress = converter != address(0) + ? converter + : wrappedToken; + + // If converter is set, approve it to spend wrappedToken + if (converter != address(0)) { + LibAsset.maxApproveERC20(IERC20(wrappedToken), converter, wad); + } + + IWrapper(wrapperAddress).withdraw(wad); SafeTransferLib.safeTransferETH(msg.sender, wad); } diff --git a/test/solidity/Periphery/TokenWrapper.t.sol b/test/solidity/Periphery/TokenWrapper.t.sol index e05cedf8d..6a378a802 100644 --- a/test/solidity/Periphery/TokenWrapper.t.sol +++ b/test/solidity/Periphery/TokenWrapper.t.sol @@ -16,7 +16,11 @@ contract TokenWrapperTest is DSTest { function setUp() public { wrappedToken = new ERC20("TestWrappedToken", "WTST", 18); - tokenWrapper = new TokenWrapper(address(wrappedToken), address(this)); + tokenWrapper = new TokenWrapper( + address(wrappedToken), + address(0), + address(this) + ); vm.deal(address(this), 100 ether); } From 633c97f4f8aaa59f894dbfe2a53ad546ee45a2ff Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 11 Dec 2025 18:19:39 +0300 Subject: [PATCH 02/15] optimize TokenWrapper with immutable variables and balance checks for converter support --- src/Periphery/TokenWrapper.sol | 47 ++++-- test/solidity/Periphery/TokenWrapper.t.sol | 158 ++++++++++++++++++ test/solidity/utils/TestBasicToken.sol | 22 +++ .../utils/TestConverterWithDecimals.sol | 56 +++++++ test/solidity/utils/TestWrappedConverter.sol | 44 +++++ 5 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 test/solidity/utils/TestBasicToken.sol create mode 100644 test/solidity/utils/TestConverterWithDecimals.sol create mode 100644 test/solidity/utils/TestWrappedConverter.sol diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index b5934f9b8..4d34903a9 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -21,8 +21,10 @@ interface IWrapper { /// @custom:version 1.2.0 contract TokenWrapper is WithdrawablePeriphery { uint256 private constant MAX_INT = 2 ** 256 - 1; - address public wrappedToken; - address public converter; + address public immutable wrappedToken; + address public immutable converter; + address private immutable wrapperAddress; + bool private immutable useConverter; /// Errors /// error WithdrawFailure(); @@ -36,17 +38,29 @@ contract TokenWrapper is WithdrawablePeriphery { ) WithdrawablePeriphery(_owner) { wrappedToken = _wrappedToken; converter = _converter; + useConverter = _converter != address(0); + wrapperAddress = useConverter ? _converter : _wrappedToken; + IERC20(wrappedToken).approve(address(this), MAX_INT); } /// External Methods /// /// @notice Wraps the native token function deposit() external payable { - address wrapperAddress = converter != address(0) - ? converter - : wrappedToken; - IWrapper(wrapperAddress).deposit{ value: msg.value }(); - IERC20(wrappedToken).transfer(msg.sender, msg.value); + if (useConverter) { + uint256 balanceBefore = IERC20(wrappedToken).balanceOf( + address(this) + ); + IWrapper(wrapperAddress).deposit{ value: msg.value }(); + uint256 balanceAfter = IERC20(wrappedToken).balanceOf( + address(this) + ); + uint256 amountReceived = balanceAfter - balanceBefore; + IERC20(wrappedToken).transfer(msg.sender, amountReceived); + } else { + IWrapper(wrapperAddress).deposit{ value: msg.value }(); + IERC20(wrappedToken).transfer(msg.sender, msg.value); + } } /// @notice Unwraps all the caller's balance of wrapped token @@ -57,17 +71,20 @@ contract TokenWrapper is WithdrawablePeriphery { // nearly MAX_UINT256. Using the balance only is a gas optimisation. uint256 wad = IERC20(wrappedToken).balanceOf(msg.sender); IERC20(wrappedToken).transferFrom(msg.sender, address(this), wad); - address wrapperAddress = converter != address(0) - ? converter - : wrappedToken; - // If converter is set, approve it to spend wrappedToken - if (converter != address(0)) { + if (useConverter) { + // Approve converter to spend wrappedToken LibAsset.maxApproveERC20(IERC20(wrappedToken), converter, wad); - } - IWrapper(wrapperAddress).withdraw(wad); - SafeTransferLib.safeTransferETH(msg.sender, wad); + uint256 balanceBefore = address(this).balance; + IWrapper(wrapperAddress).withdraw(wad); + uint256 balanceAfter = address(this).balance; + uint256 amountReceived = balanceAfter - balanceBefore; + SafeTransferLib.safeTransferETH(msg.sender, amountReceived); + } else { + IWrapper(wrapperAddress).withdraw(wad); + SafeTransferLib.safeTransferETH(msg.sender, wad); + } } // Needs to be able to receive native on `withdraw` diff --git a/test/solidity/Periphery/TokenWrapper.t.sol b/test/solidity/Periphery/TokenWrapper.t.sol index 6a378a802..0c16be2ca 100644 --- a/test/solidity/Periphery/TokenWrapper.t.sol +++ b/test/solidity/Periphery/TokenWrapper.t.sol @@ -5,6 +5,9 @@ import { DSTest } from "ds-test/test.sol"; import { Vm } from "forge-std/Vm.sol"; import { TokenWrapper } from "lifi/Periphery/TokenWrapper.sol"; import { TestWrappedToken as ERC20 } from "../utils/TestWrappedToken.sol"; +import { TestWrappedConverter } from "../utils/TestWrappedConverter.sol"; +import { TestBasicToken } from "../utils/TestBasicToken.sol"; +import { TestConverterWithDecimals } from "../utils/TestConverterWithDecimals.sol"; contract TokenWrapperTest is DSTest { // solhint-disable immutable-vars-naming @@ -12,6 +15,16 @@ contract TokenWrapperTest is DSTest { TokenWrapper private tokenWrapper; ERC20 private wrappedToken; + // Converter test variables + TokenWrapper private tokenWrapperWithConverter; + TestBasicToken private basicToken; + TestWrappedConverter private converter; + + // Decimal converter test variables + TokenWrapper private tokenWrapperWithDecimalConverter; + TestBasicToken private token6Decimals; + TestConverterWithDecimals private decimalConverter; + error ETHTransferFailed(); function setUp() public { @@ -22,6 +35,34 @@ contract TokenWrapperTest is DSTest { address(this) ); vm.deal(address(this), 100 ether); + + // Setup converter test scenario + basicToken = new TestBasicToken("BasicToken", "BASIC", 18); + converter = new TestWrappedConverter(address(basicToken)); + tokenWrapperWithConverter = new TokenWrapper( + address(basicToken), + address(converter), + address(this) + ); + + // Fund the converter with basic tokens and ETH for testing + basicToken.mint(address(converter), 100 ether); + vm.deal(address(converter), 100 ether); + + // Setup decimal converter test scenario (simulates GasUSDT0Converter) + token6Decimals = new TestBasicToken("USDT", "USDT", 6); + decimalConverter = new TestConverterWithDecimals( + address(token6Decimals) + ); + tokenWrapperWithDecimalConverter = new TokenWrapper( + address(token6Decimals), + address(decimalConverter), + address(this) + ); + + // Fund the decimal converter with 6-decimal tokens and ETH + token6Decimals.mint(address(decimalConverter), 100_000_000); // 100 USDT (6 decimals) + vm.deal(address(decimalConverter), 100 ether); } // Needed to receive ETH @@ -55,4 +96,121 @@ contract TokenWrapperTest is DSTest { tokenWrapper.withdraw(); assert(address(this).balance - initialBalance == 1 ether); } + + // ========== CONVERTER TESTS ========== + + function testCanDepositWithConverter() public { + // Initial state: user has no basic tokens + assertEq(basicToken.balanceOf(address(this)), 0); + + // User deposits ETH through TokenWrapper + tokenWrapperWithConverter.deposit{ value: 1 ether }(); + + // User should now have basic tokens + assertEq(basicToken.balanceOf(address(this)), 1 ether); + } + + function testCanWithdrawWithConverter() public { + uint256 initialBalance = address(this).balance; + + // Give user some basic tokens + basicToken.mint(address(this), 1 ether); + + // Approve TokenWrapper to spend basic tokens + basicToken.approve(address(tokenWrapperWithConverter), 1 ether); + + // User withdraws through TokenWrapper + tokenWrapperWithConverter.withdraw(); + + // User should have received ETH + assertEq(address(this).balance - initialBalance, 1 ether); + + // User should have no basic tokens left + assertEq(basicToken.balanceOf(address(this)), 0); + } + + function testConverterReceivesApproval() public { + // Give user some basic tokens + basicToken.mint(address(this), 1 ether); + + // Approve TokenWrapper + basicToken.approve(address(tokenWrapperWithConverter), 1 ether); + + // Check that converter has no allowance before withdraw + assertEq( + basicToken.allowance( + address(tokenWrapperWithConverter), + address(converter) + ), + 0 + ); + + // Withdraw + tokenWrapperWithConverter.withdraw(); + + // After withdraw, the approval should have been set + // (TokenWrapper approves converter to pull tokens) + assertEq( + basicToken.allowance( + address(tokenWrapperWithConverter), + address(converter) + ), + type(uint256).max + ); + } + + function testCanDepositWithDecimalConverter() public { + // Initial state: user has no 6-decimal tokens + assertEq(token6Decimals.balanceOf(address(this)), 0); + + // User deposits 1 ETH (18 decimals) + // Should receive 1 USDT (6 decimals) after conversion + tokenWrapperWithDecimalConverter.deposit{ value: 1 ether }(); + + // User should now have 1 USDT (1e6, not 1e18) + assertEq(token6Decimals.balanceOf(address(this)), 1_000_000); + } + + function testCanWithdrawWithDecimalConverter() public { + uint256 initialBalance = address(this).balance; + + // Give user 1 USDT (6 decimals) + token6Decimals.mint(address(this), 1_000_000); + + // Approve TokenWrapper to spend tokens + token6Decimals.approve( + address(tokenWrapperWithDecimalConverter), + 1_000_000 + ); + + // User withdraws 1 USDT (6 decimals) + tokenWrapperWithDecimalConverter.withdraw(); + + // User should have received 1 ETH (18 decimals) after conversion + assertEq(address(this).balance - initialBalance, 1 ether); + + // User should have no tokens left + assertEq(token6Decimals.balanceOf(address(this)), 0); + } + + function testDecimalConverterRoundTrip() public { + // Start with 5 ETH + uint256 depositAmount = 5 ether; + + // Deposit 5 ETH, should receive 5 USDT (6 decimals) + tokenWrapperWithDecimalConverter.deposit{ value: depositAmount }(); + assertEq(token6Decimals.balanceOf(address(this)), 5_000_000); + + // Approve and withdraw all USDT + token6Decimals.approve( + address(tokenWrapperWithDecimalConverter), + 5_000_000 + ); + + uint256 balanceBefore = address(this).balance; + tokenWrapperWithDecimalConverter.withdraw(); + + // Should receive back 5 ETH + assertEq(address(this).balance - balanceBefore, depositAmount); + } } diff --git a/test/solidity/utils/TestBasicToken.sol b/test/solidity/utils/TestBasicToken.sol new file mode 100644 index 000000000..ebade68f1 --- /dev/null +++ b/test/solidity/utils/TestBasicToken.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import { ERC20 } from "solmate/tokens/ERC20.sol"; + +/// @notice Basic ERC20 token without deposit/withdraw functions +/// @dev Used to test converter functionality +contract TestBasicToken is ERC20 { + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals + ) ERC20(_name, _symbol, _decimals) {} + + function mint(address _to, uint256 _amount) public { + _mint(_to, _amount); + } + + function burn(address _from, uint256 _amount) public { + _burn(_from, _amount); + } +} diff --git a/test/solidity/utils/TestConverterWithDecimals.sol b/test/solidity/utils/TestConverterWithDecimals.sol new file mode 100644 index 000000000..82c24f3e6 --- /dev/null +++ b/test/solidity/utils/TestConverterWithDecimals.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice Test converter that simulates decimal conversion (like GasUSDT0Converter) +/// @dev Converts 18 decimal native token to 6 decimal wrapped token (1e12 conversion) +contract TestConverterWithDecimals { + IERC20 public immutable wrappedToken; + + error WithdrawError(); + error InvalidAmount(); + + constructor(address _wrappedToken) { + wrappedToken = IERC20(_wrappedToken); + } + + function convertToWrapped(uint256 amount) internal pure returns (uint256) { + return amount / 1e12; + } + + function convertToNative(uint256 amount) internal pure returns (uint256) { + return amount * 1e12; + } + + /// @notice Accepts native token and sends wrapped tokens to caller + /// @dev Simulates 1e18 -> 1e6 conversion + function deposit() public payable { + require(msg.value % 1e12 == 0, "Invalid amount"); + uint256 amountOut = convertToWrapped(msg.value); + require( + wrappedToken.transfer(msg.sender, amountOut), + "Transfer failed" + ); + } + + /// @notice Pulls wrapped tokens from caller and sends native tokens back + /// @dev Simulates 1e6 -> 1e18 conversion + function withdraw(uint256 amount) public { + // Pull wrapped tokens from caller + require( + wrappedToken.transferFrom(msg.sender, address(this), amount), + "TransferFrom failed" + ); + + // Send native tokens to caller (with decimal conversion) + uint256 amountOut = convertToNative(amount); + (bool success, ) = payable(msg.sender).call{ value: amountOut }(""); + if (!success) { + revert WithdrawError(); + } + } + + // Needs to receive ETH for testing + receive() external payable {} +} diff --git a/test/solidity/utils/TestWrappedConverter.sol b/test/solidity/utils/TestWrappedConverter.sol new file mode 100644 index 000000000..c55bf83c4 --- /dev/null +++ b/test/solidity/utils/TestWrappedConverter.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.17; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/// @notice Test converter contract that implements WETH9 interface +/// @dev Simulates a converter that wraps/unwraps native ETH to/from an ERC20 token +contract TestWrappedConverter { + IERC20 public immutable wrappedToken; + + error WithdrawError(); + error InsufficientBalance(uint256 available, uint256 required); + + constructor(address _wrappedToken) { + wrappedToken = IERC20(_wrappedToken); + } + + /// @notice Accepts ETH and sends wrapped tokens to the caller + function deposit() public payable { + // Transfer wrapped tokens to caller equal to msg.value + require( + wrappedToken.transfer(msg.sender, msg.value), + "Transfer failed" + ); + } + + /// @notice Pulls wrapped tokens from caller and sends ETH back + function withdraw(uint256 wad) public { + // Pull wrapped tokens from caller + require( + wrappedToken.transferFrom(msg.sender, address(this), wad), + "TransferFrom failed" + ); + + // Send ETH to caller + (bool success, ) = payable(msg.sender).call{ value: wad }(""); + if (!success) { + revert WithdrawError(); + } + } + + // Needs to receive ETH for testing + receive() external payable {} +} From 0379dff171b8db3e866ecbc77ca36e076f04d12e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 12 Dec 2025 13:19:10 +0300 Subject: [PATCH 03/15] remove useless approval --- src/Periphery/TokenWrapper.sol | 43 +++++++++++++++++----------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 4d34903a9..02cac34d3 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -21,10 +21,10 @@ interface IWrapper { /// @custom:version 1.2.0 contract TokenWrapper is WithdrawablePeriphery { uint256 private constant MAX_INT = 2 ** 256 - 1; - address public immutable wrappedToken; - address public immutable converter; - address private immutable wrapperAddress; - bool private immutable useConverter; + address public immutable WRAPPED_TOKEN; + address public immutable CONVERTER; + address private immutable WRAPPER_ADDRESS; + bool private immutable USE_CONVERTER; /// Errors /// error WithdrawFailure(); @@ -36,30 +36,29 @@ contract TokenWrapper is WithdrawablePeriphery { address _converter, address _owner ) WithdrawablePeriphery(_owner) { - wrappedToken = _wrappedToken; - converter = _converter; - useConverter = _converter != address(0); - wrapperAddress = useConverter ? _converter : _wrappedToken; - IERC20(wrappedToken).approve(address(this), MAX_INT); + WRAPPED_TOKEN = _wrappedToken; + CONVERTER = _converter; + USE_CONVERTER = _converter != address(0); + WRAPPER_ADDRESS = USE_CONVERTER ? _converter : _wrappedToken; } /// External Methods /// /// @notice Wraps the native token function deposit() external payable { - if (useConverter) { - uint256 balanceBefore = IERC20(wrappedToken).balanceOf( + if (USE_CONVERTER) { + uint256 balanceBefore = IERC20(WRAPPED_TOKEN).balanceOf( address(this) ); - IWrapper(wrapperAddress).deposit{ value: msg.value }(); - uint256 balanceAfter = IERC20(wrappedToken).balanceOf( + IWrapper(WRAPPER_ADDRESS).deposit{ value: msg.value }(); + uint256 balanceAfter = IERC20(WRAPPED_TOKEN).balanceOf( address(this) ); uint256 amountReceived = balanceAfter - balanceBefore; - IERC20(wrappedToken).transfer(msg.sender, amountReceived); + IERC20(WRAPPED_TOKEN).transfer(msg.sender, amountReceived); } else { - IWrapper(wrapperAddress).deposit{ value: msg.value }(); - IERC20(wrappedToken).transfer(msg.sender, msg.value); + IWrapper(WRAPPER_ADDRESS).deposit{ value: msg.value }(); + IERC20(WRAPPED_TOKEN).transfer(msg.sender, msg.value); } } @@ -69,20 +68,20 @@ contract TokenWrapper is WithdrawablePeriphery { // to have `wad` equal to the minimum between the balance and the // given allowance, in our specific usecase allowance is always // nearly MAX_UINT256. Using the balance only is a gas optimisation. - uint256 wad = IERC20(wrappedToken).balanceOf(msg.sender); - IERC20(wrappedToken).transferFrom(msg.sender, address(this), wad); + uint256 wad = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender); + IERC20(WRAPPED_TOKEN).transferFrom(msg.sender, address(this), wad); - if (useConverter) { + if (USE_CONVERTER) { // Approve converter to spend wrappedToken - LibAsset.maxApproveERC20(IERC20(wrappedToken), converter, wad); + LibAsset.maxApproveERC20(IERC20(WRAPPED_TOKEN), CONVERTER, wad); uint256 balanceBefore = address(this).balance; - IWrapper(wrapperAddress).withdraw(wad); + IWrapper(WRAPPER_ADDRESS).withdraw(wad); uint256 balanceAfter = address(this).balance; uint256 amountReceived = balanceAfter - balanceBefore; SafeTransferLib.safeTransferETH(msg.sender, amountReceived); } else { - IWrapper(wrapperAddress).withdraw(wad); + IWrapper(WRAPPER_ADDRESS).withdraw(wad); SafeTransferLib.safeTransferETH(msg.sender, wad); } } From ecb8b1ce5c494502b96e71a901fcbfce67e7c8b2 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Fri, 12 Dec 2025 13:33:35 +0300 Subject: [PATCH 04/15] safe transfer --- archive/config/tokenwrapper.json | 225 ++++++++++++++---- .../deploy/resources/deployRequirements.json | 19 ++ src/Periphery/TokenWrapper.sol | 8 +- 3 files changed, 205 insertions(+), 47 deletions(-) diff --git a/archive/config/tokenwrapper.json b/archive/config/tokenwrapper.json index 45c18592d..fb0d7d09e 100644 --- a/archive/config/tokenwrapper.json +++ b/archive/config/tokenwrapper.json @@ -1,48 +1,183 @@ { "comment": "Note: this file is generated by a backend script, do not change it manually!", - "mainnet": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "abstract": "0x3439153EB7AF838Ad19d56E1571FBD09333C2809", - "berachain": "0x6969696969696969696969696969696969696969", - "optimism": "0x4200000000000000000000000000000000000006", - "cronos": "0x5c7f8a570d578ed84e63fdfa7b1ee72deae1ae23", - "rootstock": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", - "bsc": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", - "gnosis": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", - "velas": "0xc579d1f3cf86749e05cd06f7ade17856c2ce3126", - "fuse": "0x0be9e53fd7edac9f859882afdda116645287c629", - "polygon": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", - "xlayer": "0xe538905cf8410324e03a5a23c1c177a474d59b2b", - "fantom": "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", - "fraxtal": "0xfc00000000000000000000000000000000000006", - "boba": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000", - "zksync": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91", - "metis": "0x75cb093E4D61d2A2e65D8e0BBb01DE8d89b53481", - "polygonzkevm": "0x4f9a0e7fd2bf6067db6994cf12e4495df938e6e9", - "moonbeam": "0xacc15dc74880c9944775448304b263d191c6077f", - "moonriver": "0x98878b06940ae243284ca214f92bb71a2b032b8a", - "sei": "0xE30feDd158A2e3b13e9badaeABaFc5516e95e8C7", - "gravity": "0xBB859E225ac8Fb6BE1C7e38D87b767e95Fef0EbD", - "mantle": "0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8", - "kaia": "0x19aac5f612f524b754ca7e7c41cbfa2e981a4432", - "base": "0x4200000000000000000000000000000000000006", - "evmos": "0xd4949664cd82660aae99bedc034a0dea8a0bd517", - "immutablezkevm": "0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d", - "ink": "0x4200000000000000000000000000000000000006", - "mode": "0x4200000000000000000000000000000000000006", - "arbitrum": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", - "celo": "0x471ece3750da237f93b8e339c536989b8978a438", - "avalanche": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", - "lens": "0x6bDc36E20D267Ff0dd6097799f82e78907105e2F", - "linea": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", - "lisk": "0x4200000000000000000000000000000000000006", - "blast": "0x4300000000000000000000000000000000000004", - "worldchain": "0x4200000000000000000000000000000000000006", - "unichain": "0x4200000000000000000000000000000000000006", - "taiko": "0xA51894664A773981C6C112C43ce576f315d5b1B6", - "scroll": "0x5300000000000000000000000000000000000004", - "soneium": "0x4200000000000000000000000000000000000006", - "sonic": "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38", - "swellchain": "0x4200000000000000000000000000000000000006", - "aurora": "0xc9bdeed33cd01541e1eed10f90519d2c06fe3feb", - "harmony": "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a" + "mainnet": { + "wrappedToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "converter": "0x0000000000000000000000000000000000000000" + }, + "abstract": { + "wrappedToken": "0x3439153EB7AF838Ad19d56E1571FBD09333C2809", + "converter": "0x0000000000000000000000000000000000000000" + }, + "berachain": { + "wrappedToken": "0x6969696969696969696969696969696969696969", + "converter": "0x0000000000000000000000000000000000000000" + }, + "optimism": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "cronos": { + "wrappedToken": "0x5c7f8a570d578ed84e63fdfa7b1ee72deae1ae23", + "converter": "0x0000000000000000000000000000000000000000" + }, + "rootstock": { + "wrappedToken": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", + "converter": "0x0000000000000000000000000000000000000000" + }, + "bsc": { + "wrappedToken": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "converter": "0x0000000000000000000000000000000000000000" + }, + "gnosis": { + "wrappedToken": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + "converter": "0x0000000000000000000000000000000000000000" + }, + "velas": { + "wrappedToken": "0xc579d1f3cf86749e05cd06f7ade17856c2ce3126", + "converter": "0x0000000000000000000000000000000000000000" + }, + "fuse": { + "wrappedToken": "0x0be9e53fd7edac9f859882afdda116645287c629", + "converter": "0x0000000000000000000000000000000000000000" + }, + "polygon": { + "wrappedToken": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", + "converter": "0x0000000000000000000000000000000000000000" + }, + "xlayer": { + "wrappedToken": "0xe538905cf8410324e03a5a23c1c177a474d59b2b", + "converter": "0x0000000000000000000000000000000000000000" + }, + "fantom": { + "wrappedToken": "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", + "converter": "0x0000000000000000000000000000000000000000" + }, + "fraxtal": { + "wrappedToken": "0xfc00000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "boba": { + "wrappedToken": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000", + "converter": "0x0000000000000000000000000000000000000000" + }, + "zksync": { + "wrappedToken": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91", + "converter": "0x0000000000000000000000000000000000000000" + }, + "metis": { + "wrappedToken": "0x75cb093E4D61d2A2e65D8e0BBb01DE8d89b53481", + "converter": "0x0000000000000000000000000000000000000000" + }, + "polygonzkevm": { + "wrappedToken": "0x4f9a0e7fd2bf6067db6994cf12e4495df938e6e9", + "converter": "0x0000000000000000000000000000000000000000" + }, + "moonbeam": { + "wrappedToken": "0xacc15dc74880c9944775448304b263d191c6077f", + "converter": "0x0000000000000000000000000000000000000000" + }, + "moonriver": { + "wrappedToken": "0x98878b06940ae243284ca214f92bb71a2b032b8a", + "converter": "0x0000000000000000000000000000000000000000" + }, + "sei": { + "wrappedToken": "0xE30feDd158A2e3b13e9badaeABaFc5516e95e8C7", + "converter": "0x0000000000000000000000000000000000000000" + }, + "gravity": { + "wrappedToken": "0xBB859E225ac8Fb6BE1C7e38D87b767e95Fef0EbD", + "converter": "0x0000000000000000000000000000000000000000" + }, + "mantle": { + "wrappedToken": "0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8", + "converter": "0x0000000000000000000000000000000000000000" + }, + "kaia": { + "wrappedToken": "0x19aac5f612f524b754ca7e7c41cbfa2e981a4432", + "converter": "0x0000000000000000000000000000000000000000" + }, + "base": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "evmos": { + "wrappedToken": "0xd4949664cd82660aae99bedc034a0dea8a0bd517", + "converter": "0x0000000000000000000000000000000000000000" + }, + "immutablezkevm": { + "wrappedToken": "0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d", + "converter": "0x0000000000000000000000000000000000000000" + }, + "ink": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "mode": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "arbitrum": { + "wrappedToken": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "converter": "0x0000000000000000000000000000000000000000" + }, + "celo": { + "wrappedToken": "0x471ece3750da237f93b8e339c536989b8978a438", + "converter": "0x0000000000000000000000000000000000000000" + }, + "avalanche": { + "wrappedToken": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", + "converter": "0x0000000000000000000000000000000000000000" + }, + "lens": { + "wrappedToken": "0x6bDc36E20D267Ff0dd6097799f82e78907105e2F", + "converter": "0x0000000000000000000000000000000000000000" + }, + "linea": { + "wrappedToken": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", + "converter": "0x0000000000000000000000000000000000000000" + }, + "lisk": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "blast": { + "wrappedToken": "0x4300000000000000000000000000000000000004", + "converter": "0x0000000000000000000000000000000000000000" + }, + "worldchain": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "unichain": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "taiko": { + "wrappedToken": "0xA51894664A773981C6C112C43ce576f315d5b1B6", + "converter": "0x0000000000000000000000000000000000000000" + }, + "scroll": { + "wrappedToken": "0x5300000000000000000000000000000000000004", + "converter": "0x0000000000000000000000000000000000000000" + }, + "soneium": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "sonic": { + "wrappedToken": "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38", + "converter": "0x0000000000000000000000000000000000000000" + }, + "swellchain": { + "wrappedToken": "0x4200000000000000000000000000000000000006", + "converter": "0x0000000000000000000000000000000000000000" + }, + "aurora": { + "wrappedToken": "0xc9bdeed33cd01541e1eed10f90519d2c06fe3feb", + "converter": "0x0000000000000000000000000000000000000000" + }, + "harmony": { + "wrappedToken": "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", + "converter": "0x0000000000000000000000000000000000000000" + } } diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index bab4f4d90..adcb9ad44 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -695,5 +695,24 @@ "allowToDeployWithZeroAddress": "false" } } + }, + "TokenWrapper": { + "configData": { + "_wrappedToken": { + "configFileName": "tokenwrapper.json", + "keyInConfigFile": "..wrappedToken", + "allowToDeployWithZeroAddress": "true" + }, + "_converter": { + "configFileName": "tokenwrapper.json", + "keyInConfigFile": "..converter", + "allowToDeployWithZeroAddress": "true" + }, + "_owner": { + "configFileName": "global.json", + "keyInConfigFile": ".withdrawWallet", + "allowToDeployWithZeroAddress": "true" + } + } } } diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 02cac34d3..eac0eaf40 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -55,10 +55,14 @@ contract TokenWrapper is WithdrawablePeriphery { address(this) ); uint256 amountReceived = balanceAfter - balanceBefore; - IERC20(WRAPPED_TOKEN).transfer(msg.sender, amountReceived); + SafeTransferLib.safeTransfer( + WRAPPED_TOKEN, + msg.sender, + amountReceived + ); } else { IWrapper(WRAPPER_ADDRESS).deposit{ value: msg.value }(); - IERC20(WRAPPED_TOKEN).transfer(msg.sender, msg.value); + SafeTransferLib.safeTransfer(WRAPPED_TOKEN, msg.sender, msg.value); } } From 5a6ab81f35292c40134d7fb08a7fe6f9363d5418 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 15 Dec 2025 12:16:28 +0300 Subject: [PATCH 05/15] Update src/Periphery/TokenWrapper.sol Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/Periphery/TokenWrapper.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index eac0eaf40..7de797a78 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -73,7 +73,7 @@ contract TokenWrapper is WithdrawablePeriphery { // given allowance, in our specific usecase allowance is always // nearly MAX_UINT256. Using the balance only is a gas optimisation. uint256 wad = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender); - IERC20(WRAPPED_TOKEN).transferFrom(msg.sender, address(this), wad); + SafeTransferLib.safeTransferFrom(WRAPPED_TOKEN, msg.sender, address(this), wad); if (USE_CONVERTER) { // Approve converter to spend wrappedToken From 3ffe919e32126a6db3b7a2796b3f9d6972bba019 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 15 Dec 2025 13:31:08 +0300 Subject: [PATCH 06/15] fix --- script/deploy/facets/DeployTokenWrapper.s.sol | 3 +- .../zksync/DeployTokenWrapper.zksync.s.sol | 3 +- src/Interfaces/IWrapper.sol | 14 ++++++++ src/Periphery/TokenWrapper.sol | 34 ++++++++++++------- test/solidity/Periphery/TokenWrapper.t.sol | 21 ++++++------ test/solidity/utils/TestBasicToken.sol | 3 +- .../utils/TestConverterWithDecimals.sol | 23 ++++++------- test/solidity/utils/TestWrappedConverter.sol | 21 ++++++------ 8 files changed, 73 insertions(+), 49 deletions(-) create mode 100644 src/Interfaces/IWrapper.sol diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index ef681efe1..2daf86a1a 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; diff --git a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol index ef681efe1..2daf86a1a 100644 --- a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol +++ b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; diff --git a/src/Interfaces/IWrapper.sol b/src/Interfaces/IWrapper.sol new file mode 100644 index 000000000..3963669b0 --- /dev/null +++ b/src/Interfaces/IWrapper.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: LGPL-3.0-only + +pragma solidity ^0.8.17; + +/// @title IWrapper +/// @notice Interface for token wrapper contracts +/// @author LI.FI (https://li.fi) +/// @custom:version 1.0.0 +interface IWrapper { + function deposit() external payable; + + // solhint-disable-next-line explicit-types + function withdraw(uint wad) external; +} diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 7de797a78..580569195 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; // solhint-disable-next-line no-unused-import @@ -6,14 +7,7 @@ import { LibAsset } from "../Libraries/LibAsset.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; - -/// External wrapper interface -interface IWrapper { - function deposit() external payable; - - // solhint-disable-next-line explicit-types - function withdraw(uint wad) external; -} +import { IWrapper } from "../Interfaces/IWrapper.sol"; /// @title TokenWrapper /// @author LI.FI (https://li.fi) @@ -29,7 +23,11 @@ contract TokenWrapper is WithdrawablePeriphery { /// Errors /// error WithdrawFailure(); - /// Constructor /// + /// @notice Creates a new TokenWrapper contract + /// @param _wrappedToken Address of the wrapped token (e.g., WETH, or token returned by converter) + /// @param _converter Address of converter contract, or address(0) if wrapping 1:1 without conversion + /// @param _owner Address that will own this contract and can withdraw stuck tokens + /// @dev If converter is provided, all wrap/unwrap operations go through it for decimal or other conversions // solhint-disable-next-line no-empty-blocks constructor( address _wrappedToken, @@ -44,7 +42,9 @@ contract TokenWrapper is WithdrawablePeriphery { /// External Methods /// - /// @notice Wraps the native token + /// @notice Wraps the native token and transfers wrapped tokens to caller + /// @dev If converter is set, uses it to convert native to wrapped tokens and measures actual amount received + /// @dev If no converter, wraps native 1:1 and transfers msg.value of wrapped tokens function deposit() external payable { if (USE_CONVERTER) { uint256 balanceBefore = IERC20(WRAPPED_TOKEN).balanceOf( @@ -66,14 +66,22 @@ contract TokenWrapper is WithdrawablePeriphery { } } - /// @notice Unwraps all the caller's balance of wrapped token + /// @notice Unwraps all the caller's balance of wrapped token and returns native tokens + /// @dev Pulls wrapped tokens from msg.sender based on their balance (requires prior approval) + /// @dev If converter is set, approves converter and measures actual native amount received + /// @dev If no converter, unwraps 1:1 and transfers exact wad amount of native tokens function withdraw() external { // While in a general purpose contract it would make sense // to have `wad` equal to the minimum between the balance and the // given allowance, in our specific usecase allowance is always // nearly MAX_UINT256. Using the balance only is a gas optimisation. uint256 wad = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender); - SafeTransferLib.safeTransferFrom(WRAPPED_TOKEN, msg.sender, address(this), wad); + SafeTransferLib.safeTransferFrom( + WRAPPED_TOKEN, + msg.sender, + address(this), + wad + ); if (USE_CONVERTER) { // Approve converter to spend wrappedToken diff --git a/test/solidity/Periphery/TokenWrapper.t.sol b/test/solidity/Periphery/TokenWrapper.t.sol index 0c16be2ca..532014017 100644 --- a/test/solidity/Periphery/TokenWrapper.t.sol +++ b/test/solidity/Periphery/TokenWrapper.t.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { DSTest } from "ds-test/test.sol"; @@ -68,13 +69,13 @@ contract TokenWrapperTest is DSTest { // Needed to receive ETH receive() external payable {} - function testCanDeposit() public { + function test_CanDeposit() public { assert(wrappedToken.balanceOf(address(this)) == 0); tokenWrapper.deposit{ value: 1 ether }(); assert(wrappedToken.balanceOf(address(this)) == 1 ether); } - function testCanWithdrawToken() public { + function test_CanWithdrawToken() public { // Send some ETH to the contract (bool success, ) = address(tokenWrapper).call{ value: 1 ether }(""); if (!success) revert ETHTransferFailed(); @@ -88,7 +89,7 @@ contract TokenWrapperTest is DSTest { assertEq(address(this).balance - initialBalance, 1 ether); } - function testCanWithdraw() public { + function test_CanWithdraw() public { uint256 initialBalance = address(this).balance; vm.deal(address(wrappedToken), 100 ether); wrappedToken.mint(address(this), 1 ether); @@ -99,7 +100,7 @@ contract TokenWrapperTest is DSTest { // ========== CONVERTER TESTS ========== - function testCanDepositWithConverter() public { + function test_CanDepositWithConverter() public { // Initial state: user has no basic tokens assertEq(basicToken.balanceOf(address(this)), 0); @@ -110,7 +111,7 @@ contract TokenWrapperTest is DSTest { assertEq(basicToken.balanceOf(address(this)), 1 ether); } - function testCanWithdrawWithConverter() public { + function test_CanWithdrawWithConverter() public { uint256 initialBalance = address(this).balance; // Give user some basic tokens @@ -129,7 +130,7 @@ contract TokenWrapperTest is DSTest { assertEq(basicToken.balanceOf(address(this)), 0); } - function testConverterReceivesApproval() public { + function test_ConverterReceivesApproval() public { // Give user some basic tokens basicToken.mint(address(this), 1 ether); @@ -159,7 +160,7 @@ contract TokenWrapperTest is DSTest { ); } - function testCanDepositWithDecimalConverter() public { + function test_CanDepositWithDecimalConverter() public { // Initial state: user has no 6-decimal tokens assertEq(token6Decimals.balanceOf(address(this)), 0); @@ -171,7 +172,7 @@ contract TokenWrapperTest is DSTest { assertEq(token6Decimals.balanceOf(address(this)), 1_000_000); } - function testCanWithdrawWithDecimalConverter() public { + function test_CanWithdrawWithDecimalConverter() public { uint256 initialBalance = address(this).balance; // Give user 1 USDT (6 decimals) @@ -193,7 +194,7 @@ contract TokenWrapperTest is DSTest { assertEq(token6Decimals.balanceOf(address(this)), 0); } - function testDecimalConverterRoundTrip() public { + function test_DecimalConverterRoundTrip() public { // Start with 5 ETH uint256 depositAmount = 5 ether; diff --git a/test/solidity/utils/TestBasicToken.sol b/test/solidity/utils/TestBasicToken.sol index ebade68f1..7bf303f21 100644 --- a/test/solidity/utils/TestBasicToken.sol +++ b/test/solidity/utils/TestBasicToken.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { ERC20 } from "solmate/tokens/ERC20.sol"; diff --git a/test/solidity/utils/TestConverterWithDecimals.sol b/test/solidity/utils/TestConverterWithDecimals.sol index 82c24f3e6..20cb40d86 100644 --- a/test/solidity/utils/TestConverterWithDecimals.sol +++ b/test/solidity/utils/TestConverterWithDecimals.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -6,13 +7,15 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Test converter that simulates decimal conversion (like GasUSDT0Converter) /// @dev Converts 18 decimal native token to 6 decimal wrapped token (1e12 conversion) contract TestConverterWithDecimals { - IERC20 public immutable wrappedToken; + IERC20 public immutable WRAPPED_TOKEN; error WithdrawError(); error InvalidAmount(); + error TransferFailed(); + error TransferFromFailed(); constructor(address _wrappedToken) { - wrappedToken = IERC20(_wrappedToken); + WRAPPED_TOKEN = IERC20(_wrappedToken); } function convertToWrapped(uint256 amount) internal pure returns (uint256) { @@ -26,22 +29,18 @@ contract TestConverterWithDecimals { /// @notice Accepts native token and sends wrapped tokens to caller /// @dev Simulates 1e18 -> 1e6 conversion function deposit() public payable { - require(msg.value % 1e12 == 0, "Invalid amount"); + if (msg.value % 1e12 != 0) revert InvalidAmount(); uint256 amountOut = convertToWrapped(msg.value); - require( - wrappedToken.transfer(msg.sender, amountOut), - "Transfer failed" - ); + if (!WRAPPED_TOKEN.transfer(msg.sender, amountOut)) + revert TransferFailed(); } /// @notice Pulls wrapped tokens from caller and sends native tokens back /// @dev Simulates 1e6 -> 1e18 conversion function withdraw(uint256 amount) public { // Pull wrapped tokens from caller - require( - wrappedToken.transferFrom(msg.sender, address(this), amount), - "TransferFrom failed" - ); + if (!WRAPPED_TOKEN.transferFrom(msg.sender, address(this), amount)) + revert TransferFromFailed(); // Send native tokens to caller (with decimal conversion) uint256 amountOut = convertToNative(amount); diff --git a/test/solidity/utils/TestWrappedConverter.sol b/test/solidity/utils/TestWrappedConverter.sol index c55bf83c4..3def1a36a 100644 --- a/test/solidity/utils/TestWrappedConverter.sol +++ b/test/solidity/utils/TestWrappedConverter.sol @@ -1,4 +1,5 @@ -// SPDX-License-Identifier: Unlicense +// SPDX-License-Identifier: LGPL-3.0-only + pragma solidity ^0.8.17; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; @@ -6,31 +7,29 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /// @notice Test converter contract that implements WETH9 interface /// @dev Simulates a converter that wraps/unwraps native ETH to/from an ERC20 token contract TestWrappedConverter { - IERC20 public immutable wrappedToken; + IERC20 public immutable WRAPPED_TOKEN; error WithdrawError(); error InsufficientBalance(uint256 available, uint256 required); + error TransferFailed(); + error TransferFromFailed(); constructor(address _wrappedToken) { - wrappedToken = IERC20(_wrappedToken); + WRAPPED_TOKEN = IERC20(_wrappedToken); } /// @notice Accepts ETH and sends wrapped tokens to the caller function deposit() public payable { // Transfer wrapped tokens to caller equal to msg.value - require( - wrappedToken.transfer(msg.sender, msg.value), - "Transfer failed" - ); + if (!WRAPPED_TOKEN.transfer(msg.sender, msg.value)) + revert TransferFailed(); } /// @notice Pulls wrapped tokens from caller and sends ETH back function withdraw(uint256 wad) public { // Pull wrapped tokens from caller - require( - wrappedToken.transferFrom(msg.sender, address(this), wad), - "TransferFrom failed" - ); + if (!WRAPPED_TOKEN.transferFrom(msg.sender, address(this), wad)) + revert TransferFromFailed(); // Send ETH to caller (bool success, ) = payable(msg.sender).call{ value: wad }(""); From dcb8a6d11c9c70c110283f17fc4d126ef433fd64 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 16 Dec 2025 12:41:13 +0300 Subject: [PATCH 07/15] fixes --- archive/config/tokenwrapper.json | 183 ------------------ config/networks.json | 20 ++ script/deploy/facets/DeployTokenWrapper.s.sol | 9 +- .../deploy/resources/deployRequirements.json | 15 +- .../zksync/DeployTokenWrapper.zksync.s.sol | 5 +- src/Periphery/TokenWrapper.sol | 55 ++++-- test/solidity/Periphery/TokenWrapper.t.sol | 34 +--- 7 files changed, 84 insertions(+), 237 deletions(-) delete mode 100644 archive/config/tokenwrapper.json diff --git a/archive/config/tokenwrapper.json b/archive/config/tokenwrapper.json deleted file mode 100644 index fb0d7d09e..000000000 --- a/archive/config/tokenwrapper.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "comment": "Note: this file is generated by a backend script, do not change it manually!", - "mainnet": { - "wrappedToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "converter": "0x0000000000000000000000000000000000000000" - }, - "abstract": { - "wrappedToken": "0x3439153EB7AF838Ad19d56E1571FBD09333C2809", - "converter": "0x0000000000000000000000000000000000000000" - }, - "berachain": { - "wrappedToken": "0x6969696969696969696969696969696969696969", - "converter": "0x0000000000000000000000000000000000000000" - }, - "optimism": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "cronos": { - "wrappedToken": "0x5c7f8a570d578ed84e63fdfa7b1ee72deae1ae23", - "converter": "0x0000000000000000000000000000000000000000" - }, - "rootstock": { - "wrappedToken": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", - "converter": "0x0000000000000000000000000000000000000000" - }, - "bsc": { - "wrappedToken": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", - "converter": "0x0000000000000000000000000000000000000000" - }, - "gnosis": { - "wrappedToken": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", - "converter": "0x0000000000000000000000000000000000000000" - }, - "velas": { - "wrappedToken": "0xc579d1f3cf86749e05cd06f7ade17856c2ce3126", - "converter": "0x0000000000000000000000000000000000000000" - }, - "fuse": { - "wrappedToken": "0x0be9e53fd7edac9f859882afdda116645287c629", - "converter": "0x0000000000000000000000000000000000000000" - }, - "polygon": { - "wrappedToken": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", - "converter": "0x0000000000000000000000000000000000000000" - }, - "xlayer": { - "wrappedToken": "0xe538905cf8410324e03a5a23c1c177a474d59b2b", - "converter": "0x0000000000000000000000000000000000000000" - }, - "fantom": { - "wrappedToken": "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", - "converter": "0x0000000000000000000000000000000000000000" - }, - "fraxtal": { - "wrappedToken": "0xfc00000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "boba": { - "wrappedToken": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000", - "converter": "0x0000000000000000000000000000000000000000" - }, - "zksync": { - "wrappedToken": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91", - "converter": "0x0000000000000000000000000000000000000000" - }, - "metis": { - "wrappedToken": "0x75cb093E4D61d2A2e65D8e0BBb01DE8d89b53481", - "converter": "0x0000000000000000000000000000000000000000" - }, - "polygonzkevm": { - "wrappedToken": "0x4f9a0e7fd2bf6067db6994cf12e4495df938e6e9", - "converter": "0x0000000000000000000000000000000000000000" - }, - "moonbeam": { - "wrappedToken": "0xacc15dc74880c9944775448304b263d191c6077f", - "converter": "0x0000000000000000000000000000000000000000" - }, - "moonriver": { - "wrappedToken": "0x98878b06940ae243284ca214f92bb71a2b032b8a", - "converter": "0x0000000000000000000000000000000000000000" - }, - "sei": { - "wrappedToken": "0xE30feDd158A2e3b13e9badaeABaFc5516e95e8C7", - "converter": "0x0000000000000000000000000000000000000000" - }, - "gravity": { - "wrappedToken": "0xBB859E225ac8Fb6BE1C7e38D87b767e95Fef0EbD", - "converter": "0x0000000000000000000000000000000000000000" - }, - "mantle": { - "wrappedToken": "0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8", - "converter": "0x0000000000000000000000000000000000000000" - }, - "kaia": { - "wrappedToken": "0x19aac5f612f524b754ca7e7c41cbfa2e981a4432", - "converter": "0x0000000000000000000000000000000000000000" - }, - "base": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "evmos": { - "wrappedToken": "0xd4949664cd82660aae99bedc034a0dea8a0bd517", - "converter": "0x0000000000000000000000000000000000000000" - }, - "immutablezkevm": { - "wrappedToken": "0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d", - "converter": "0x0000000000000000000000000000000000000000" - }, - "ink": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "mode": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "arbitrum": { - "wrappedToken": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", - "converter": "0x0000000000000000000000000000000000000000" - }, - "celo": { - "wrappedToken": "0x471ece3750da237f93b8e339c536989b8978a438", - "converter": "0x0000000000000000000000000000000000000000" - }, - "avalanche": { - "wrappedToken": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", - "converter": "0x0000000000000000000000000000000000000000" - }, - "lens": { - "wrappedToken": "0x6bDc36E20D267Ff0dd6097799f82e78907105e2F", - "converter": "0x0000000000000000000000000000000000000000" - }, - "linea": { - "wrappedToken": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", - "converter": "0x0000000000000000000000000000000000000000" - }, - "lisk": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "blast": { - "wrappedToken": "0x4300000000000000000000000000000000000004", - "converter": "0x0000000000000000000000000000000000000000" - }, - "worldchain": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "unichain": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "taiko": { - "wrappedToken": "0xA51894664A773981C6C112C43ce576f315d5b1B6", - "converter": "0x0000000000000000000000000000000000000000" - }, - "scroll": { - "wrappedToken": "0x5300000000000000000000000000000000000004", - "converter": "0x0000000000000000000000000000000000000000" - }, - "soneium": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "sonic": { - "wrappedToken": "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38", - "converter": "0x0000000000000000000000000000000000000000" - }, - "swellchain": { - "wrappedToken": "0x4200000000000000000000000000000000000006", - "converter": "0x0000000000000000000000000000000000000000" - }, - "aurora": { - "wrappedToken": "0xc9bdeed33cd01541e1eed10f90519d2c06fe3feb", - "converter": "0x0000000000000000000000000000000000000000" - }, - "harmony": { - "wrappedToken": "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a", - "converter": "0x0000000000000000000000000000000000000000" - } -} diff --git a/config/networks.json b/config/networks.json index d92f4c841..a2cd8e390 100644 --- a/config/networks.json +++ b/config/networks.json @@ -1069,6 +1069,26 @@ "deployedWithSolcVersion": "0.8.17", "devNotes": "Contract verification is not working. In contact with the team to find a resolution. " }, + "stable": { + "name": "stable", + "chainId": 101010, + "nativeAddress": "0x0000000000000000000000000000000000000000", + "nativeCurrency": "gUSDT", + "wrappedNativeAddress": "0x779ded0c9e1022225f8e0630b35a9b54be713736", + "converterAddress": "0xded1660192d4d82e7c0b628ba556861edbb5cada", + "status": "active", + "type": "mainnet", + "rpcUrl": "https://rpc.stable.xyz", + "verificationType": "etherscan", + "explorerUrl": "https://stablescan.xyz", + "explorerApiUrl": "https://stablescan.xyz/api", + "multicallAddress": "0x0000000000000000000000000000000000000000", + "safeAddress": "0x0000000000000000000000000000000000000000", + "gasZipChainId": 0, + "isZkEVM": false, + "deployedWithEvmVersion": "london", + "deployedWithSolcVersion": "0.8.17" + }, "superposition": { "name": "superposition", "chainId": 55244, diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index 2daf86a1a..b8c4f5627 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -9,6 +9,8 @@ import { stdJson } from "forge-std/Script.sol"; contract DeployScript is DeployScriptBase { using stdJson for string; + error InvalidConverter(); + constructor() DeployScriptBase("TokenWrapper") {} function run() @@ -30,7 +32,7 @@ contract DeployScript is DeployScriptBase { string.concat(".", network, ".wrappedNativeAddress") ); - // try to get converter address, default to address(0) if not found + // Try to get converter address, default to address(0) if not found address converterAddress; try vm.parseJsonAddress( @@ -43,6 +45,11 @@ contract DeployScript is DeployScriptBase { converterAddress = address(0); } + // Verify converter is a contract if address is non-zero + if (converterAddress != address(0)) { + if (converterAddress.code.length == 0) revert InvalidConverter(); + } + // get path of global config file string memory globalConfigPath = string.concat( root, diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index adcb9ad44..44b628d6e 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -699,19 +699,14 @@ "TokenWrapper": { "configData": { "_wrappedToken": { - "configFileName": "tokenwrapper.json", - "keyInConfigFile": "..wrappedToken", - "allowToDeployWithZeroAddress": "true" - }, - "_converter": { - "configFileName": "tokenwrapper.json", - "keyInConfigFile": "..converter", - "allowToDeployWithZeroAddress": "true" + "configFileName": "networks.json", + "keyInConfigFile": "..wrappedNativeAddress", + "allowToDeployWithZeroAddress": "false" }, "_owner": { "configFileName": "global.json", - "keyInConfigFile": ".withdrawWallet", - "allowToDeployWithZeroAddress": "true" + "keyInConfigFile": ".refundWallet", + "allowToDeployWithZeroAddress": "false" } } } diff --git a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol index 2daf86a1a..cf317e0f2 100644 --- a/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol +++ b/script/deploy/zksync/DeployTokenWrapper.zksync.s.sol @@ -30,7 +30,7 @@ contract DeployScript is DeployScriptBase { string.concat(".", network, ".wrappedNativeAddress") ); - // try to get converter address, default to address(0) if not found + // Try to get converter address, default to address(0) if not found address converterAddress; try vm.parseJsonAddress( @@ -43,6 +43,9 @@ contract DeployScript is DeployScriptBase { converterAddress = address(0); } + // Note: On zkEVM, we cannot verify if the converter is a contract address + // using extcodesize during deployment, as contract code may not be available yet + // get path of global config file string memory globalConfigPath = string.concat( root, diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 580569195..138f704d8 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -14,14 +14,15 @@ import { IWrapper } from "../Interfaces/IWrapper.sol"; /// @notice Provides functionality for wrapping and unwrapping tokens /// @custom:version 1.2.0 contract TokenWrapper is WithdrawablePeriphery { - uint256 private constant MAX_INT = 2 ** 256 - 1; address public immutable WRAPPED_TOKEN; address public immutable CONVERTER; - address private immutable WRAPPER_ADDRESS; bool private immutable USE_CONVERTER; /// Errors /// error WithdrawFailure(); + error InvalidWrappedToken(); + error InvalidOwner(); + error InvalidConverter(); /// @notice Creates a new TokenWrapper contract /// @param _wrappedToken Address of the wrapped token (e.g., WETH, or token returned by converter) @@ -34,10 +35,33 @@ contract TokenWrapper is WithdrawablePeriphery { address _converter, address _owner ) WithdrawablePeriphery(_owner) { + if (_wrappedToken == address(0)) revert InvalidWrappedToken(); + if (_owner == address(0)) revert InvalidOwner(); + WRAPPED_TOKEN = _wrappedToken; - CONVERTER = _converter; USE_CONVERTER = _converter != address(0); - WRAPPER_ADDRESS = USE_CONVERTER ? _converter : _wrappedToken; + + if (USE_CONVERTER) { + if (!_isContract(_converter)) revert InvalidConverter(); + CONVERTER = _converter; + // Approve converter once for all future withdrawals (gas optimization) + LibAsset.maxApproveERC20( + IERC20(_wrappedToken), + _converter, + type(uint256).max + ); + } else { + CONVERTER = _wrappedToken; + } + } + + /// @dev Check if an address is a contract + function _isContract(address _addr) private view returns (bool) { + uint256 size; + assembly { + size := extcodesize(_addr) + } + return size > 0; } /// External Methods /// @@ -50,7 +74,7 @@ contract TokenWrapper is WithdrawablePeriphery { uint256 balanceBefore = IERC20(WRAPPED_TOKEN).balanceOf( address(this) ); - IWrapper(WRAPPER_ADDRESS).deposit{ value: msg.value }(); + IWrapper(CONVERTER).deposit{ value: msg.value }(); uint256 balanceAfter = IERC20(WRAPPED_TOKEN).balanceOf( address(this) ); @@ -61,40 +85,37 @@ contract TokenWrapper is WithdrawablePeriphery { amountReceived ); } else { - IWrapper(WRAPPER_ADDRESS).deposit{ value: msg.value }(); + IWrapper(CONVERTER).deposit{ value: msg.value }(); SafeTransferLib.safeTransfer(WRAPPED_TOKEN, msg.sender, msg.value); } } /// @notice Unwraps all the caller's balance of wrapped token and returns native tokens /// @dev Pulls wrapped tokens from msg.sender based on their balance (requires prior approval) - /// @dev If converter is set, approves converter and measures actual native amount received - /// @dev If no converter, unwraps 1:1 and transfers exact wad amount of native tokens + /// @dev If converter is set, measures actual native amount received after conversion + /// @dev If no converter, unwraps 1:1 and transfers exact amount of native tokens function withdraw() external { // While in a general purpose contract it would make sense - // to have `wad` equal to the minimum between the balance and the + // to have `amount` equal to the minimum between the balance and the // given allowance, in our specific usecase allowance is always // nearly MAX_UINT256. Using the balance only is a gas optimisation. - uint256 wad = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender); + uint256 amount = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender); SafeTransferLib.safeTransferFrom( WRAPPED_TOKEN, msg.sender, address(this), - wad + amount ); if (USE_CONVERTER) { - // Approve converter to spend wrappedToken - LibAsset.maxApproveERC20(IERC20(WRAPPED_TOKEN), CONVERTER, wad); - uint256 balanceBefore = address(this).balance; - IWrapper(WRAPPER_ADDRESS).withdraw(wad); + IWrapper(CONVERTER).withdraw(amount); uint256 balanceAfter = address(this).balance; uint256 amountReceived = balanceAfter - balanceBefore; SafeTransferLib.safeTransferETH(msg.sender, amountReceived); } else { - IWrapper(WRAPPER_ADDRESS).withdraw(wad); - SafeTransferLib.safeTransferETH(msg.sender, wad); + IWrapper(CONVERTER).withdraw(amount); + SafeTransferLib.safeTransferETH(msg.sender, amount); } } diff --git a/test/solidity/Periphery/TokenWrapper.t.sol b/test/solidity/Periphery/TokenWrapper.t.sol index 532014017..bef03fcea 100644 --- a/test/solidity/Periphery/TokenWrapper.t.sol +++ b/test/solidity/Periphery/TokenWrapper.t.sol @@ -26,6 +26,8 @@ contract TokenWrapperTest is DSTest { TestBasicToken private token6Decimals; TestConverterWithDecimals private decimalConverter; + uint256 private constant ONE_USDT = 1_000_000; + error ETHTransferFailed(); function setUp() public { @@ -131,26 +133,8 @@ contract TokenWrapperTest is DSTest { } function test_ConverterReceivesApproval() public { - // Give user some basic tokens - basicToken.mint(address(this), 1 ether); - - // Approve TokenWrapper - basicToken.approve(address(tokenWrapperWithConverter), 1 ether); - - // Check that converter has no allowance before withdraw - assertEq( - basicToken.allowance( - address(tokenWrapperWithConverter), - address(converter) - ), - 0 - ); - - // Withdraw - tokenWrapperWithConverter.withdraw(); - - // After withdraw, the approval should have been set - // (TokenWrapper approves converter to pull tokens) + // Check that converter was approved in constructor + // (TokenWrapper approves converter once during deployment for gas efficiency) assertEq( basicToken.allowance( address(tokenWrapperWithConverter), @@ -169,19 +153,19 @@ contract TokenWrapperTest is DSTest { tokenWrapperWithDecimalConverter.deposit{ value: 1 ether }(); // User should now have 1 USDT (1e6, not 1e18) - assertEq(token6Decimals.balanceOf(address(this)), 1_000_000); + assertEq(token6Decimals.balanceOf(address(this)), ONE_USDT); } function test_CanWithdrawWithDecimalConverter() public { uint256 initialBalance = address(this).balance; // Give user 1 USDT (6 decimals) - token6Decimals.mint(address(this), 1_000_000); + token6Decimals.mint(address(this), ONE_USDT); // Approve TokenWrapper to spend tokens token6Decimals.approve( address(tokenWrapperWithDecimalConverter), - 1_000_000 + ONE_USDT ); // User withdraws 1 USDT (6 decimals) @@ -200,12 +184,12 @@ contract TokenWrapperTest is DSTest { // Deposit 5 ETH, should receive 5 USDT (6 decimals) tokenWrapperWithDecimalConverter.deposit{ value: depositAmount }(); - assertEq(token6Decimals.balanceOf(address(this)), 5_000_000); + assertEq(token6Decimals.balanceOf(address(this)), 5 * ONE_USDT); // Approve and withdraw all USDT token6Decimals.approve( address(tokenWrapperWithDecimalConverter), - 5_000_000 + 5 * ONE_USDT ); uint256 balanceBefore = address(this).balance; From 8843234da9cd18d64c9a81dac8520d3280388e10 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 16 Dec 2025 12:45:55 +0300 Subject: [PATCH 08/15] fixes --- script/deploy/facets/DeployTokenWrapper.s.sol | 5 ++--- src/Periphery/TokenWrapper.sol | 10 ++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index b8c4f5627..2bc0001a6 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -5,12 +5,11 @@ pragma solidity ^0.8.17; import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; import { TokenWrapper } from "lifi/Periphery/TokenWrapper.sol"; import { stdJson } from "forge-std/Script.sol"; +import { InvalidContract } from "lifi/Errors/GenericErrors.sol"; contract DeployScript is DeployScriptBase { using stdJson for string; - error InvalidConverter(); - constructor() DeployScriptBase("TokenWrapper") {} function run() @@ -47,7 +46,7 @@ contract DeployScript is DeployScriptBase { // Verify converter is a contract if address is non-zero if (converterAddress != address(0)) { - if (converterAddress.code.length == 0) revert InvalidConverter(); + if (converterAddress.code.length == 0) revert InvalidContract(); } // get path of global config file diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index 138f704d8..a199b9cfd 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -8,6 +8,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { IWrapper } from "../Interfaces/IWrapper.sol"; +import { InvalidContract, NoTransferToNullAddress } from "../Errors/GenericErrors.sol"; /// @title TokenWrapper /// @author LI.FI (https://li.fi) @@ -20,9 +21,6 @@ contract TokenWrapper is WithdrawablePeriphery { /// Errors /// error WithdrawFailure(); - error InvalidWrappedToken(); - error InvalidOwner(); - error InvalidConverter(); /// @notice Creates a new TokenWrapper contract /// @param _wrappedToken Address of the wrapped token (e.g., WETH, or token returned by converter) @@ -35,14 +33,14 @@ contract TokenWrapper is WithdrawablePeriphery { address _converter, address _owner ) WithdrawablePeriphery(_owner) { - if (_wrappedToken == address(0)) revert InvalidWrappedToken(); - if (_owner == address(0)) revert InvalidOwner(); + if (_wrappedToken == address(0)) revert NoTransferToNullAddress(); + if (_owner == address(0)) revert NoTransferToNullAddress(); WRAPPED_TOKEN = _wrappedToken; USE_CONVERTER = _converter != address(0); if (USE_CONVERTER) { - if (!_isContract(_converter)) revert InvalidConverter(); + if (!_isContract(_converter)) revert InvalidContract(); CONVERTER = _converter; // Approve converter once for all future withdrawals (gas optimization) LibAsset.maxApproveERC20( From d364b8b236d81ee9ade2dbf3d41c3a79f9e20d2e Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 16 Dec 2025 13:28:45 +0300 Subject: [PATCH 09/15] fixes --- script/deploy/facets/DeployTokenWrapper.s.sol | 4 +++- src/Periphery/TokenWrapper.sol | 11 +---------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index 2bc0001a6..3f326f5eb 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -6,6 +6,7 @@ import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; import { TokenWrapper } from "lifi/Periphery/TokenWrapper.sol"; import { stdJson } from "forge-std/Script.sol"; import { InvalidContract } from "lifi/Errors/GenericErrors.sol"; +import { LibAsset } from "lifi/Libraries/LibAsset.sol"; contract DeployScript is DeployScriptBase { using stdJson for string; @@ -46,7 +47,8 @@ contract DeployScript is DeployScriptBase { // Verify converter is a contract if address is non-zero if (converterAddress != address(0)) { - if (converterAddress.code.length == 0) revert InvalidContract(); + if (!LibAsset.isContract(converterAddress)) + revert InvalidContract(); } // get path of global config file diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index a199b9cfd..f7ab2b5fb 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -40,7 +40,7 @@ contract TokenWrapper is WithdrawablePeriphery { USE_CONVERTER = _converter != address(0); if (USE_CONVERTER) { - if (!_isContract(_converter)) revert InvalidContract(); + if (!LibAsset.isContract(_converter)) revert InvalidContract(); CONVERTER = _converter; // Approve converter once for all future withdrawals (gas optimization) LibAsset.maxApproveERC20( @@ -53,15 +53,6 @@ contract TokenWrapper is WithdrawablePeriphery { } } - /// @dev Check if an address is a contract - function _isContract(address _addr) private view returns (bool) { - uint256 size; - assembly { - size := extcodesize(_addr) - } - return size > 0; - } - /// External Methods /// /// @notice Wraps the native token and transfers wrapped tokens to caller From 59c8de43682f63881f449260264af1f83af9ed71 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 16 Dec 2025 14:02:19 +0300 Subject: [PATCH 10/15] fixes --- archive/config/tokenwrapper.json | 48 +++++++++++++++++++ .../deploy/resources/deployRequirements.json | 5 ++ 2 files changed, 53 insertions(+) create mode 100644 archive/config/tokenwrapper.json diff --git a/archive/config/tokenwrapper.json b/archive/config/tokenwrapper.json new file mode 100644 index 000000000..45c18592d --- /dev/null +++ b/archive/config/tokenwrapper.json @@ -0,0 +1,48 @@ +{ + "comment": "Note: this file is generated by a backend script, do not change it manually!", + "mainnet": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + "abstract": "0x3439153EB7AF838Ad19d56E1571FBD09333C2809", + "berachain": "0x6969696969696969696969696969696969696969", + "optimism": "0x4200000000000000000000000000000000000006", + "cronos": "0x5c7f8a570d578ed84e63fdfa7b1ee72deae1ae23", + "rootstock": "0x542fda317318ebf1d3deaf76e0b632741a7e677d", + "bsc": "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + "gnosis": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", + "velas": "0xc579d1f3cf86749e05cd06f7ade17856c2ce3126", + "fuse": "0x0be9e53fd7edac9f859882afdda116645287c629", + "polygon": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", + "xlayer": "0xe538905cf8410324e03a5a23c1c177a474d59b2b", + "fantom": "0x21be370d5312f44cb42ce377bc9b8a0cef1a4c83", + "fraxtal": "0xfc00000000000000000000000000000000000006", + "boba": "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000", + "zksync": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91", + "metis": "0x75cb093E4D61d2A2e65D8e0BBb01DE8d89b53481", + "polygonzkevm": "0x4f9a0e7fd2bf6067db6994cf12e4495df938e6e9", + "moonbeam": "0xacc15dc74880c9944775448304b263d191c6077f", + "moonriver": "0x98878b06940ae243284ca214f92bb71a2b032b8a", + "sei": "0xE30feDd158A2e3b13e9badaeABaFc5516e95e8C7", + "gravity": "0xBB859E225ac8Fb6BE1C7e38D87b767e95Fef0EbD", + "mantle": "0x78c1b0C915c4FAA5FffA6CAbf0219DA63d7f4cb8", + "kaia": "0x19aac5f612f524b754ca7e7c41cbfa2e981a4432", + "base": "0x4200000000000000000000000000000000000006", + "evmos": "0xd4949664cd82660aae99bedc034a0dea8a0bd517", + "immutablezkevm": "0x3a0c2ba54d6cbd3121f01b96dfd20e99d1696c9d", + "ink": "0x4200000000000000000000000000000000000006", + "mode": "0x4200000000000000000000000000000000000006", + "arbitrum": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "celo": "0x471ece3750da237f93b8e339c536989b8978a438", + "avalanche": "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", + "lens": "0x6bDc36E20D267Ff0dd6097799f82e78907105e2F", + "linea": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", + "lisk": "0x4200000000000000000000000000000000000006", + "blast": "0x4300000000000000000000000000000000000004", + "worldchain": "0x4200000000000000000000000000000000000006", + "unichain": "0x4200000000000000000000000000000000000006", + "taiko": "0xA51894664A773981C6C112C43ce576f315d5b1B6", + "scroll": "0x5300000000000000000000000000000000000004", + "soneium": "0x4200000000000000000000000000000000000006", + "sonic": "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38", + "swellchain": "0x4200000000000000000000000000000000000006", + "aurora": "0xc9bdeed33cd01541e1eed10f90519d2c06fe3feb", + "harmony": "0xcf664087a5bb0237a0bad6742852ec6c8d69a27a" +} diff --git a/script/deploy/resources/deployRequirements.json b/script/deploy/resources/deployRequirements.json index 44b628d6e..ccf444498 100644 --- a/script/deploy/resources/deployRequirements.json +++ b/script/deploy/resources/deployRequirements.json @@ -703,6 +703,11 @@ "keyInConfigFile": "..wrappedNativeAddress", "allowToDeployWithZeroAddress": "false" }, + "_converter": { + "configFileName": "networks.json", + "keyInConfigFile": "..converterAddress", + "allowToDeployWithZeroAddress": "true" + }, "_owner": { "configFileName": "global.json", "keyInConfigFile": ".refundWallet", From 8cf10c2895dc9ba0a2a658b2901e51ca02ff546c Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 22 Dec 2025 16:18:19 +0300 Subject: [PATCH 11/15] refactor: move converterAddress to dedicated tokenconverters.json config - Create new config/tokenconverters.json for token converter addresses - Update DeployTokenWrapper.s.sol to read from tokenconverters.json - Remove converterAddress from networks.json stable chain entry - Defaults to address(0) for chains without a converter configured --- config/networks.json | 1 - config/tokenconverters.json | 5 +++++ script/deploy/facets/DeployTokenWrapper.s.sol | 8 ++++++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 config/tokenconverters.json diff --git a/config/networks.json b/config/networks.json index a2cd8e390..505a157c9 100644 --- a/config/networks.json +++ b/config/networks.json @@ -1075,7 +1075,6 @@ "nativeAddress": "0x0000000000000000000000000000000000000000", "nativeCurrency": "gUSDT", "wrappedNativeAddress": "0x779ded0c9e1022225f8e0630b35a9b54be713736", - "converterAddress": "0xded1660192d4d82e7c0b628ba556861edbb5cada", "status": "active", "type": "mainnet", "rpcUrl": "https://rpc.stable.xyz", diff --git a/config/tokenconverters.json b/config/tokenconverters.json new file mode 100644 index 000000000..9b1bf8c74 --- /dev/null +++ b/config/tokenconverters.json @@ -0,0 +1,5 @@ +{ + "stable": { + "converterAddress": "0xded1660192d4d82e7c0b628ba556861edbb5cada" + } +} diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index 3f326f5eb..2da8c821f 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -32,11 +32,15 @@ contract DeployScript is DeployScriptBase { string.concat(".", network, ".wrappedNativeAddress") ); - // Try to get converter address, default to address(0) if not found + // Try to get converter address from tokenconverters.json, default to address(0) if not found address converterAddress; + string memory convertersConfigPath = string.concat( + root, + "/config/tokenconverters.json" + ); try vm.parseJsonAddress( - vm.readFile(path), + vm.readFile(convertersConfigPath), string.concat(".", network, ".converterAddress") ) returns (address addr) { From 19f5d45a1068f9f1894dffca65adcf7dfede9a20 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 22 Dec 2025 16:37:12 +0300 Subject: [PATCH 12/15] Update script/deploy/facets/DeployTokenWrapper.s.sol Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- script/deploy/facets/DeployTokenWrapper.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/deploy/facets/DeployTokenWrapper.s.sol b/script/deploy/facets/DeployTokenWrapper.s.sol index 2da8c821f..95980856e 100644 --- a/script/deploy/facets/DeployTokenWrapper.s.sol +++ b/script/deploy/facets/DeployTokenWrapper.s.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-only - +pragma solidity ^0.8.17; pragma solidity ^0.8.17; import { DeployScriptBase } from "./utils/DeployScriptBase.sol"; From 1fb5247a1df1a4164d5dec461d2e2392c42da964 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 22 Dec 2025 16:37:53 +0300 Subject: [PATCH 13/15] remove stable --- config/networks.json | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/config/networks.json b/config/networks.json index 505a157c9..9a2d640ac 100644 --- a/config/networks.json +++ b/config/networks.json @@ -49,7 +49,7 @@ "rpcUrl": "https://rpc.apechain.com/http", "verificationType": "etherscan", "explorerUrl": "https://apescan.io", - "explorerApiUrl": "https://api.apescan.io/api", + "explorerApiUrl": "https://api.etherscan.io/v2/api?chainid=33139", "multicallAddress": "0xcA11bde05977b3631167028862bE2a173976CA11", "safeAddress": "0x18FA92415EA566828b4A5191B4cC7B73e4230059", "gasZipChainId": 296, @@ -1071,22 +1071,23 @@ }, "stable": { "name": "stable", - "chainId": 101010, - "nativeAddress": "0x0000000000000000000000000000000000000000", + "chainId": 988, + "nativeAddress": "0x0000000000000000000000000000000000001000", "nativeCurrency": "gUSDT", - "wrappedNativeAddress": "0x779ded0c9e1022225f8e0630b35a9b54be713736", + "wrappedNativeAddress": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", "status": "active", "type": "mainnet", "rpcUrl": "https://rpc.stable.xyz", "verificationType": "etherscan", "explorerUrl": "https://stablescan.xyz", - "explorerApiUrl": "https://stablescan.xyz/api", - "multicallAddress": "0x0000000000000000000000000000000000000000", - "safeAddress": "0x0000000000000000000000000000000000000000", - "gasZipChainId": 0, + "explorerApiUrl": "https://api.etherscan.io/v2/api?chainid=988", + "multicallAddress": "0xcA11bde05977b3631167028862bE2a173976CA11", + "safeAddress": "0xBa663f314B78492de579835F85Dc90d62E563cD1", + "gasZipChainId": 513, "isZkEVM": false, - "deployedWithEvmVersion": "london", - "deployedWithSolcVersion": "0.8.17" + "deployedWithEvmVersion": "cancun", + "deployedWithSolcVersion": "0.8.29", + "create3Factory": "0xeBbbaC35500713C4AD49929e1bE4225c7efF6510" }, "superposition": { "name": "superposition", From bc81b9c10da9e32fcf1d1a285508b1668a4ff552 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 22 Dec 2025 17:12:15 +0300 Subject: [PATCH 14/15] refactor: optimize TokenWrapper with precalculated swap ratio Replace balance-before/after pattern with immutable SWAP_RATIO_MULTIPLIER that is calculated once in constructor based on wrapped token decimals. This provides gas savings by eliminating balance checks and calculations, ensures deterministic conversion amounts, and simplifies the code by removing conditional branches. --- src/Periphery/TokenWrapper.sol | 45 +++++++++++----------------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index f7ab2b5fb..d7415c76f 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.17; // solhint-disable-next-line no-unused-import import { LibAsset } from "../Libraries/LibAsset.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { IWrapper } from "../Interfaces/IWrapper.sol"; @@ -18,6 +19,7 @@ contract TokenWrapper is WithdrawablePeriphery { address public immutable WRAPPED_TOKEN; address public immutable CONVERTER; bool private immutable USE_CONVERTER; + uint256 private immutable SWAP_RATIO_MULTIPLIER; /// Errors /// error WithdrawFailure(); @@ -48,41 +50,29 @@ contract TokenWrapper is WithdrawablePeriphery { _converter, type(uint256).max ); + // Calculate swap ratio based on decimal difference between native (18) and wrapped token + uint8 wrappedDecimals = IERC20Metadata(_wrappedToken).decimals(); + SWAP_RATIO_MULTIPLIER = 10 ** wrappedDecimals; } else { CONVERTER = _wrappedToken; + SWAP_RATIO_MULTIPLIER = 1 ether; // 1:1 ratio for 18 decimals } } /// External Methods /// /// @notice Wraps the native token and transfers wrapped tokens to caller - /// @dev If converter is set, uses it to convert native to wrapped tokens and measures actual amount received + /// @dev If converter is set, uses it to convert native to wrapped tokens using precalculated ratio /// @dev If no converter, wraps native 1:1 and transfers msg.value of wrapped tokens function deposit() external payable { - if (USE_CONVERTER) { - uint256 balanceBefore = IERC20(WRAPPED_TOKEN).balanceOf( - address(this) - ); - IWrapper(CONVERTER).deposit{ value: msg.value }(); - uint256 balanceAfter = IERC20(WRAPPED_TOKEN).balanceOf( - address(this) - ); - uint256 amountReceived = balanceAfter - balanceBefore; - SafeTransferLib.safeTransfer( - WRAPPED_TOKEN, - msg.sender, - amountReceived - ); - } else { - IWrapper(CONVERTER).deposit{ value: msg.value }(); - SafeTransferLib.safeTransfer(WRAPPED_TOKEN, msg.sender, msg.value); - } + IWrapper(CONVERTER).deposit{ value: msg.value }(); + uint256 wrappedAmount = (msg.value * SWAP_RATIO_MULTIPLIER) / 1 ether; + SafeTransferLib.safeTransfer(WRAPPED_TOKEN, msg.sender, wrappedAmount); } /// @notice Unwraps all the caller's balance of wrapped token and returns native tokens /// @dev Pulls wrapped tokens from msg.sender based on their balance (requires prior approval) - /// @dev If converter is set, measures actual native amount received after conversion - /// @dev If no converter, unwraps 1:1 and transfers exact amount of native tokens + /// @dev Uses precalculated ratio to determine native token amount to return function withdraw() external { // While in a general purpose contract it would make sense // to have `amount` equal to the minimum between the balance and the @@ -96,16 +86,9 @@ contract TokenWrapper is WithdrawablePeriphery { amount ); - if (USE_CONVERTER) { - uint256 balanceBefore = address(this).balance; - IWrapper(CONVERTER).withdraw(amount); - uint256 balanceAfter = address(this).balance; - uint256 amountReceived = balanceAfter - balanceBefore; - SafeTransferLib.safeTransferETH(msg.sender, amountReceived); - } else { - IWrapper(CONVERTER).withdraw(amount); - SafeTransferLib.safeTransferETH(msg.sender, amount); - } + IWrapper(CONVERTER).withdraw(amount); + uint256 nativeAmount = (amount * 1 ether) / SWAP_RATIO_MULTIPLIER; + SafeTransferLib.safeTransferETH(msg.sender, nativeAmount); } // Needs to be able to receive native on `withdraw` From 4f4f19defd4e6e1ccd2c3901b897218cae05f826 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Mon, 22 Dec 2025 17:18:30 +0300 Subject: [PATCH 15/15] refactor: use InvalidConfig for constructor validation in TokenWrapper Replace NoTransferToNullAddress with InvalidConfig for _wrappedToken and _owner parameter validation to follow project's constructor parameter validation pattern. --- src/Periphery/TokenWrapper.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Periphery/TokenWrapper.sol b/src/Periphery/TokenWrapper.sol index d7415c76f..bf07fe87d 100644 --- a/src/Periphery/TokenWrapper.sol +++ b/src/Periphery/TokenWrapper.sol @@ -9,7 +9,7 @@ import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/I import { WithdrawablePeriphery } from "../Helpers/WithdrawablePeriphery.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; import { IWrapper } from "../Interfaces/IWrapper.sol"; -import { InvalidContract, NoTransferToNullAddress } from "../Errors/GenericErrors.sol"; +import { InvalidContract, InvalidConfig } from "../Errors/GenericErrors.sol"; /// @title TokenWrapper /// @author LI.FI (https://li.fi) @@ -35,8 +35,8 @@ contract TokenWrapper is WithdrawablePeriphery { address _converter, address _owner ) WithdrawablePeriphery(_owner) { - if (_wrappedToken == address(0)) revert NoTransferToNullAddress(); - if (_owner == address(0)) revert NoTransferToNullAddress(); + if (_wrappedToken == address(0)) revert InvalidConfig(); + if (_owner == address(0)) revert InvalidConfig(); WRAPPED_TOKEN = _wrappedToken; USE_CONVERTER = _converter != address(0);