Skip to content

Commit

Permalink
Support quick registration of NTV assets (#56)
Browse files Browse the repository at this point in the history
For better UX:

- Allow NTV have bridgeBurnData with optional last tokenAddress.
- If the `tokenAddress` is non zero, the NTV may attempt to register the
token if asset handler is not yet present
  • Loading branch information
StanislavBreadless authored Dec 9, 2024
1 parent c944c09 commit 502ee80
Show file tree
Hide file tree
Showing 18 changed files with 284 additions and 109 deletions.
9 changes: 6 additions & 3 deletions l1-contracts/contracts/bridge/L1Nullifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,8 @@ contract L1Nullifier is IL1Nullifier, ReentrancyGuard, Ownable2StepUpgradeable,
assetId = DataEncoding.encodeNTVAssetId(block.chainid, _l1Token);
}
// For legacy deposits, the l2 receiver is not required to check tx data hash
// bytes memory transferData = abi.encode(_amount, _depositSender);
bytes memory assetData = abi.encode(_amount, address(0));
// The token address does not have to be provided for this functionality either.
bytes memory assetData = DataEncoding.encodeBridgeBurnData(_amount, address(0), address(0));

_verifyAndClearFailedTransfer({
_checkedInLegacyBridge: false,
Expand Down Expand Up @@ -696,7 +696,10 @@ contract L1Nullifier is IL1Nullifier, ReentrancyGuard, Ownable2StepUpgradeable,
uint16 _l2TxNumberInBatch,
bytes32[] calldata _merkleProof
) external override onlyLegacyBridge {
bytes memory assetData = abi.encode(_amount, _depositSender);
// For legacy deposits, the l2 receiver is not required to check tx data hash
// The token address does not have to be provided for this functionality either.
bytes memory assetData = DataEncoding.encodeBridgeBurnData(_amount, address(0), address(0));

/// the legacy bridge can only be used with L1 native tokens.
bytes32 assetId = DataEncoding.encodeNTVAssetId(block.chainid, _l1Asset);

Expand Down
20 changes: 17 additions & 3 deletions l1-contracts/contracts/bridge/asset-router/AssetRouterBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {DataEncoding} from "../../common/libraries/DataEncoding.sol";
import {L2_NATIVE_TOKEN_VAULT_ADDR} from "../../common/L2ContractAddresses.sol";

import {IBridgehub} from "../../bridgehub/IBridgehub.sol";
import {Unauthorized, AssetHandlerDoesNotExist} from "../../common/L1ContractErrors.sol";
import {Unauthorized} from "../../common/L1ContractErrors.sol";
import {INativeTokenVault} from "../ntv/INativeTokenVault.sol";

/// @author Matter Labs
/// @custom:security-contact security@matterlabs.dev
Expand Down Expand Up @@ -133,18 +134,31 @@ abstract contract AssetRouterBase is IAssetRouterBase, Ownable2StepUpgradeable,
/// @param _originalCaller The `msg.sender` address from the external call that initiated current one.
/// @param _transferData The encoded data, which is used by the asset handler to determine L2 recipient and amount. Might include extra information.
/// @param _passValue Boolean indicating whether to pass msg.value in the call.
/// @param _nativeTokenVault The address of the native token vault.
/// @return bridgeMintCalldata The calldata used by remote asset handler to mint tokens for recipient.
function _burn(
uint256 _chainId,
uint256 _nextMsgValue,
bytes32 _assetId,
address _originalCaller,
bytes memory _transferData,
bool _passValue
bool _passValue,
address _nativeTokenVault
) internal returns (bytes memory bridgeMintCalldata) {
address l1AssetHandler = assetHandlerAddress[_assetId];
if (l1AssetHandler == address(0)) {
revert AssetHandlerDoesNotExist(_assetId);
// As a UX feature, whenever an asset handler is not present, we always try to register asset within native token vault.
// The Native Token Vault is trusted to revert in an asset does not belong to it.
//
// Note, that it may "pollute" error handling a bit: instead of getting error for asset handler not being
// present, the user will get whatever error the native token vault will return, however, providing
// more advanced error handling requires more extensive code and will be added in the future releases.
INativeTokenVault(_nativeTokenVault).tryRegisterTokenFromBurnData(_transferData, _assetId);

// We do not do any additional transformations here (like setting `assetHandler` in the mapping),
// because we expect that all those happened inside `tryRegisterTokenFromBurnData`

l1AssetHandler = _nativeTokenVault;
}

uint256 msgValue = _passValue ? msg.value : 0;
Expand Down
2 changes: 0 additions & 2 deletions l1-contracts/contracts/bridge/asset-router/IL2AssetRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,4 @@ interface IL2AssetRouter is IAssetRouterBase {
/// a legacy asset.
/// @param _assetId The assetId of the legacy token.
function setLegacyTokenAssetHandler(bytes32 _assetId) external;

function withdrawToken(address _l2NativeToken, bytes memory _assetData) external returns (bytes32);
}
30 changes: 20 additions & 10 deletions l1-contracts/contracts/bridge/asset-router/L1AssetRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {
_msgValue: 0,
_assetId: _assetId,
_originalCaller: _originalCaller,
_data: abi.encode(_amount, address(0))
_data: DataEncoding.encodeBridgeBurnData(_amount, address(0), address(0))
});

// Note that we don't save the deposited amount, as this is for the base token, which gets sent to the refundRecipient if the tx fails
Expand Down Expand Up @@ -267,17 +267,20 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {
revert AssetIdNotSupported(assetId);
}

address ntvCached = address(nativeTokenVault);

bytes memory bridgeMintCalldata = _burn({
_chainId: _chainId,
_nextMsgValue: _value,
_assetId: assetId,
_originalCaller: _originalCaller,
_transferData: transferData,
_passValue: true
_passValue: true,
_nativeTokenVault: ntvCached
});

bytes32 txDataHash = DataEncoding.encodeTxDataHash({
_nativeTokenVault: address(nativeTokenVault),
_nativeTokenVault: ntvCached,
_encodingVersion: encodingVersion,
_originalCaller: _originalCaller,
_assetId: assetId,
Expand Down Expand Up @@ -392,7 +395,7 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {
}
}

return (assetId, abi.encode(_depositAmount, _l2Receiver));
return (assetId, DataEncoding.encodeBridgeBurnData(_depositAmount, _l2Receiver, _l1Token));
}

/// @notice Ensures that token is registered with native token vault.
Expand Down Expand Up @@ -526,7 +529,12 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {

bytes32 _assetId;
{
bytes memory bridgeMintCalldata;
// Note, that to keep the code simple, while avoiding "stack too deep" error,
// this `bridgeData` variable is reused in two places with different meanings:
// - Firstly, it denotes the bridgeBurn data to be used for the NativeTokenVault
// - Secondly, after the call to `_burn` function, it denotes the `bridgeMint` data that
// will be sent to the L2 counterpart of the L1NTV.
bytes memory bridgeData = DataEncoding.encodeBridgeBurnData(_amount, _l2Receiver, _l1Token);
// Inner call to encode data to decrease local var numbers
_assetId = _ensureTokenRegisteredWithNTV(_l1Token);
// Legacy bridge is only expected to use native tokens for L1.
Expand All @@ -536,16 +544,18 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {

IERC20(_l1Token).forceApprove(address(nativeTokenVault), _amount);

bridgeMintCalldata = _burn({
// Note, that starting from here `bridgeData` starts denoting bridgeMintData.
bridgeData = _burn({
_chainId: ERA_CHAIN_ID,
_nextMsgValue: 0,
_assetId: _assetId,
_originalCaller: _originalCaller,
_transferData: abi.encode(_amount, _l2Receiver),
_passValue: false
_transferData: bridgeData,
_passValue: false,
_nativeTokenVault: address(nativeTokenVault)
});

bytes memory l2TxCalldata = getDepositCalldata(_originalCaller, _assetId, bridgeMintCalldata);
bytes memory l2TxCalldata = getDepositCalldata(_originalCaller, _assetId, bridgeData);

// If the refund recipient is not specified, the refund will be sent to the sender of the transaction.
// Otherwise, the refund will be sent to the specified address.
Expand All @@ -567,7 +577,7 @@ contract L1AssetRouter is AssetRouterBase, IL1AssetRouter, ReentrancyGuard {
}

{
bytes memory transferData = abi.encode(_amount, _l2Receiver);
bytes memory transferData = DataEncoding.encodeBridgeBurnData(_amount, _l2Receiver, _l1Token);
// Save the deposited amount to claim funds on L1 if the deposit failed on L2
L1_NULLIFIER.bridgehubConfirmL2TransactionForwarded(
ERA_CHAIN_ID,
Expand Down
30 changes: 9 additions & 21 deletions l1-contracts/contracts/bridge/asset-router/L2AssetRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {IAssetRouterBase} from "./IAssetRouterBase.sol";
import {AssetRouterBase} from "./AssetRouterBase.sol";

import {IL2NativeTokenVault} from "../ntv/IL2NativeTokenVault.sol";
import {INativeTokenVault} from "../ntv/INativeTokenVault.sol";
import {IL2SharedBridgeLegacy} from "../interfaces/IL2SharedBridgeLegacy.sol";
import {IAssetHandler} from "../interfaces/IAssetHandler.sol";
import {IBridgedStandardToken} from "../interfaces/IBridgedStandardToken.sol";
import {IL1ERC20Bridge} from "../interfaces/IL1ERC20Bridge.sol";

Expand Down Expand Up @@ -152,18 +150,6 @@ contract L2AssetRouter is AssetRouterBase, IL2AssetRouter {
return _withdrawSender(_assetId, _assetData, msg.sender, true);
}

/// @dev IMPORTANT: this method will be deprecated in one of the future releases, so contracts
/// that rely on it must be upgradeable.
function withdrawToken(address _l2NativeToken, bytes memory _assetData) public returns (bytes32) {
bytes32 recordedAssetId = INativeTokenVault(L2_NATIVE_TOKEN_VAULT_ADDR).assetId(_l2NativeToken);
uint256 recordedOriginChainId = INativeTokenVault(L2_NATIVE_TOKEN_VAULT_ADDR).originChainId(recordedAssetId);
if (recordedOriginChainId != block.chainid && recordedOriginChainId != 0) {
revert AssetIdNotSupported(recordedAssetId);
}
bytes32 assetId = _ensureTokenRegisteredWithNTV(_l2NativeToken);
return _withdrawSender(assetId, _assetData, msg.sender, true);
}

/*//////////////////////////////////////////////////////////////
Internal & Helpers
//////////////////////////////////////////////////////////////*/
Expand All @@ -185,18 +171,19 @@ contract L2AssetRouter is AssetRouterBase, IL2AssetRouter {
address _sender,
bool _alwaysNewMessageFormat
) internal returns (bytes32 txHash) {
address assetHandler = assetHandlerAddress[_assetId];
bytes memory _l1bridgeMintData = IAssetHandler(assetHandler).bridgeBurn({
bytes memory l1bridgeMintData = _burn({
_chainId: L1_CHAIN_ID,
_msgValue: 0,
_nextMsgValue: 0,
_assetId: _assetId,
_originalCaller: _sender,
_data: _assetData
_transferData: _assetData,
_passValue: false,
_nativeTokenVault: L2_NATIVE_TOKEN_VAULT_ADDR
});

bytes memory message;
if (_alwaysNewMessageFormat || L2_LEGACY_SHARED_BRIDGE == address(0)) {
message = _getAssetRouterWithdrawMessage(_assetId, _l1bridgeMintData);
message = _getAssetRouterWithdrawMessage(_assetId, l1bridgeMintData);
// slither-disable-next-line unused-return
txHash = L2ContractHelper.sendMessageToL1(message);
} else {
Expand All @@ -206,7 +193,8 @@ contract L2AssetRouter is AssetRouterBase, IL2AssetRouter {
if (l1Token == address(0)) {
revert AssetIdNotSupported(_assetId);
}
(uint256 amount, address l1Receiver) = abi.decode(_assetData, (uint256, address));
// slither-disable-next-line unused-return
(uint256 amount, address l1Receiver, ) = DataEncoding.decodeBridgeBurnData(_assetData);
message = _getSharedBridgeWithdrawMessage(l1Receiver, l1Token, amount);
txHash = IL2SharedBridgeLegacy(L2_LEGACY_SHARED_BRIDGE).sendMessageToL1(message);
}
Expand Down Expand Up @@ -325,7 +313,7 @@ contract L2AssetRouter is AssetRouterBase, IL2AssetRouter {
revert TokenNotLegacy();
}
bytes32 assetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, l1Address);
bytes memory data = abi.encode(_amount, _l1Receiver);
bytes memory data = DataEncoding.encodeBridgeBurnData(_amount, _l1Receiver, _l2Token);
_withdrawSender(assetId, data, _sender, false);
}

Expand Down
3 changes: 3 additions & 0 deletions l1-contracts/contracts/bridge/ntv/INativeTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ interface INativeTokenVault {

/// @notice Used to get the expected bridged token address corresponding to its native counterpart
function calculateCreate2TokenAddress(uint256 _originChainId, address _originToken) external view returns (address);

/// @notice Tries to register a token from the provided `_burnData` and reverts if it is not possible.
function tryRegisterTokenFromBurnData(bytes calldata _burnData, bytes32 _expectedAssetId) external;
}
18 changes: 13 additions & 5 deletions l1-contracts/contracts/bridge/ntv/L1NativeTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,10 @@ contract L1NativeTokenVault is IL1NativeTokenVault, IL1AssetHandler, NativeToken
address _originalCaller,
// solhint-disable-next-line no-unused-vars
bool _depositChecked,
bytes calldata _data
uint256 _depositAmount,
address _receiver,
address _nativeToken
) internal override returns (bytes memory _bridgeMintData) {
uint256 _depositAmount;
(_depositAmount, ) = abi.decode(_data, (uint256, address));
bool depositChecked = IL1AssetRouter(address(ASSET_ROUTER)).transferFundsToNTV(
_assetId,
_depositAmount,
Expand All @@ -178,7 +178,9 @@ contract L1NativeTokenVault is IL1NativeTokenVault, IL1AssetHandler, NativeToken
_assetId: _assetId,
_originalCaller: _originalCaller,
_depositChecked: depositChecked,
_data: _data
_depositAmount: _depositAmount,
_receiver: _receiver,
_nativeToken: _nativeToken
});
}

Expand All @@ -193,7 +195,8 @@ contract L1NativeTokenVault is IL1NativeTokenVault, IL1AssetHandler, NativeToken
address _depositSender,
bytes calldata _data
) external payable override requireZeroValue(msg.value) onlyAssetRouter whenNotPaused {
(uint256 _amount, ) = abi.decode(_data, (uint256, address));
// slither-disable-next-line unused-return
(uint256 _amount, , ) = DataEncoding.decodeBridgeBurnData(_data);
address l1Token = tokenAddress[_assetId];
if (_amount == 0) {
revert NoFundsTransferred();
Expand Down Expand Up @@ -228,6 +231,11 @@ contract L1NativeTokenVault is IL1NativeTokenVault, IL1AssetHandler, NativeToken
INTERNAL & HELPER FUNCTIONS
//////////////////////////////////////////////////////////////*/

function _registerTokenIfBridgedLegacy(address) internal override returns (bytes32) {
// There are no legacy tokens present on L1.
return bytes32(0);
}

// get the computed address before the contract DeployWithCreate2 deployed using Bytecode of contract DeployWithCreate2 and salt specified by the sender
function calculateCreate2TokenAddress(
uint256 _originChainId,
Expand Down
36 changes: 32 additions & 4 deletions l1-contracts/contracts/bridge/ntv/L2NativeTokenVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {L2ContractHelper, IContractDeployer} from "../../common/libraries/L2Cont
import {SystemContractsCaller} from "../../common/libraries/SystemContractsCaller.sol";
import {DataEncoding} from "../../common/libraries/DataEncoding.sol";

import {NoLegacySharedBridge, TokenIsLegacy, TokenIsNotLegacy, EmptyAddress, EmptyBytes32, AddressMismatch, DeployFailed, AssetIdNotSupported} from "../../common/L1ContractErrors.sol";
import {AssetIdAlreadyRegistered, NoLegacySharedBridge, TokenIsLegacy, TokenIsNotLegacy, EmptyAddress, EmptyBytes32, AddressMismatch, DeployFailed, AssetIdNotSupported} from "../../common/L1ContractErrors.sol";

/// @author Matter Labs
/// @custom:security-contact security@matterlabs.dev
Expand Down Expand Up @@ -84,17 +84,45 @@ contract L2NativeTokenVault is IL2NativeTokenVault, NativeTokenVault {
}
}

function _registerTokenIfBridgedLegacy(address _tokenAddress) internal override returns (bytes32) {
// In zkEVM immutables are stored in a storage of a system contract,
// so it makes sense to cache them for efficiency.
IL2SharedBridgeLegacy legacyBridge = L2_LEGACY_SHARED_BRIDGE;
if (address(legacyBridge) == address(0)) {
// No legacy bridge, the token must be native
return bytes32(0);
}

address l1TokenAddress = legacyBridge.l1TokenAddress(_tokenAddress);
if (l1TokenAddress == address(0)) {
// The token is not legacy
return bytes32(0);
}

return _registerLegacyTokenAssetId(_tokenAddress, l1TokenAddress);
}

/// @notice Sets the legacy token asset ID for the given L2 token address.
function setLegacyTokenAssetId(address _l2TokenAddress) public {
if (assetId[_l2TokenAddress] != bytes32(0)) {
revert AssetIdAlreadyRegistered();
}
if (address(L2_LEGACY_SHARED_BRIDGE) == address(0)) {
revert NoLegacySharedBridge();
}
address l1TokenAddress = L2_LEGACY_SHARED_BRIDGE.l1TokenAddress(_l2TokenAddress);
if (l1TokenAddress == address(0)) {
revert TokenIsNotLegacy();
}
bytes32 newAssetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, l1TokenAddress);

_registerLegacyTokenAssetId(_l2TokenAddress, l1TokenAddress);
}

function _registerLegacyTokenAssetId(
address _l2TokenAddress,
address _l1TokenAddress
) internal returns (bytes32 newAssetId) {
newAssetId = DataEncoding.encodeNTVAssetId(L1_CHAIN_ID, _l1TokenAddress);
IL2AssetRouter(L2_ASSET_ROUTER_ADDR).setLegacyTokenAssetHandler(newAssetId);
tokenAddress[newAssetId] = _l2TokenAddress;
assetId[_l2TokenAddress] = newAssetId;
Expand Down Expand Up @@ -253,15 +281,15 @@ contract L2NativeTokenVault is IL2NativeTokenVault, NativeTokenVault {
// on L2s we don't track the balance
}

function _registerToken(address _nativeToken) internal override {
function _registerToken(address _nativeToken) internal override returns (bytes32) {
if (
address(L2_LEGACY_SHARED_BRIDGE) != address(0) &&
L2_LEGACY_SHARED_BRIDGE.l1TokenAddress(_nativeToken) != address(0)
) {
// Legacy tokens should be registered via `setLegacyTokenAssetId`.
revert TokenIsLegacy();
}
super._registerToken(_nativeToken);
return super._registerToken(_nativeToken);
}

/*//////////////////////////////////////////////////////////////
Expand Down
Loading

0 comments on commit 502ee80

Please sign in to comment.