Countersmart contract residing in one chain, where the integer property (
counter) is stored and can be incremented by a call initiated by and only by the
Incrementorcontract from another chain. In other words, when we perform some action against the
Incrementorcontract, it initiates a cross-chain transaction (a submission, in terms of the deBridge protocol) which is being started on one chain, relayed to, and then executed on another chain; during this cross-chain transaction, a
Countercontract is called. The following image presents a high-level overview of a cross-chain transaction we are going to achieve:
deBridgeGatesmart contract only, and second, this call should occur only during the cross-chain transaction originating from the
Incrementorsmart contract at the specific address on another chain. How can this be achieved?
nativeSender) - i.e. an entity that actually initiated the transaction by calling the
deBridgeGatecontract on the origin chain. When the cross-chain transaction is being relayed, the
deBridgeGatecontract on the destination chain temporarily exposes the state of the transaction it currently handles via its properties and then calls a target smart contract (whose address is also a part of a payload) through its helper intermediary. It means that the contract on the target address may access this data.
Countermust know the
deBridgeGatecontract's address on the current chain, so it is reasonable to inject it via a constructor:
receiveIncrementCommandmethod using a modifier. The first obvious check we must perform is to ensure this method is called by the
deBridgeGate's helper intermediary - a
CallProxycontract responsible for performing actual calls (the
deBridgeGatecontract doesn't make calls directly for security considerations):
deBridgeGatehandles some cross-chain transactions relayed from another chain.
Countercontract, for example like this:
Incrementorcontract and get its address, we may let the
Countercontract know about it by calling the
Countercontract starts storing the caller's address, the cross-chain transaction is allowed to originate from, we may reuse this data and add the additional validation logic to the modifier:
receiveIncrementCommandmethod, it becomes properly protected from unauthorized calls and ready to receive commands from the trusted contract on another chain:
Countercontract and know its interface, we may design the
Incrementorcontract, which is responsible for initiating the cross-chain call to
receiveIncrementCommand(). First things first, we must let the
Incrementorknow where the
Countercontract actually resides, so we inject it with the chain ID and address of the
Countercontract, and we also specify the address of the
deBridgeGatecontract as well:
Incrementor, so let its interface be as simple as follows:
send()method — the only entry point to initiate a transaction. It accepts plenty of non-trivial variables and structs. Let's overview all of them to make our submission happen.
send()method is marked as payable (meaning that it accepts ether during a call) and it is necessary to bypass enough ether to cover the protocol fee (or global fixed native fee, according to the internal definition) taken in the native currency of the chain. How much? deBridge tries to stick to around one dollar fee, so the fee varies from chain to chain: for example, at the time of writing the fee on Ethereum is 0.001 ETH and the fee on Polygon is 0.5 MATIC. You are advised to retrieve the actual fee amount by reading
globalFixedNativeFeeproperty either on-chain:
executionFee(or included gas) is the amount of the bridged tokens you are allowing the
deBridgeGateto give as an incentive to a third party in return for successful claim transaction execution on the destination chain. In other words, this is a prepayment for potential gas expenses on the target chain, that will be transferred by the protocol. DeBridge runs its own Claimer service (as a part of the deBridge infrastructure), but in fact, anyone can run such service on its own without any security implications, because
executionFeedistribution is a part of the protocol smart contract. This is an advanced topic that runs out of scope of this document, so for the sake of simplicity just set this value to zero.
CallProxyto revert the whole claim transaction in case the call to the receiver address (the callee contract on the destination chain; it is the
Countercontract in our case) fails. Select the proper behavior wisely, ensuring it is aligned with the design of the callee contract: for example, the contract may fail deliberately and irretrievably so it may be reasonable to handle this call gracefully and mark the whole cross-chain transaction as succeeded. Keep in mind that once the claim transaction succeeds it
submissionIdis marked as used, so you cannot replay the transaction on the destination chain.
CallProxyto expose the address that initiated the cross-chain transaction (submission) on the origin chain. Again, choose wisely: as for our case, the
Countercontract expects this data, so we need to ensure it's presented on the destination chain.
fallbackAddressis the address on the destination chain where the bridged funds will be transferred to in case the call to the receiver address fails AND
REVERT_IF_EXTERNAL_FAILis not set. Since we don't bridge any funds (only the calldata), this field is not very important though it is mandatory to set it. Mind that this address must be packed into bytes.
datais the field for the
calldatato execute on the destination chain. Not a big deal if the contracts reside on different chains: we can encode the call using the interface of the contract to call. In our case, we can import the interface of the
ICrossChainCounter) and use it with the
encodeWithSelectorto produce valid instructions aligned with the interface of
autoParamsstruct with settings that suit our needs:
autoParamsstruct either on-chain (as in the example above) or off-chain using ethers.js or web3.js.
_tokenAddressis the address of the ERC-20 token contract whose tokens you are willing to bridge additionally along with the
calldata. If you are willing to bridge the native currency (e.g. ETH from Ethereum), use the zero address (
_amountis the amount of tokens (of the contract specified in the first arg) you are willing to bridge. Dealing with bridged assets is an advanced topic which out of the scope of this document, so in this example, it is enough to set this arg to zero. The following things are worth mentioning: first, the gate cuts a small 0.1% fee off the bridged asset; second, if you bridge the native currency of the origin blockchain, you must not forget to supply an additional amount to cover the protocol fee, but not include it in this arg value; third, the aforementioned
executionFee(included gas) is counted in the currency of this bridged asset, so its decimals must be in sync with this asset; fourth, ERC-20 tokens should not be transferred in/out explicitly, use allowance and
_chainIdTosets the destination chain ID. Consider looking at chainlist.org for known chain IDs, see the list of supported chains in our docs, or query
deBridgeGate.getChainToConfigon-chain property for programmatic access to the list of chains supported by deBridge.
_receiverdefines the address on the destination chain to receive bridged assets (if any) and be called by the
CallProxycontract in case the call data is given. In the given example, we must set this arg to the address of the
_permitallows the caller to specify EIP-2612-compliant signed approval for the
deBridgeGatecontract to transfer the tokens specified in the first arg. Not applicable here.
_useAssetFeeallows paying the protocol fee in the currency of the asset being bridged rather than the native currency of the blockchain. Not applicable here.
_referralCodeis used to mark the submission with your own code, which will be used later.
_autoParamsis the encoded
autoParamsstruct we've crafted in the previous chapter.
_autoParams. The snippet that actually makes a call to the
deBridgeGatecontract may look like this:
deBridgeGateis made within a blockchain, the cross-chain transaction is being initiated.
Incrementorin our case) submits a new cross-chain call, the
deBridgeGatecontract emits a
Sentevent containing all necessary details about the cross-chain call, including the
submissionId— the global cross-chain identifier of such a call. The
submissionIdis the important thing to identify our submission, so we must capture it either by parsing the event manually or using deSDK which does this action for us:
deBridgeGate.claim()method on the destination chain must be crafted using the data from taken from various sources, which isn't an easy task, so there is deSDK which takes the burden of data preparation:
deBridgeGate.claim()method, and finally sign and broadcast your transaction and wait for the
Claimedevent. This will indicate a successful submission completion.