Setup 2
Staking contract
Create a new file in the contracts/contracts folder called Staking.sol.
touch contracts/Staking.sol
Then copy and paste the staking contract:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.21;
import { IL1Messenger } from '@matterlabs/zksync-contracts/contracts/system-contracts/interfaces/IL1Messenger.sol';
contract Staking {
address constant L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR = 0x0000000000000000000000000000000000008008;
IL1Messenger public L1Messenger = IL1Messenger(L2_TO_L1_MESSENGER_SYSTEM_CONTRACT_ADDR);
struct Deposit {
uint256 amount;
bool madeFirstDeposit;
}
// mapping to address to deposit amount and timestamp
mapping(address => Deposit) public deposits;
function deposit() external payable {
require(msg.value > 0, 'no amount deposited');
Deposit memory lastDeposit = deposits[msg.sender];
deposits[msg.sender].amount = lastDeposit.amount + msg.value;
if (lastDeposit.madeFirstDeposit != true) {
deposits[msg.sender].madeFirstDeposit = true;
bytes memory message = abi.encode(msg.sender);
L1Messenger.sendToL1(message);
}
}
function withdraw() external {
Deposit memory lastDeposit = deposits[msg.sender];
require(lastDeposit.amount > 0, 'no amount deposited');
deposits[msg.sender].amount = 0;
(bool sent, ) = msg.sender.call{ value: lastDeposit.amount }('');
require(sent, 'send failed');
}
}
There are two functions in the staking contract: deposit and withdraw.
When a user first calls the deposit method, the contract sends an interop message with their encoded address.
To send the message, we use the sendToL1 function on the L1Messenger contract, which is pre-deployed on every ZKsync chain at address 0x00..008008.
The withdraw method simply refunds the user their deposited funds.
This contract will be deployed to multiple chains, allowing users to choose which chain they want to stake their ETH in order to access the token reward.
Token contract
Create a new file in the contracts/contracts folder called InteropToken.sol.
touch contracts/InteropToken.sol
Then copy and paste the token contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;
import { ERC20 } from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import { Ownable } from '@openzeppelin/contracts/access/Ownable.sol';
import { IMessageVerification } from '@matterlabs/zksync-contracts/contracts/l1-contracts/state-transition/chain-interfaces/IMessageVerification.sol';
import { L2Message } from '@matterlabs/zksync-contracts/contracts/l1-contracts/common/Messaging.sol';
contract InteropToken is ERC20, Ownable {
// mapping of chain IDs to the approved staking contract address
mapping(uint256 => address) public approvedStakingContracts;
// mapping of addresses to if they have minted tokens or not
mapping(address => bool) public addressesThatMinted;
// mapping of chain IDs to number of adresses that have minted some tokens
mapping(uint256 => uint256) public rewardsByChain;
// the chain ID with the most rewards
uint256 public winningChainId;
address constant L2_MESSAGE_VERIFICATION_ADDRESS = 0x0000000000000000000000000000000000010009;
IMessageVerification public l2MessageVerifier = IMessageVerification(L2_MESSAGE_VERIFICATION_ADDRESS);
constructor(
uint256[] memory _chainIds,
address[] memory _stakingContracts
) ERC20('InteropToken', 'INTR') Ownable(msg.sender) {
require(_chainIds.length == _stakingContracts.length, 'Length mismatch');
for (uint256 i = 0; i < _chainIds.length; ++i) {
require(_stakingContracts[i] != address(0), 'Zero address used');
require(approvedStakingContracts[_chainIds[i]] == address(0), 'Staking contract address already set');
approvedStakingContracts[_chainIds[i]] = _stakingContracts[i];
}
}
function mint(
uint256 _sourceChainId,
uint256 _l1BatchNumber,
uint256 _l2MessageIndex,
L2Message calldata _l2MessageData,
bytes32[] calldata _proof
) external {
// token can only be minted once per address
require(addressesThatMinted[msg.sender] == false, 'Token already minted');
require(approvedStakingContracts[_sourceChainId] == _l2MessageData.sender, 'Message origin not approved');
bool result = checkMessageProof(_sourceChainId, _l1BatchNumber, _l2MessageIndex, _l2MessageData, _proof);
require(result == true, 'Message not verified');
address sender = abi.decode(_l2MessageData.data, (address));
require(sender == msg.sender, 'Message sender is not depositor');
rewardsByChain[_sourceChainId]++;
if (rewardsByChain[_sourceChainId] > rewardsByChain[winningChainId]) {
winningChainId = _sourceChainId;
}
addressesThatMinted[msg.sender] = true;
_mint(msg.sender, 1);
}
function checkMessageProof(
uint256 _sourceChainId,
uint256 _l1BatchNumber,
uint256 _l2MessageIndex,
L2Message calldata _l2MessageData,
bytes32[] calldata _proof
) public view returns (bool) {
bool result = l2MessageVerifier.proveL2MessageInclusionShared(
_sourceChainId,
_l1BatchNumber,
_l2MessageIndex,
_l2MessageData,
_proof
);
return result;
}
}
This token contract is used to reward users for staking ETH on any of the staking contracts deployed to different chains.
In the token contract's constructor function, a list of approved staking contract addresses and their chain IDs must be defined. We use this in the contract to make sure interop messages were sent from the correct contracts.
This contract has a mint function that takes the arguments needed for message verification.
We use the proveL2MessageInclusionShared method in the L2 message verification contract for verification.
This contract is deployed at 0x..10009 on each ZKsync chain.
There are a few different checks happening in the mint function:
- The user has not previously claimed a reward token.
- The interop message is verifiable.
- The interop message was sent from an approved staking contract.
- The interop message matches the user's address.
Ignition scripts
Now that we have the contracts ready, let's add some ignition scripts for deploying.
Staking ignition script
Create a new file in the ignition/modules folder called Staking.ts:
touch ignition/modules/Staking.ts
Then copy and paste the module below:
import { buildModule } from '@nomicfoundation/hardhat-ignition/modules';
export default buildModule('StakingModule', (m) => {
const counter = m.contract('Staking');
return { counter };
});
Token ignition script
Create another file in the ignition/modules folder called InteropToken.ts:
touch ignition/modules/InteropToken.ts
Then copy and paste the module below: