Skip to content

Instantly share code, notes, and snippets.

@itzmeanjan
Last active July 6, 2021 06:43
Show Gist options
  • Save itzmeanjan/02b18ed506c5fb0788992009308a3c5f to your computer and use it in GitHub Desktop.
Save itzmeanjan/02b18ed506c5fb0788992009308a3c5f to your computer and use it in GitHub Desktop.
An illustration of sending data from Ethereum root chain to Matic child chain
// File: contracts/child/ChildToken/ChildERC20.sol
pragma solidity 0.6.6;
contract ChildERC20 is
ERC20,
IChildToken,
AccessControlMixin,
NativeMetaTransaction,
ChainConstants,
ContextMixin
{
bytes32 public constant DEPOSITOR_ROLE = keccak256("DEPOSITOR_ROLE");
constructor(
string memory name_,
string memory symbol_,
uint8 decimals_,
address childChainManager
) public ERC20(name_, symbol_) {
_setupContractId("ChildERC20");
_setupDecimals(decimals_);
_setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
_setupRole(DEPOSITOR_ROLE, childChainManager);
_initializeEIP712(name_, ERC712_VERSION);
}
// This is to support Native meta transactions
// never use msg.sender directly, use _msgSender() instead
function _msgSender()
internal
override
view
returns (address payable sender)
{
return ContextMixin.msgSender();
}
/**
* @notice called when token is deposited on root chain
* @dev Should be callable only by ChildChainManager
* Should handle deposit by minting the required amount for user
* Make sure minting is done only by this function
* @param user user address for whom deposit is being done
* @param depositData abi encoded amount
*/
function deposit(address user, bytes calldata depositData)
external
override
only(DEPOSITOR_ROLE)
{
uint256 amount = abi.decode(depositData, (uint256));
// tokens minted on child chain for `user`
_mint(user, amount);
// this is not the only way of doing it, using low level call here
// this might be little overengineering for this given scenario
(bool success, bytes memory result) = address(this).call(abi.encodeWithSignature("callback(address,uint256)", user, amount));
// or as we know function to be called in this contract, we can do another thing
// given `callback` function is predefined for child chain contract
// where we can also emit some event which can be caught, offchain
callback(user, amount);
}
function callback(address user, uint256 amount) internal {
// doing nothing useful here, as of now
// but you can always implement custom logic here, which is to be executed
// whenever any token deposit is performed from root to child
}
/**
* @notice called when user wants to withdraw tokens back to root chain
* @dev Should burn user's tokens. This transaction will be verified when exiting on root chain
* @param amount amount of tokens to withdraw
*/
function withdraw(uint256 amount) external {
_burn(_msgSender(), amount);
}
}
@itzmeanjan
Copy link
Author

itzmeanjan commented Sep 18, 2020

Just token amount & deposit for address

Please check deposit method implementation for ChildERC20.sol.

This can facilitate calling some callback function in same contract i.e. child token on Matic, which can be done any of following two ways

  • Directly call that method, because it's defined in that contract
  • Or go other way, by calling callback function using low-level address(this).call(...) : which might be little overkill given this scenario.

More Data

Or if you want to send some more data from main chain contract to child chain, you do following

  • [ Deposit Token ] First deposit token for user by calling depositFor in RootChainManager
function depositFor(address user, address rootToken, bytes calldata depositData) external override {
      // implementation goes here
}
function syncState(address receiver, bytes calldata data) external {}
  • [ Send more data ] Now I'm assuming you're interested in sending more data to your target contract, or lets say any arbitrary message, then we're going to call again StateSender.syncState | this, but this time make sure, you're passing proper target contract address on child chain i.e. Matic, which will be invoked by our chain syncer's internally, so make sure you don't use any kind of event emission there. We call this kind of function calls system-calls, because they get called by our heimdal layer state syncer nodes.
function functionInContractOnRootChain(address rootChainManager, address user, address rootToken,
    bytes calldata depositData, address stateSender, 
    address targetContractOnChildChain, bytes calldata moreData) external {
     // this is where we're going to call deposit on root chain manager
     RootChainManager(rootChainManager).depositFor(user, rootToken, depositData);

     // then we can consider sending more data to child chain by calling `syncState` on stateSender 
     // targetContractOnChildChain : address of DataReceiver on Matic, check below
     StateSender(stateSender).syncState(targetContractOnChildChain, moreData);
}
  • [ Send more data ] You can check this implementation of StateSender, deployed on base chain i.e. Ethereum, which emits StateSynced event, that are tracked by our heimdal layer nodes.
  • [ Send more data ] So finally your call has arrived to target contract in child chain, where now you can decode you byte data, using abi.decode. Now you can do what you want to do with this synced message from Ethreum to Matic.

But wait, how does the heimdal layer state syncer get to know which function on target contract to be specified ?

  • Well looks like we need to implement certain function, in target contract on child chain, so that heimdal layer's syncer gets to know which one call.

Yes, we need to implement this interface, where onStateReceived must be implemented.

pragma solidity ^0.7.1;

contract DataReceiver is IChildChainManager {
   function onStateReceived(uint256, bytes calldata data) external override only(STATE_SYNCER_ROLE) { 
        (bytes32 syncType, bytes memory syncData) = abi.decode(data, (bytes32, bytes));
        // syncData is of our interest, this is what was passed to `StateSender.syncState` above
        // now decode it & process it here
   }

   function mapToken(address rootToken, address childToken) external override {
       // skipping it as it doesn't concern us now
   }

}

So heimdal layer syncer will call this function on address specified in syncState(address receiver, bytes calldata data), which you called on root chain.

Now in your child contract, in onStateReceived you can put custom logic. Take some inspiration from ChildChainManager.

Now it's upto you what you do with synced data, which you just sent form Ethereum to Matic.

Was I clear ?

Let me know 😉

@hack3r-0m
Copy link

hack3r-0m commented Mar 21, 2021

@itzmeanjan is it onStateReceived or onStateReceive (without d) or both works? because I have seen few contracts on matic-pos-portal with the latter one

@mahendran216
Copy link

What is the use of that function _msgSender() in line 30??
Can someone please explain the working of that function??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment