LogoLogo
  • Entangle
    • Overview
    • Security Audits
  • Universal Interoperability Protocol
    • Overview
    • Architecture
      • Scalability and Network Stability
        • L2 Utility Blockchains
        • Transmitter Groups
      • Security and Consensus Mechanism
      • Finality
      • Execution Latency
      • Compatibility and Interoperability
    • Developer Guides
      • Getting Started
      • Solidity
        • Simple Abstract Messenger Example
        • Deploying Your Custom EVM Protocol
        • Bridging Tokens with UIP
        • Become an EVM Transmitter
      • Solana
        • Simple Abstract Messenger Example
        • Deploying Your Custom Solana Protocol
        • Become a Solana Transmitter
      • Calculate Cross-Chain Transaction Cost
      • Customizable Message Transmission Options
      • How to Debug Sent Messages
      • SDK Setup
      • Revenue Sharing for Transmitters
      • How to Become a Super Transmitter
    • Endpoints
  • Universal Data Feeds
    • Overview
    • Architecture
      • Data Delivery Methods
        • Pull Model
        • Push Model
      • Oracle Contract & User Interaction
    • Developer Guides
      • Custom Data Feeds
      • Fetch Data via Pull Model (PAYG)
        • EVM Smart Contracts
        • Solana Smart Contracts
      • Fetch Data via Pull Model (Subscriptions)
        • EVM Smart Contracts
        • Solana Smart Contracts
      • Fetch Data via Push Model
        • EVM Smart Contracts
        • Solana Smart Contracts
    • User Guides
      • Accessing Feeds
      • Subscribe to a Data Feed
      • Check Subscription
      • Manage Subscription
      • Renew Subscription
    • Data Endpoints
  • Universal Token Standard
    • Overview
    • Architecture
      • Fee Components
    • Developer Guides
      • Manual Deploy
        • Token Deployment Types
        • Create a Custom Token
        • Factory Blueprint Deployment
        • Examples
          • Initial Setup
          • UTS Connector
            • Mint & Burn Connector Scheme
            • Lock & Unlock Connector Scheme
            • Connector Over Native Currency
          • UTS Token
            • Simple Token
            • Token with Messages
      • Bridge SDK
      • Token Verification
      • Fees Calculation & Gas Estimation Logic
      • Estimations
    • User Guides
      • Launch Universal Token
      • Create a Liquidity Pool
      • Expand Existing Token
      • Transfer Liquidity to Connector
      • Bridging
    • Contract Addresses
  • Entangle Interoperable Blockchain
    • Overview
    • Architecture
    • Developer Guides
      • Set up a Validator Node
      • Delegating to Validators
      • Undelegating from Validators
      • Supported Accounts
  • More
    • Media Kit
    • FAQ
    • Report an Issue
    • Become a Partner
Powered by GitBook
On this page

Was this helpful?

Export as PDF
  1. Universal Token Standard
  2. Developer Guides
  3. Manual Deploy
  4. Examples
  5. UTS Token

Token with Messages

Create contract with name UTSTokenWithLogicShowcase .

You need to import:

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

We start with defining the contract and the dependencies. For the access control we choose Ownable and also since it is UTS Token we need to import ERC20 implementation, here we are using one by openzeppelin.

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

In this implementation we also used Ownable and ERC20.

contract UTSTokenWithLogicShowcase is UTSBase, Ownable, ERC20 {}
constructor(
    address _router,  
    uint256[] memory _allowedChainIds,
    ChainConfig[] memory _chainConfigs
) Ownable(msg.sender) ERC20("UTS Token With Logic Showcase", "TWL") {
    __UTSBase_init(address(this), decimals());

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

    _mint(msg.sender, 1_000_000 * 10 ** decimals());
}

Then we need do define constructor. Since it is Token, we don't need a lot, just general UTS settings and ERC20 metadata.

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

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

Also we added mint function to premint some tokens for deployer address.

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

Also let's define some events to have a possibility to debug and track transactions and also some errors and library.

using AddressConverter for bytes;

error UTSTokenWithLogicShowcase__E0();

error UTSTokenWithLogicShowcase__E1();

error UTSTokenWithLogicShowcase__E2();

event MultiBridged(
    address indexed spender, 
    address from, 
    bytes indexed dstPeerAddressIndexed, 
    bytes dstPeerAddress,
    bytes[] receivers, 
    uint256[] amounts,
    uint256 indexed dstChainId
);

event MultiRedeemed(
    address[] indexed receiversIndexed, 
    address[] receivers,
    uint256[] amounts, 
    bytes indexed srcPeerAddressIndexed, 
    bytes srcPeerAddress,
    uint256 indexed srcChainId,
    bytes sender
);

After this step we need to override 3 functions: _mintTo, _burnFrom and _authorizeCall. We need to do it, because this token is mint/burn, so, internal logic should be of main bridging function should be done via mint/burn of underlying token.

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 bridgedAmount) {
    if (from != spender) _spendAllowance(from, spender, amount);

    _update(from, address(0), amount);

    // This is just an example of how multi-bridge can be implemented.
    if (customPayload.length > 0) {
        // {address} type array can be used here if the token will exist only in EVM-compatible chains.
        (bytes[] memory _receivers, uint256[] memory _amounts) = abi.decode(customPayload, (bytes[], uint256[]));

        // Some basic checks are added here:
        //     1. the {_receivers} and {_amounts} arrays lengths are the same
        //     2. the {_receivers} addresses are not empty
        //     3. the sum of {_amounts} is equal to {amount}

        if (_receivers.length != _amounts.length) revert UTSTokenWithLogicShowcase__E0();

        uint256 _amountsSum;

        for (uint256 i; _receivers.length > i; ++i) {
            if (_receivers[i].length == 0) revert UTSTokenWithLogicShowcase__E1();

            _amountsSum += _amounts[i];
        }

        if (amount != _amountsSum) revert UTSTokenWithLogicShowcase__E2();

        ChainConfig memory config = _chainConfig[dstChainId];

        emit MultiBridged(spender, from, config.peerAddress, config.peerAddress, _receivers, _amounts, dstChainId);
    }

    return amount;
}

In this implementation, we use customPayload as an option to transfer tokens to multiple receivers in different amounts just by one transaction. To do that, we define rules for how the raw bytes passed in customPayload should be encoded. In the _burnFrom function, we burn the tokens from the token holder in the provided amount and implement basic validations of the customPayload, if it is provided.

function _mintTo(
    address to,
    uint256 amount,
    bytes memory customPayload,
    Origin memory origin
) internal override returns(uint256 receivedAmount) {
    if (customPayload.length > 0) {
        (bytes[] memory _receiversInBytes, uint256[] memory _amounts) = abi.decode(customPayload, (bytes[], uint256[]));

        address[] memory _receivers = new address[](_receiversInBytes.length);

        for (uint256 i; _receiversInBytes.length > i; ++i) {
            _receivers[i] = _receiversInBytes[i].toAddress();
            _update(address(0), _receivers[i], _amounts[i]);
        }

        emit MultiRedeemed(
            _receivers, 
            _receivers,
            _amounts, 
            origin.peerAddress, 
            origin.peerAddress,
            origin.chainId,
            origin.sender
        );

    } else {
        _update(address(0), to, amount);
    }

    return amount;
}

Also, it is necessary to implement tokens minting to the receiver or multiple receivers in the _mintTo function. Both possible scenarios need to be handled depending on whether a meaningful customPayload was provided. In this case, the raw bytes should be decoded, and the corresponding token _amounts should be minted to _receivers.

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

PreviousSimple TokenNextBridge SDK

Last updated 4 months ago

Was this helpful?

We need setup _router address, that can be found .

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

You can find full code

here
here
here