Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/tokenconverters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"stable": {
"converterAddress": "0xded1660192d4d82e7c0b628ba556861edbb5cada"
}
}
35 changes: 33 additions & 2 deletions script/deploy/facets/DeployTokenWrapper.s.sol
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity ^0.8.17;
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";
import { LibAsset } from "lifi/Libraries/LibAsset.sol";

contract DeployScript is DeployScriptBase {
using stdJson for string;
Expand All @@ -29,6 +32,29 @@ contract DeployScript is DeployScriptBase {
string.concat(".", network, ".wrappedNativeAddress")
);

// 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(convertersConfigPath),
string.concat(".", network, ".converterAddress")
)
returns (address addr) {
converterAddress = addr;
} catch {
converterAddress = address(0);
}

// Verify converter is a contract if address is non-zero
if (converterAddress != address(0)) {
if (!LibAsset.isContract(converterAddress))
revert InvalidContract();
}

// get path of global config file
string memory globalConfigPath = string.concat(
root,
Expand All @@ -43,6 +69,11 @@ contract DeployScript is DeployScriptBase {
".refundWallet"
);

return abi.encode(wrappedNativeAddress, refundWalletAddress);
return
abi.encode(
wrappedNativeAddress,
converterAddress,
refundWalletAddress
);
}
}
19 changes: 19 additions & 0 deletions script/deploy/resources/deployRequirements.json
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,25 @@
}
}
},
"TokenWrapper": {
"configData": {
"_wrappedToken": {
"configFileName": "networks.json",
"keyInConfigFile": ".<NETWORK>.wrappedNativeAddress",
"allowToDeployWithZeroAddress": "false"
},
"_converter": {
"configFileName": "networks.json",
"keyInConfigFile": ".<NETWORK>.converterAddress",
"allowToDeployWithZeroAddress": "true"
},
"_owner": {
"configFileName": "global.json",
"keyInConfigFile": ".refundWallet",
"allowToDeployWithZeroAddress": "false"
}
}
},
"NEARIntentsFacet": {
"configData": {
"_backendSigner": {
Expand Down
38 changes: 36 additions & 2 deletions script/deploy/tron/deploy-and-register-periphery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)

Expand Down Expand Up @@ -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}`)

Expand Down
26 changes: 24 additions & 2 deletions script/deploy/zksync/DeployTokenWrapper.zksync.s.sol
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -29,6 +30,22 @@ 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);
}

// 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,
Expand All @@ -43,6 +60,11 @@ contract DeployScript is DeployScriptBase {
".refundWallet"
);

return abi.encode(wrappedNativeAddress, refundWalletAddress);
return
abi.encode(
wrappedNativeAddress,
converterAddress,
refundWalletAddress
);
}
}
14 changes: 14 additions & 0 deletions src/Interfaces/IWrapper.sol
Original file line number Diff line number Diff line change
@@ -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;
}
83 changes: 59 additions & 24 deletions src/Periphery/TokenWrapper.sol
Original file line number Diff line number Diff line change
@@ -1,59 +1,94 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: LGPL-3.0-only

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";

/// 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";
import { InvalidContract, NoTransferToNullAddress } from "../Errors/GenericErrors.sol";

/// @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 immutable WRAPPED_TOKEN;
address public immutable CONVERTER;
bool private immutable USE_CONVERTER;
uint256 private immutable SWAP_RATIO_MULTIPLIER;

/// 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,
address _converter,

Check notice

Code scanning / Olympix Integrated Security

Parameters passed to a constructor that are not validated for correct values may lead to contract creation in an undesired state. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/no-parameter-validation-in-constructor Low

Parameters passed to a constructor that are not validated for correct values may lead to contract creation in an undesired state. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/no-parameter-validation-in-constructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see the validation of the constructor parameters. Why was this marked as fixed? @ezynda3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed dcb8a6d, 8843234: Added constructor parameter validation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we use InvalidConfig() error for this

address _owner
) WithdrawablePeriphery(_owner) {
wrappedToken = _wrappedToken;
IERC20(wrappedToken).approve(address(this), MAX_INT);
if (_wrappedToken == address(0)) revert NoTransferToNullAddress();
if (_owner == address(0)) revert NoTransferToNullAddress();
Comment on lines +38 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use InvalidConfig() error for constructor parameter validation.

Per past review discussion, the team preference for constructor parameter validation is to use InvalidConfig() error rather than NoTransferToNullAddress(). This aligns with the pattern used elsewhere in the codebase for constructor validation.

🔧 Proposed fix
-        if (_wrappedToken == address(0)) revert NoTransferToNullAddress();
-        if (_owner == address(0)) revert NoTransferToNullAddress();
+        if (_wrappedToken == address(0)) revert InvalidConfig();
+        if (_owner == address(0)) revert InvalidConfig();

Based on past review comments.

🤖 Prompt for AI Agents
In src/Periphery/TokenWrapper.sol around lines 38 to 39, the constructor
currently reverts with NoTransferToNullAddress() for invalid constructor
parameters; replace those reverts to use InvalidConfig() instead for both
_wrappedToken and _owner checks so constructor parameter validation follows the
project's InvalidConfig() pattern.


WRAPPED_TOKEN = _wrappedToken;
USE_CONVERTER = _converter != address(0);

if (USE_CONVERTER) {
if (!LibAsset.isContract(_converter)) revert InvalidContract();
CONVERTER = _converter;
// Approve converter once for all future withdrawals (gas optimization)
LibAsset.maxApproveERC20(
IERC20(_wrappedToken),
_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
/// @notice Wraps the native token and transfers wrapped tokens to caller
/// @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 {
IWrapper(wrappedToken).deposit{ value: msg.value }();
IERC20(wrappedToken).transfer(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
/// @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 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 `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(wrappedToken).balanceOf(msg.sender);
IERC20(wrappedToken).transferFrom(msg.sender, address(this), wad);
IWrapper(wrappedToken).withdraw(wad);
SafeTransferLib.safeTransferETH(msg.sender, wad);
uint256 amount = IERC20(WRAPPED_TOKEN).balanceOf(msg.sender);
SafeTransferLib.safeTransferFrom(
WRAPPED_TOKEN,
msg.sender,
address(this),
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`
Expand Down
Loading
Loading