The development of a new chain mainly involves developing cross-chain modules. In chains supporting EVM, the cross-chain module works as a set of smart contracts.
In some cases, it can also work as a native module.
To help you develop it, here Poly Network team offers examples in Solidity to develop smart contract for each main method. You may refer to the complete code of these contracts.
[!Note|style:flat|label:Notice] If the chain integrated to Poly Network supports EVM, you can freely use Poly cross-chain contracts as templates. If not, you may need to develop your own contracts containing the main features, as shown in the following guidelines.
In general, cross-chain contracts are sorted into logic, data, and proxy contracts. You could either follow the methods listed below or choose other ways for your project.
-
List of contracts:
-
Cross Chain Manager Contract: It serves as an agent to manage the cross-chain transaction both on the source chain and the target chain. It may be referred to as CCM contract in the following context.
-
Cross Chain Data Contract: It serves as a database of cross-chain transactions. It may be referred to as CCD contract in the following context.
-
Cross Chain Manager Proxy Contract: It serves as a proxy of the CCM contract to update the CCM contracts stored in CCD contracts. It may be referred to as CCMP contract in the following context.
-
Business Logic Contract: It executes the business logic of cross-chain projects. It interacts with users and the CCM contract both on the source and target chains. We also offer the guidelines for developing a Business Logic Contract.
-
-
Interactions among contracts
The following four features are the core functions of CCM contracts and must be implemented.
This step is meant to implement the methods of synchronizing the genesis block header of the Poly chain to the CCM contract.
/* @notice Sync Poly chain genesis block header to smart contract
* @dev This function can only be called once; nextbookkeeper of rawHeader can't be empty
* @param rawHeader Poly chain genesis block raw header or raw Header including switching consensus peers info
* @return true or false
*/
function initGenesisBlock(bytes memory rawHeader, bytes memory pubKeyList) whenNotPaused public returns(bool) {
// Load Ethereum cross chain data contract
IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress);
// Make sure the contract has not been initialized before
require(eccd.getCurEpochConPubKeyBytes().length == 0, "EthCrossChainData contract has already been initialized!");
// Parse header and convit the public keys into nextBookKeeper and compare it with header.nextBookKeeper to verify the validity of signature
ECCUtils.Header memory header = ECCUtils.deserializeHeader(rawHeader);
(bytes20 nextBookKeeper, address[] memory keepers) = ECCUtils.verifyPubkey(pubKeyList);
require(header.nextBookkeeper == nextBookKeeper, "NextBookers illegal");
// Record current epoch start height and public keys (by storing them in address format)
require(eccd.putCurEpochStartHeight(header.height), "Save Poly chain current epoch start height to Data contract failed!");
require(eccd.putCurEpochConPubKeyBytes(ECCUtils.serializeKeepers(keepers)), "Save Poly chain current epoch book keepers to Data contract failed!");
// Fire the event
emit InitGenesisBlockEvent(header.height, rawHeader);
return true;
}
- This method should be called initially and can only be called once. For the input data
rawHeader
, thenextbookkeeper
can not be empty. - Firstly, this function checks the public key of the current epoch to make sure that the CCM contract is uninitialized.
- Next, the raw header is parsed to get the
header.nextBookKeeper
. Comparing it with thenextBookKeeper
converted from pubKeyList, the validity of the signature can be verified. - After verifying the signature, we can record the current epoch start height and the public keys by storing them in the address format. And then emit the event
InitGenesisBlockEvent
.
This step is meant to implement the methods of changing the Poly Chain consensus validator, which is called BookKeeper
in the code.
/* @notice change Poly chain consensus bookkeeper
* @param rawHeader Poly chain change bookkeeper block raw header
* @param pubKeyList Poly chain consensus nodes public key list
* @param sigList Poly chain consensus nodes signature list
* @return true or false
*/
function changeBookKeeper(bytes memory rawHeader, bytes memory pubKeyList, bytes memory sigList) whenNotPaused public returns(bool) {
// Load Ethereum cross chain data contract
ECCUtils.Header memory header = ECCUtils.deserializeHeader(rawHeader);
IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress);
// Make sure rawHeader.height is higher than recorded current epoch start height
uint64 curEpochStartHeight = eccd.getCurEpochStartHeight();
require(header.height > curEpochStartHeight, "The height of header is lower than current epoch start height!");
// Ensure the rawHeader is the key header including info of switching consensus peers by containing non-empty nextBookKeeper field
require(header.nextBookkeeper != bytes20(0), "The nextBookKeeper of header is empty");
// Verify signature of rawHeader comes from pubKeyList
address[] memory polyChainBKs = ECCUtils.deserializeKeepers(eccd.getCurEpochConPubKeyBytes());
uint n = polyChainBKs.length;
require(ECCUtils.verifySig(rawHeader, sigList, polyChainBKs, n - (n - 1) / 3), "Verify signature failed!");
// Convert pubKeyList into Ethereum address format and make sure the compound address from the converted Ethereum addresses
// equals passed in header.nextBookKeeper
(bytes20 nextBookKeeper, address[] memory keepers) = ECCUtils.verifyPubkey(pubKeyList);
require(header.nextBookkeeper == nextBookKeeper, "NextBookers illegal");
// update current epoch start height of Poly chain and current epoch consensus peers book keepers addresses
require(eccd.putCurEpochStartHeight(header.height), "Save MC LatestHeight to Data contract failed!");
require(eccd.putCurEpochConPubKeyBytes(ECCUtils.serializeKeepers(keepers)), "Save Poly chain book keepers bytes to Data contract failed!");
// Fire the change book keeper event
emit ChangeBookKeeperEvent(header.height, rawHeader);
return true;
}
- Firstly, you need to make sure the
rawHeader.height
is higher than the recorded current epoch starts height. - Then you need to ensure that the
rawHeader
is the key header, including info of switching consensus peers by containing a non-emptynextBookKeeper
field. - Analogous to
initGenesisBlock()
, we also need to parse the raw header to get theheader.nextBookKeeper
. Comparing it with thenextBookKeeper
converted from pubKeyList, we can verify the validity of the signature. - After verifying the signature, we can record the current epoch start height and current epoch consensus peers bookkeepers by storing them in the address format. And then emit the event
ChangeBookKeeperEvent
.
This step is meant to implement the methods of pushing the serialized cross-chain transaction information to the Poly chain.
/*
* @param toChainId The target chain id
* @param toAddress The address in bytes format to receive the same amount of tokens in the target chain
* @param toContract Target smart contract address in bytes in the target blockchain
* @param txData Transaction data for target chain, include toAssetHash, toAddress, amount
* @return true or false
*/
function crossChain(uint64 toChainId, bytes calldata toContract, bytes calldata method, bytes calldata txData) whenNotPaused external returns (bool) {
// Only allow whitelist contract to call
require(whiteListFromContract[msg.sender],"Invalid from contract");
// Load Ethereum cross chain data contract
IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress);
// To help differentiate two txs, the ethTxHashIndex is increasing automatically
uint256 txHashIndex = eccd.getEthTxHashIndex();
// Convert the uint256 into bytes
bytes memory paramTxHash = Utils.uint256ToBytes(txHashIndex);
// Construct the makeTxParam, and put the hash info storage, proving tx existence
bytes memory rawParam = abi.encodePacked(ZeroCopySink.WriteVarBytes(paramTxHash),
ZeroCopySink.WriteVarBytes(abi.encodePacked(sha256(abi.encodePacked(address(this), paramTxHash)))),
ZeroCopySink.WriteVarBytes(Utils.addressToBytes(msg.sender)),
ZeroCopySink.WriteUint64(toChainId),
ZeroCopySink.WriteVarBytes(toContract),
ZeroCopySink.WriteVarBytes(method),
ZeroCopySink.WriteVarBytes(txData)
);
// Must save it in the storage to be included in the proof to be verified.
require(eccd.putEthTxHash(keccak256(rawParam)), "Save ethTxHash by index to Data contract failed!");
// Fire the cross chain event denoting there is a cross chain request from Ethereum network to other public chains through Poly chain network
emit CrossChainEvent(tx.origin, paramTxHash, msg.sender, toChainId, toContract, rawParam);
return true;
}
- Only the contracts in the whitelist can call this method.
- It creates cross chain transactions, invoked by service contracts when a cross chain function is carried out in the logic contract.
- This method constructs the
rawParam
, which contains transaction hash,msg.sender
, target chain ID, business logic contract to be invoked on the target chain, the target method to be invoked, and the serialized transaction data which has been already constructed in the business logic contract. - Then the hash of
rawParam
is put into storage, proving the existence of the transaction.
This step is meant to implement the methods of verifying the block header and Merkle proof. If this verification is passed, the transaction can be executed on the target chain.
/*
* @param proof Poly chain transaction Merkle proof
* @param rawHeader The header containing crossStateRoot to verify the above tx Merkle proof
* @param headerProof The header Merkle proof used to verify rawHeader
* @param curRawHeader Any header in current epoch consensus of Poly chain
* @param headerSig The converted signature variable for solidity derived from Poly chain consensus nodes' signature
* used to verify the validity of curRawHeader
* @return true or false
*/
function verifyHeaderAndExecuteTx(bytes memory proof, bytes memory rawHeader, bytes memory headerProof, bytes memory curRawHeader,bytes memory headerSig) whenNotPaused public returns (bool){
ECCUtils.Header memory header = ECCUtils.deserializeHeader(rawHeader);
// Load ehereum cross chain data contract
IEthCrossChainData eccd = IEthCrossChainData(EthCrossChainDataAddress);
// Get stored consensus public key bytes of current Poly chain epoch and deserialize Poly chain consensus public key bytes to address[]
address[] memory polyChainBKs = ECCUtils.deserializeKeepers(eccd.getCurEpochConPubKeyBytes());
uint256 curEpochStartHeight = eccd.getCurEpochStartHeight();
uint n = polyChainBKs.length;
if (header.height >= curEpochStartHeight) {
// It's enough to verify rawHeader signature
require(ECCUtils.verifySig(rawHeader, headerSig, polyChainBKs, n - ( n - 1) / 3), "Verify Poly chain header signature failed!");
} else {
// We need to verify the signature of curHeader
require(ECCUtils.verifySig(curRawHeader, headerSig, polyChainBKs, n - ( n - 1) / 3), "Verify Poly chain current epoch header signature failed!");
// Then use curHeader.StateRoot and headerProof to verify rawHeader.CrossStateRoot
ECCUtils.Header memory curHeader = ECCUtils.deserializeHeader(curRawHeader);
bytes memory proveValue = ECCUtils.MerkleProve(headerProof, curHeader.blockRoot);
require(ECCUtils.getHeaderHash(rawHeader) == Utils.bytesToBytes32(proveValue), "verify header proof failed!");
}
// Through rawHeader.CrossStatesRoot, the toMerkleValue or cross chain msg can be verified and parsed from proof
bytes memory toMerkleValueBs = ECCUtils.MerkleProve(proof, header.crossStatesRoot);
// Parse the toMerkleValue struct and make sure the tx has not been processed, then mark this tx as processed
ECCUtils.ToMerkleValue memory toMerkleValue = ECCUtils.deserializeMerkleValue(toMerkleValueBs);
require(!eccd.checkIfFromChainTxExist(toMerkleValue.fromChainID, Utils.bytesToBytes32(toMerkleValue.txHash)), "the transaction has been executed!");
require(eccd.markFromChainTxExist(toMerkleValue.fromChainID, Utils.bytesToBytes32(toMerkleValue.txHash)), "Save crosschain tx exist failed!");
// Ethereum ChainId is 2, we need to check the transaction is for Ethereum network
require(toMerkleValue.makeTxParam.toChainId == chainId, "This Tx is not aiming at this network!");
// Obtain the target contract, so that Ethereum cross chain manager contract can trigger the executation of cross chain tx on Ethereum side
address toContract = Utils.bytesToAddress(toMerkleValue.makeTxParam.toContract);
// only invoke PreWhiteListed Contract and method For Now
require(whiteListContractMethodMap[toContract][toMerkleValue.makeTxParam.method],"Invalid to contract or method");
//TODO: check this part to make sure we commit the next line when doing local net UT test
require(_executeCrossChainTx(toContract, toMerkleValue.makeTxParam.method, toMerkleValue.makeTxParam.args, toMerkleValue.makeTxParam.fromContract, toMerkleValue.fromChainID), "Execute CrossChain Tx failed!");
// Fire the cross chain event denoting the executation of cross chain tx is successful,
// and this tx is coming from other public chains to current Ethereum network
emit VerifyHeaderAndExecuteTxEvent(toMerkleValue.fromChainID, toMerkleValue.makeTxParam.toContract, toMerkleValue.txHash, toMerkleValue.makeTxParam.txHash);
return true;
}
/*
* @notice Dynamically invoke the target contract, trigger execution of cross-chain tx
on Ethereum side
* @param _toContract the Ethereum Cross Chain Manager contract will invoke the target contract
* @param _method At which method will be invoked within the target contract
* @param _args The parameter that will be passed into the target contract
* @param _fromContractAddr From chain smart contract address
* @param _fromChainId Indicate from which chain current cross-chain tx comes
* @return true or false
*/
function _executeCrossChainTx(address _toContract, bytes memory _method, bytes memory _args, bytes memory _fromContractAddr, uint64 _fromChainId) internal returns (bool){
// Ensure the target contract gonna be invoked is indeed a contract rather than a normal account address
require(Utils.isContract(_toContract), "The passed in address is not a contract!");
bytes memory returnData;
bool success;
// The returnData will be bytes32, the last byte must be 01;
(success, returnData) = _toContract.call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_args, _fromContractAddr, _fromChainId)));
// Ensure the executation is successful
require(success == true, "EthCrossChain call business contract failed");
// Ensure the returned value is true
require(returnData.length != 0, "No return value from business contract!");
(bool res,) = ZeroCopySource.NextBool(returnData, 31);
require(res == true, "EthCrossChain call business contract return is not true");
return true;
}
- The relayer should invoke this method. In some circumstances, users can invoke this method by themselves if they get the valid block information from Poly.
- This method fetches and processes cross-chain transactions, finds the Merkle root of a transaction based on the block height (in the block header), and verifies the transaction's legitimacy using the transaction parameters.
- After verifying the Poly chain block header and proof, you still need to check if the parameters
toContract
andtoMerkleValue.makeTxParam.method
have been listed in whitelists. - The business logic contract deployed on the target chain is then invoked, which processes the business logic contract through the internal method
_executeCrossChainTx()
:- This method is meant to invoke the target contract and trigger the execution of cross-chain tx on the target chain.
- Firstly, you need to ensure that the target contract is waiting to invoke a contract rather than a standard account address.
- Then construct a target business logic contract method: you need to
encodePacked
the_method
and the input data format"(bytes,bytes,uint64)"
. - Then it would
keccak256
the encoded string, usingbytes4
to take the first four bytes of the call data for a function call that specifies what function to call. - Parameter
_method
is from thetoMerkleValue
, which is parsed fromproof
. And the input parameters format is restricted as (bytes_args
, bytes_fromContractAddr
, uint64_fromChainId
). These two parts are encodePacked as a method call.
- After calling the method, you need to check the return value. Only if the return value is true will the whole cross-chain transaction be executed successfully.
To guarantee the safety of the CCM contract, we keep whitelists of contract addresses and methods to prevent invalid calls. Meanwhile, we also set whiteLister
to manage these whitelists of the CCM contract.
Here is the template for adding a whitelist. We highly encourage developers to develop similar features of authority management in personal projects.
As mentioned above, CCD is functioned as storing and catching data of CCM. You can take different methods to realize CCM module for different chains according to your actual needs. In general, Poly Network team deploys the CCD contract separately for the CCD module, but you can also realize the module by deploying an integrated contracts. See the source code for the specifics.
CCMP is used to manage CCM. Similar to CCD, CCMP module also has two ways to be implemented. And you can see the source code for the specifics.