Rayls Docs

RaylsErc20Handler

Definition

In Rayls SDK, apart from what you can build using the base contract RaylsApp, we offer other contracts to complement and ease your implementations. Here, you will see the handler for Erc20, which inherits from RaylsApp — to become integrated to Rayls — and also, of course, from Erc20.

Methods

If you wish to explore the contract on your own, it can be found in the section below. Here we'll highlight only some of the main methods.

Constructor

abstract contract RaylsErc20Handler is RaylsApp, ERC20, Initializable, Ownable {
    string tokenName;
    string tokenSymbol;
    mapping(address => uint256) lockedAmount;
    /**
     * @dev Constructor to initialize the RaylsCore with the provided endpoint and owner.
     * @param _endpoint The address of the Rayls endpoint.
     * @param _owner The address of the owner of the RaylsCore.
     */

    constructor(string memory _name, string memory _symbol, address _endpoint, address _owner)
        ERC20(_name, _symbol)
        RaylsApp(_endpoint)
        Ownable(_owner)
    {
        tokenName = _name;
        tokenSymbol = _symbol;
        _disableInitializers();
    }
  
  	// ...
}

As we can see, we have the standard _name and _symbol to represent the ERC20 in the constructor, as well as the _endpoint for the RaylsApp and an address to set as the _owner

initialize

function initialize(string memory _name, string memory _symbol, uint8 decimals) public initializer {
    address _owner = _getOwnerAddressOnInitialize();
    address _endpoint = _getEndpointAddressOnInitialize();
    resourceId = _getResourceIdOnInitialize();
    // ERC20 initizalization
    tokenName = _name;
    tokenSymbol = _symbol;
    decimals = decimals;
    // RaylsApp Initialization
    _transferOwnership(_owner);
    endpoint = IRaylsEndpoint(_endpoint);
}

As this contract can be used by proxy contracts, we need to do the initializations in a standard methods, not on the constructor (otherwise the proxy contract state won't be updated properly).

mint & burn

/**
     * @dev Mint new tokens and submit an update to the CommitChain.
     * @param to The address to which the new tokens will be minted.
     * @param value The amount of tokens to mint.
*/
function mint(address to, uint256 value) public virtual onlyOwner {
    _mint(to, value);
    uint8 updateType = 1;
    _submitTokenUpdate(value, updateType);
}

/**
     * @dev Burn tokens and submit an update to the CommitChain.
     * @param from The address from which the tokens will be burned.
     * @param value The amount of tokens to burn.
*/
function burn(address from, uint256 value) public virtual onlyOwner {
    _burn(from, value);
    uint8 updateType = 0;
    _submitTokenUpdate(value, updateType);
}

Given we are on Rayls, if we want our token to be validated cross-chain, both mint & burn activities must be not only done inside the PL but also sent to the commit chain — that's what _submitTokenUpdate does.

_submitTokenUpdate

/**
     * @dev Submit a token balance update to the CommitChain.
     * @param amount The amount of tokens to update.
     * @param updateType The type of update (0 for burn, 1 for mint).
*/
function submitTokenUpdate(uint256 amount, uint8 updateType) public virtual onlyOwner {
    _submitTokenUpdate(amount, updateType);
}

/**
     * @dev Internal function to submit a token balance update to the CommitChain. This function encodes a function call to the TokenRegistry contract.
     * @param amount The amount of tokens to update.
     * @param updateType The type of update (0 for burn, 1 for mint).
*/
function _submitTokenUpdate(uint256 amount, uint8 updateType) internal {
    _raylsSend(
        endpoint.getCommitChainId(),
        endpoint.getCommitChainAddress("TokenRegistry"),
        abi.encodeWithSignature(
            "updateTokenBalance(uint256,uint256,bytes32,uint8)",
            endpoint.getChainId(),
            amount,
            resourceId,
            updateType
        )
    );
}

Here we use a RaylsApp functionality, _raylsSend, to communicate cross-chain with the commit chain and inform the TokenRegistry that we are either minting or burning a certain amount of the token

sendTeleport

function sendTeleport(
    uint256 chainId,
    bytes memory _payload,
    bytes memory _lockDataPayload,
    bytes memory _revertDataPayloadSender,
    bytes memory _revertDataPayloadReceiver,
    BridgedTransferMetadata memory transferMetadata
) internal {
    require(resourceId != bytes32(0), "Token not registered.");

    _raylsSendToResourceId(
        chainId,
        resourceId,
        _payload,
        _lockDataPayload,
        _revertDataPayloadSender,
        _revertDataPayloadReceiver,
        transferMetadata
    );
}

The base of every following teleport method: It basically proxies the request to the RaylsApp method _raylsSendToResourceId, which sends a payload (including possible atomic revert mechanisms) cross-chain.

teleport

function teleport(address to, uint256 value, uint256 chainId) public virtual returns (bool) {
    _burn(msg.sender, value);

    BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
      assetType: RaylsBridgeableERC.ERC20,
      id: 0,
      from: msg.sender,
      to: to,
      amount: value
    });

    sendTeleport(
      chainId,
      abi.encodeWithSignature("receiveTeleport(address,uint256)", to, value),
      bytes(""),
      bytes(""),
      bytes(""),
      transferMetadata
    );

    return true;
}

A method to teleport (send cross-chain) an amount of the token. This is a vanilla teleport, which means there are no revert mechanisms, that is, it assumes the transaction will be a success.

teleportFrom

function teleportFrom(address from, address to, uint256 value, uint256 chainId) public virtual returns (bool) {
    address spender = _msgSender();
    _spendAllowance(from, spender, value);
    _burn(from, value);

    BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
      assetType: RaylsBridgeableERC.ERC20,
      id: 0,
      from: from,
      to: to,
      amount: value
    });

    sendTeleport(
      chainId,
      abi.encodeWithSignature("receiveTeleport(address,uint256)", to, value),
      bytes(""),
      bytes(""),
      bytes(""),
      transferMetadata
    );

    return true;
}

A method to teleport (send cross-chain) an amount of the token, but spending an allowance from other adress. This is a vanilla teleport, which means there are no revert mechanisms, that is, it assumes the transaction will be a success.

teleportAtomic

function teleportAtomic(address to, uint256 value, uint256 destinationChainId) public virtual returns (bool) {
    _burn(msg.sender, value);

    BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
      assetType: RaylsBridgeableERC.ERC20,
      id: 0,
      from: msg.sender,
      to: to,
      amount: value
    });

    sendTeleport(
      destinationChainId,
      abi.encodeWithSignature("receiveTeleportAtomic(address,uint256)", to, value),
      abi.encodeWithSignature("unlock(address,uint256)", to, value),
      abi.encodeWithSignature("revertTeleportMint(address,uint256)", msg.sender, value),
      abi.encodeWithSignature("revertTeleportBurn(address,uint256)", msg.sender, value),
      transferMetadata
    );
 
    return true;
}

A method to teleport (send cross-chain) an amount of the token. This is an atomic teleport, which means there are revert mechanisms, that is, if the transaction fails by some reason, it will be reverted.

teleportAtomicFrom

function teleportAtomicFrom(
  	address from, 
  	address to, 
  	uint256 value, 
  	uint256 destinationChainId
) public virtual returns (bool)
{
    address spender = _msgSender();
    _spendAllowance(from, spender, value);
    _burn(from, value);

    BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
      assetType: RaylsBridgeableERC.ERC20,
      id: 0,
      from: from,
      to: to,
      amount: value
    });

    sendTeleport(
      destinationChainId,
      abi.encodeWithSignature("receiveTeleportAtomic(address,uint256)", to, value),
      abi.encodeWithSignature("unlock(address,uint256)", to, value),
      abi.encodeWithSignature("revertTeleportMint(address,uint256)", from, value),
      abi.encodeWithSignature("revertTeleportBurn(address,uint256)", from, value),
      transferMetadata
    );
    return true;
}

A method to teleport (send cross-chain) an amount of the token, but spending an allowance from other adress. This is an atomic teleport, which means there are revert mechanisms, that is, if the transaction fails by some reason, it will be reverted.

Contract

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {RaylsApp} from "../RaylsApp.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "../interfaces/IRaylsEndpoint.sol";
import "../RaylsMessage.sol";
import "../libraries/Pedersen/Curve.sol";
import "../libraries/SharedObjects.sol";
import "../libraries/Utils.sol";

abstract contract RaylsErc20Handler is RaylsApp, ERC20, Initializable, Ownable {
    string tokenName;
    string tokenSymbol;
    mapping(address => uint256) lockedAmount;
    /**
     * @dev Constructor to initialize the RaylsCore with the provided endpoint and owner.
     * @param _endpoint The address of the Rayls endpoint.
     * @param _owner The address of the owner of the RaylsCore.
     */

    constructor(string memory _name, string memory _symbol, address _endpoint, address _owner)
        ERC20(_name, _symbol)
        RaylsApp(_endpoint)
        Ownable(_owner)
    {
        tokenName = _name;
        tokenSymbol = _symbol;
        _disableInitializers();
    }

    function initialize(string memory _name, string memory _symbol, uint8 decimals) public initializer {
        address _owner = _getOwnerAddressOnInitialize();
        address _endpoint = _getEndpointAddressOnInitialize();
        resourceId = _getResourceIdOnInitialize();
        // ERC20 initizalization
        tokenName = _name;
        tokenSymbol = _symbol;
        decimals = decimals;
        // RaylsApp Initialization
        _transferOwnership(_owner);
        endpoint = IRaylsEndpoint(_endpoint);
    }
    /**
     * @dev Returns the name of the token.
     * Overrided method to allow erc20 to be initializable
     */

    function name() public view override returns (string memory) {
        return tokenName;
    }

    /**
     * @dev Mint new tokens and submit an update to the CommitChain.
     * @param to The address to which the new tokens will be minted.
     * @param value The amount of tokens to mint.
     */
    function mint(address to, uint256 value) public virtual onlyOwner {
        _mint(to, value);
        uint8 updateType = 1;
        _submitTokenUpdate(value, updateType);
    }

    /**
     * @dev Burn tokens and submit an update to the CommitChain.
     * @param from The address from which the tokens will be burned.
     * @param value The amount of tokens to burn.
     */
    function burn(address from, uint256 value) public virtual onlyOwner {
        _burn(from, value);
        uint8 updateType = 0;
        _submitTokenUpdate(value, updateType);
    }

    /**
     * @dev Submit a token balance update to the CommitChain.
     * @param amount The amount of tokens to update.
     * @param updateType The type of update (0 for burn, 1 for mint).
     */
    function submitTokenUpdate(uint256 amount, uint8 updateType) public virtual onlyOwner {
        _submitTokenUpdate(amount, updateType);
    }

    /**
     * @dev Internal function to submit a token balance update to the CommitChain. This function encodes a function call to the TokenRegistry contract.
     * @param amount The amount of tokens to update.
     * @param updateType The type of update (0 for burn, 1 for mint).
     */
    function _submitTokenUpdate(uint256 amount, uint8 updateType) internal {
        _raylsSend(
            endpoint.getCommitChainId(),
            endpoint.getCommitChainAddress("TokenRegistry"),
            abi.encodeWithSignature(
                "updateTokenBalance(uint256,uint256,bytes32,uint8)",
                endpoint.getChainId(),
                amount,
                resourceId,
                updateType
            )
        );
    }
    /**
     * @dev Returns the symbol of the token, usually a shorter version of the
     * name.
     * Overrided method to allow erc20 to be initializable
     */

    function symbol() public view override returns (string memory) {
        return tokenSymbol;
    }

    function teleport(address to, uint256 value, uint256 chainId) public virtual returns (bool) {
        _burn(msg.sender, value);

        BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
            assetType: RaylsBridgeableERC.ERC20,
            id: 0,
            from: msg.sender,
            to: to,
            amount: value
        });

        sendTeleport(
            chainId,
            abi.encodeWithSignature("receiveTeleport(address,uint256)", to, value),
            bytes(""),
            bytes(""),
            bytes(""),
            transferMetadata
        );
        return true;
    }

    function teleportFrom(address from, address to, uint256 value, uint256 chainId) public virtual returns (bool) {
        address spender = _msgSender();
        _spendAllowance(from, spender, value);
        _burn(from, value);

        BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
            assetType: RaylsBridgeableERC.ERC20,
            id: 0,
            from: from,
            to: to,
            amount: value
        });

        sendTeleport(
            chainId,
            abi.encodeWithSignature("receiveTeleport(address,uint256)", to, value),
            bytes(""),
            bytes(""),
            bytes(""),
            transferMetadata
        );

        return true;
    }

    function teleportAtomic(address to, uint256 value, uint256 destinationChainId) public virtual returns (bool) {
        _burn(msg.sender, value);

        BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
            assetType: RaylsBridgeableERC.ERC20,
            id: 0,
            from: msg.sender,
            to: to,
            amount: value
        });

        sendTeleport(
            destinationChainId,
            abi.encodeWithSignature("receiveTeleportAtomic(address,uint256)", to, value),
            abi.encodeWithSignature("unlock(address,uint256)", to, value),
            abi.encodeWithSignature("revertTeleportMint(address,uint256)", msg.sender, value),
            abi.encodeWithSignature("revertTeleportBurn(address,uint256)", msg.sender, value),
            transferMetadata
        );
        return true;
    }

    function teleportAtomicFrom(address from, address to, uint256 value, uint256 destinationChainId)
        public
        virtual
        returns (bool)
    {
        address spender = _msgSender();
        _spendAllowance(from, spender, value);
        _burn(from, value);

        BridgedTransferMetadata memory transferMetadata = BridgedTransferMetadata({
            assetType: RaylsBridgeableERC.ERC20,
            id: 0,
            from: from,
            to: to,
            amount: value
        });

        sendTeleport(
            destinationChainId,
            abi.encodeWithSignature("receiveTeleportAtomic(address,uint256)", to, value),
            abi.encodeWithSignature("unlock(address,uint256)", to, value),
            abi.encodeWithSignature("revertTeleportMint(address,uint256)", from, value),
            abi.encodeWithSignature("revertTeleportBurn(address,uint256)", from, value),
            transferMetadata
        );
        return true;
    }

    function receiveTeleport(address to, uint256 value) public virtual receiveMethod {
        _mint(to, value);
    }

    function receiveTeleportAtomic(address to, uint256 value) public virtual receiveMethod {
        _mint(owner(), value);
        if (to != owner()) {
            _lock(to, value);
        }
    }

    function revertTeleportMint(address to, uint256 value) public virtual receiveMethod {
        _mint(to, value);
    }

    function revertTeleportBurn(address to, uint256 value) public virtual receiveMethod {
        _burn(to, value);
    }

    function receiveResourceId(bytes32 _resourceId) public virtual receiveMethod onlyFromCommitChain {
        resourceId = _resourceId;
        _registerResourceId();
    }

    function submitTokenRegistration() public virtual {
        _raylsSend(
            endpoint.getCommitChainId(),
            endpoint.getCommitChainAddress("TokenRegistry"),
            abi.encodeWithSignature(
                "addToken((string,string,uint256,uint256,bytes,bytes,bool,uint8))",
                SharedObjects.TokenRegistrationData({
                    name: name(),
                    symbol: symbol(),
                    totalSupply: totalSupply(),
                    issuerChainId: endpoint.getChainId(),
                    bytecode: address(this).code,
                    initializerParams: _generateInitializerParams(),
                    isFungible: true,
                    ercStandard: SharedObjects.ErcStandard.ERC20
                })
            )
        );
    }

    function _generateInitializerParams() internal view returns (bytes memory) {
        return abi.encodeWithSignature("initialize(string,string,uint8)", tokenName, tokenSymbol, decimals());
    }

    function _commitBalance(address from) internal {
        uint256 balance = balanceOf(from);
        (uint256 x, uint256 y) = Curve.derivePubKey(balance);
        _raylsSend(
            endpoint.getCommitChainId(),
            endpoint.getCommitChainAddress("BalanceCommitment"),
            abi.encodeWithSignature(
                "commitBalance(uint256,address,bytes32,uint256,uint256)",
                endpoint.getChainId(),
                address(this),
                Utils.addressToBytes32(from),
                x,
                y
            )
        );
    }
    /**
     * @notice Unlocks the locked funds and calls transfer
     * @param to Address of a "to" account to unlock the funds to
     * @param amount Amount of tokens to unlock
     */

    function unlock(address to, uint256 amount) external returns (bool) {
        if (to != owner()) {
            bool success = _unlock(to, amount);
            require(success, "cannot unlock the assets");
            _transfer(owner(), to, amount);
            return true;
        }
        return true;
    }

    function _lock(address to, uint256 amount) internal {
        require(amount > 0, "Amount must be greater than 0");
        require(to != address(0));
        lockedAmount[to] += amount;
    }

    function _unlock(address to, uint256 amount) internal returns (bool) {
        require(to != address(0));

        uint256 amountToUnlock = lockedAmount[to];
        require(amount > 0 && amount <= amountToUnlock, "Not enough funds to unlock");

        lockedAmount[to] -= amount;

        return true;
    }

    function getLockedAmount(address account) public view returns (uint256) {
        return lockedAmount[account];
    }

    function sendTeleport(
        uint256 chainId,
        bytes memory _payload,
        bytes memory _lockDataPayload,
        bytes memory _revertDataPayloadSender,
        bytes memory _revertDataPayloadReceiver,
        BridgedTransferMetadata memory transferMetadata
    ) internal {
        require(resourceId != bytes32(0), "Token not registered.");

        _raylsSendToResourceId(
            chainId,
            resourceId,
            _payload,
            _lockDataPayload,
            _revertDataPayloadSender,
            _revertDataPayloadReceiver,
            transferMetadata
        );
    }

    function _update(address from, address to, uint256 value) internal override {
        super._update(from, to, value); // calls oz implementation, if it works there's no revert
        if (from != address(0)) {
            _commitBalance(from);
        }
        if (to != address(0)) {
            _commitBalance(to);
        }
    }
}