Connector Over Native Currency

The main idea of this connector is to have a possibility to bridge native currency between chains.

Create contract with name UTSConnectorNativeShowcase .

You need to import:

import "@openzeppelin/contracts/access/Ownable.sol";
import "@entangle-labs/uts-contracts/contracts/ERC20/UTSBase";

UTSBase is our main contract that should be inherited by any Token or Connector.

In this implementation we also used Ownable

contract UTSConnectorNativeShowcase is UTSBase, Ownable {}

We are starting with defining contract and dependencies. As access control we are choosing Ownable.

Also we need to define NATIVE_TRANSFER_GAS_LIMIT some errors and address converter library.

using AddressConverter for address;

uint256 public immutable NATIVE_TRANSFER_GAS_LIMIT;

error UTSConnectorNativeShowcase__E0();

error UTSConnectorNativeShowcase__E1();

error UTSConnectorNativeShowcase__E2(bytes);

constructor(
    address _router,  
    uint256[] memory _allowedChainIds,
    ChainConfig[] memory _chainConfigs,
    uint8 _nativeCurrencyDecimals,
    uint256 _nativeTransferGasLimit
) Ownable(msg.sender) {
    __UTSBase_init(address(0), _nativeCurrencyDecimals);

    _setRouter(_router);
    _setChainConfig(_allowedChainIds, _chainConfigs);

    NATIVE_TRANSFER_GAS_LIMIT = _nativeTransferGasLimit;
}

Then we need do define constructor. Since it is connector, we need _nativeCurrencyDecimals( since our asset for bridging is native currency, we can provide a null address, but meaningful decimals value still required). We need also setup _router address, that can be found here.

_allowedChainIds are simply whitelist of chain id's, where you are allowing to bridge tokens.

_chainConfigs is array of settings responsible for bridge settings. We described this config here.

In constructor we need to call __UTSBase_init function to initialize UTS Base contract. Also we need to set router and chain config.

_nativeTransferGasLimit serves for defend your and your users gas to avoid attack vector over too complicated or useless receive function at the receiver address.

We can skip setting router and chain configs in constructor, but before starting bridging we need to do it with relevant public functions.

Lets start with defining 2 view functions that will help us to track balance of this connector and also decimals of native currency and also receive function that will allow our connector to receive native currency.

receive() external payable {}

function underlyingDecimals() external view returns(uint8) {
    return _decimals;
}

function underlyingBalance() external view returns(uint256) {
    return address(this).balance;
}

After this step we need to override 3 functions: _mintTo, _burnFrom and _authorizeCall.

Since this connector is working via lock/unlock scheme, you need to fulfill it, to start bridging to this connector. This can be done or via just transferring native to connector, or you can initially bridge your native from this connector to any chain where you have UTS token or connector with non zero balance and initial funds will be locked in connector.

function _authorizeCall() internal override onlyOwner() {}
function _burnFrom(
    address /* spender */,
    address /* from */, 
    bytes memory /* to */, 
    uint256 amount, 
    uint256 /* dstChainId */, 
    bytes memory /* customPayload */
) internal override returns(uint256 bridgedNativeAmount) {
    if (amount >= msg.value) revert UTSConnectorNativeShowcase__E0();

    return amount;
}
function _mintTo(
    address to,
    uint256 amount,
    bytes memory /* customPayload */,
    Origin memory /* origin */
) internal override returns(uint256 receivedNativeAmount) {
    if (amount > address(this).balance) revert UTSConnectorNativeShowcase__E1();

    (bool _success, bytes memory _response) = to.call{value: amount, gas: NATIVE_TRANSFER_GAS_LIMIT}("");

    if (!_success) revert UTSConnectorNativeShowcase__E2(_response);

    return amount;
}

In the outbounding transaction we need to ensure that msg.value we are going to accept is greater than amount, to ensure that we received enough native currency.

In the inbounding transaction we need to ensure that we have enough native currency on connector balance and as a transfer method we will use raw call, since we want also provide restricted gas value.

Don't forget to check success.

Last but not least we need to override one our internal function that is responsible for sending request to router for bridge proposal. Here we need modify value of this message. Main fee asset in our protocol is native currency, and we need to transfer some fee to router to pay fee. If we will leave as it is, it will transfer whole msg.value to router. It is incorrect logic in this case, so we need to substruct our amount that will be locked on the connector balance.

function _sendRequest(
    uint256 payment,
    bytes memory dstToken,
    bytes memory to,
    uint256 amount,
    uint8 srcDecimals,
    uint256 dstChainId,
    uint64 dstGasLimit,
    bytes memory customPayload,
    bytes memory protocolPayload
) internal override returns(bool success) {
    return IUTSRouter(router()).bridge{value: payment - amount}( 
        dstToken,
        msg.sender.toBytes(),
        to,
        amount,
        srcDecimals,
        dstChainId,
        dstGasLimit,
        customPayload,
        protocolPayload
    );
}

So, our contract is ready, now it can be deployed on networks and we can start bridging tokens between each chain.

You can find full code here

Last updated