A vulnerability exists in the token bridging mechanism that allows transfer of non-standard tokens with fee-on-transfer functionality from Layer 1 (L1) to Layer 2 (L2), despite documentation stating such tokens are not supported. This issue can occur when transferring base tokens or using the `Bridgehub` for ERC20 token transfers.
This means tokens with transfer fees can be bridged without proper accounting, potentially leading to discrepancies in token balances.
Manual review.
Unit test.
function _bridgeBurnNativeToken(
uint256 _chainId,
bytes32 _assetId,
address _originalCaller,
bool _depositChecked,
bytes calldata _data
) internal override returns (bytes memory _bridgeMintData) {
console.log("L1NativeTokenVault:_bridgeBurnNativeToken");
if (!_depositChecked) {
IERC20 token = IERC20(tokenAddress[_assetId]);
uint balanceBefore = token.balanceOf(address(this));
uint256 _depositAmount;
(_depositAmount,) = abi.decode(_data, (uint256, address));
bool depositDone = IL1AssetRouter(address(ASSET_ROUTER)).transferFundsToNTV(
_assetId,
_depositAmount,
_originalCaller
);
if (depositDone) {
uint256 balanceAfter = token.balanceOf(address(this));
if (balanceAfter - balanceBefore != _depositAmount) {
revert TokensWithFeesNotSupported();
}
_depositChecked = true;
}
}
console.log("depositChecked", _depositChecked);
_bridgeMintData = super._bridgeBurnNativeToken({
_chainId: _chainId,
_assetId: _assetId,
_originalCaller: _originalCaller,
_depositChecked: _depositChecked,
_data: _data
});
}
contract TestnetERC20TokenWithFee is TestnetERC20Token {
constructor(string memory name_, string memory symbol_, uint8 decimals_) TestnetERC20Token(name_, symbol_, decimals_) {
}
function transfer(address to, uint256 amount) public virtual override returns (bool) {
uint fee = amount / 10;
_burn(msg.sender, fee);
return super.transfer(to, amount - fee);
}
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
uint fee = amount / 10;
_burn(from, fee);
return super.transferFrom(from, to, amount - fee);
}
}
function test_bridgehubDepositBaseToken_ErcWithFee() public {
TestnetERC20TokenWithFee tokenWithFee = new TestnetERC20TokenWithFee("TestnetERC20TokenWithFee", "TET", 18);
bytes32 tokenWithFeeAssetId = DataEncoding.encodeNTVAssetId(block.chainid, address(tokenWithFee));
vm.prank(address(nativeTokenVault));
nativeTokenVault.registerToken(address(tokenWithFee));
tokenWithFee.mint(alice, amount);
vm.prank(alice);
tokenWithFee.approve(address(sharedBridge), amount);
vm.prank(bridgehubAddress);
vm.expectEmit(true, true, true, true, address(sharedBridge));
emit BridgehubDepositBaseTokenInitiated(chainId, alice, tokenWithFeeAssetId, amount);
sharedBridge.bridgehubDepositBaseToken(chainId, tokenWithFeeAssetId, alice, amount);
assertEq(tokenWithFee.balanceOf(alice), 0);
uint amountMinusFee = amount - (amount / 10);
assertEq(tokenWithFee.balanceOf(address(nativeTokenVault)), amountMinusFee);
}
function test_bridgehubDeposit_ErcWithFee() public {
TestnetERC20TokenWithFee tokenWithFee = new TestnetERC20TokenWithFee("TestnetERC20TokenWithFee", "TET", 18);
bytes32 tokenWithFeeAssetId = DataEncoding.encodeNTVAssetId(block.chainid, address(tokenWithFee));
vm.prank(address(nativeTokenVault));
nativeTokenVault.registerToken(address(tokenWithFee));
tokenWithFee.mint(alice, amount);
vm.prank(alice);
tokenWithFee.approve(address(sharedBridge), amount);
bytes memory transferData = abi.encode(amount, bob);
bytes memory depositData = bytes.concat(NEW_ENCODING_VERSION, abi.encode(tokenWithFeeAssetId, transferData));
bytes memory erc20Metadata = nativeTokenVault.getERC20Getters(address(tokenWithFee), block.chainid);
bytes32 txDataHash = DataEncoding.encodeTxDataHash({
_nativeTokenVault: address(nativeTokenVault),
_encodingVersion: NEW_ENCODING_VERSION,
_originalCaller: alice,
_assetId: tokenWithFeeAssetId,
_transferData: transferData
});
bytes memory bridgeMintCalldata = DataEncoding.encodeBridgeMintData({
_originalCaller: alice,
_remoteReceiver: bob,
_originToken: address(tokenWithFee),
_amount: amount,
_erc20Metadata: erc20Metadata
});
vm.prank(bridgehubAddress);
vm.expectEmit(true, false, true, false, address(sharedBridge));
emit BridgehubDepositInitiated(chainId, txDataHash, alice, tokenWithFeeAssetId, bridgeMintCalldata);
sharedBridge.bridgehubDeposit(chainId, alice, 0, depositData);
assertEq(tokenWithFee.balanceOf(alice), 0);
uint amountMinusFee = amount - (amount / 10);
assertEq(tokenWithFee.balanceOf(address(nativeTokenVault)), amountMinusFee);
}