# Exercise #2: Cross-chain staking

## Token handling mechanisms

To transfer tokens using CCIP, token pools on both blockchains must exist. That's why you can transfer only supported tokens, not all of them.

Technically, tokens are not transferred. Instead, they are locked or burned on the source chain and then unlocked or minted on the destination chain

Token handling mechanisms are a key aspect of how token transfers work. They each have different characteristics with trade-offs for issuers, holders, and DeFi applications.

1. **Burn & Mint**\
   Tokens are burned on the source chain and minted natively on the destination chain<br>
2. **Lock & Mint (Reverse: Burn & Unlock)**\
   Tokens are locked on the source chain (in Token Pools), and wrapped/synthetic/derivative tokens that represent the locked tokens are minted on the destination chain.<br>
3. **Lock & Unlock** *\[ON THE ROADMAP]*\
   Transferred tokens are locked on the source chain (in Token Pools) and unlocked from Token Pools on the destination chain. This feature is not live yet.

### **Burn & Mint**

Tokens are burned on the source chain and minted natively on the destination chain

<figure><img src="/files/VV30OduO6rXXDPoaGYwE" alt="" width="353"><figcaption><p>Burn &#x26; Mint</p></figcaption></figure>

* Use cases:
  * Tokens that are natively minted on multiple blockchains and have a variable total supply.
  * Examples: stablecoins, synthetic/derivative tokens, wrapped tokens (from Lock & Mint)

### **Lock & Mint (Reverse: Burn & Unlock)**

Tokens are locked on the source chain (in Token Pools), and wrapped/synthetic/derivative tokens that represent the locked tokens are minted on the destination chain.

<figure><img src="https://2422224061-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEm7Dwh3rUCIyHWLvZwRo%2Fuploads%2FISEk2DBW7Vx1cOqrFeSA%2Flock%20mint.png?alt=media&#x26;token=a03b2ae9-cdac-457c-989d-30590487fdaf" alt="" width="375"><figcaption><p>Lock &#x26; Mint (Reverse: Burn &#x26; Unlock)</p></figcaption></figure>

* Use cases:
  * Tokens minted on a single chain (e.g., LINK)
  * Tokens with encoded constraints (supply/burn/mint)
  * Secure minting function with Proof-of-Reserve

### **Lock & Unlock** *\[ON THE ROADMAP]*

Transferred tokens are locked on the source chain (in Token Pools) and unlocked from Token Pools on the destination chain. **This feature is not live yet.**

<figure><img src="https://2422224061-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEm7Dwh3rUCIyHWLvZwRo%2Fuploads%2FxIz307NQlfDK8FIAVCvi%2Flock%20unlock.png?alt=media&#x26;token=51e2f166-74ed-4b81-9a2d-f81721f537b9" alt="" width="375"><figcaption><p>Lock &#x26; Unlock</p></figcaption></figure>

* Use cases:
  * Canonical/dominant wrapped tokens (eg: WETH, LINK …)

## Rate Limits

The main objective of Rate Limits is to manage risk by limiting the value flowing through CCIP. A rate limit always applies to a lane.

Each available lane has two rate limits:

* **Network rate limit:** Each network has a limit on the total USD value that can be transferred from one network across all available lanes. CCIP uses Chainlink Data Feeds to calculate the total USD value of tokens transferred on one network.
* **Token rate limits:** Each individual lane on a network might have a limit on the total number of tokens that it can transfer. This limit is independent of the USD value of the tokens.

Both limits have the following concepts:

* **Bucket**: holds the value that can be transferred at any given moment.
* **Capacity**: the capacity of the bucket represents the maximum value that can be transferred at once via a single transaction.
* **Refill Rate**: the rate at which the bucket is refilled, denominated per second.
* **Availability**: the current amount of value in the bucket to be transferred.

<details>

<summary>Token Bucket Rate Limiting code</summary>

```solidity
function _consume(TokenBucket storage s_bucket, uint256 requestTokens, address tokenAddress) internal {
    // If there is no value to remove or rate limiting is turned off, skip this step to reduce gas usage
    if (!s_bucket.isEnabled || requestTokens == 0) {
      return;
    }

    uint256 tokens = s_bucket.tokens;
    uint256 capacity = s_bucket.capacity;
    uint256 timeDiff = block.timestamp - s_bucket.lastUpdated;

    if (timeDiff != 0) {
      if (tokens > capacity) revert BucketOverfilled();

      // Refill tokens when arriving at a new block time
      tokens = _calculateRefill(capacity, tokens, timeDiff, s_bucket.rate);

      s_bucket.lastUpdated = uint32(block.timestamp);
    }

    if (capacity < requestTokens) {
      // Token address 0 indicates consuming aggregate value rate limit capacity.
      if (tokenAddress == address(0)) revert AggregateValueMaxCapacityExceeded(capacity, requestTokens);
      revert TokenMaxCapacityExceeded(capacity, requestTokens, tokenAddress);
    }
    if (tokens < requestTokens) {
      uint256 rate = s_bucket.rate;
      // Wait required until the bucket is refilled enough to accept this value, round up to next higher second
      // Consume is not guaranteed to succeed after wait time passes if there is competing traffic.
      // This acts as a lower bound of wait time.
      uint256 minWaitInSeconds = ((requestTokens - tokens) + (rate - 1)) / rate;

      if (tokenAddress == address(0)) revert AggregateValueRateLimitReached(minWaitInSeconds, tokens);
      revert TokenRateLimitReached(minWaitInSeconds, tokens, tokenAddress);
    }
    tokens -= requestTokens;

    // Downcast is safe here, as tokens is not larger than capacity
    s_bucket.tokens = uint128(tokens);
    emit TokensConsumed(requestTokens);
}


function _currentTokenBucketState(TokenBucket memory bucket) internal view returns (TokenBucket memory) {
    // We update the bucket to reflect the status at the exact time of the
    // call. This means we might need to refill a part of the bucket based
    // on the time that has passed since the last update.
    bucket.tokens = uint128(
      _calculateRefill(bucket.capacity, bucket.tokens, block.timestamp - bucket.lastUpdated, bucket.rate)
    );
    bucket.lastUpdated = uint32(block.timestamp);
    return bucket;
}


function _calculateRefill(
    uint256 capacity,
    uint256 tokens,
    uint256 timeDiff,
    uint256 rate
  ) private pure returns (uint256) {
    return _min(capacity, tokens + timeDiff * rate);
  }
```

</details>

Let's see how these Rate Limits work in an example:

* Capacity = $100,000
* Refill rate = $100 per second<br>
* Simulation
  * t=0s: the bucket is empty
  * t=300s: availability of $30,000
  * t=300s: user tries to send $40,000 → has to wait until t=400s when bucket has $40,000 available
  * t=400s: user submits $40,000 transfer again → transfer is executed, availability is $0
  * t=500s: availability of $10,000
  * t=1400s: bucket has maxed at $100,000 capacity

You can check these limits for each lane on the Official Chainlink Documentation. Here are the parameters for the real-world example, [Ethereum Sepolia => Optimism Goerli lane](https://docs.chain.link/ccip/supported-networks#ethereum-sepolia--optimism-goerli-lane):

<figure><img src="https://2422224061-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FEm7Dwh3rUCIyHWLvZwRo%2Fuploads%2FS7iFlD1Cx04FmrEbFxYC%2Flimits.png?alt=media&#x26;token=35bf2a49-f438-460b-b1ef-c9d3aa61de13" alt=""><figcaption><p>Rate Limits</p></figcaption></figure>

If a rate limit is hit, the following errors can be returned. The user can handle these gracefully in their front end so end-users are optimally informed to decide their next step. In the case of multiple tokens in a single token transfer, the error is returned for the first hit. That is why `tokenAddress` is included in the error as a parameter.

* `TokenMaxCapacityExceeded(uint256 capacity, uint256 requested, address tokenAddress);` - User requests to transfer more of a token than the capacity of the bucket.
* `TokenRateLimitReached(uint256 minWaitInSeconds, uint256 available, address tokenAddress);` - User requests to transfer more of a token than is currently available in the bucket. The User might have to wait at least minWaitInSeconds for enough availability or transfer the currently available amount.
* `AggregateValueMaxCapacityExceeded(uint256 capacity, uint256 requested);` - User requests to transfer more value than the capacity of the aggregate rate limit bucket.
* `AggregateValueRateLimitReached(uint256 minWaitInSeconds, uint256 available);` - User requests to transfer more value than currently available in the bucket. The User might have to wait at least minWaitInSeconds for enough availability or transfer the currently available amount.

## Getting started

We are going to build the exercise number two on top of the previous exercise, which means that you already have some amount of CCIP-BnM and LINK tokens on Avalanche Fuji, and you have already installed [@chainlink/contracts](https://www.npmjs.com/package/@chainlink/contracts) and [@chainlink/contracts-ccip](https://www.npmjs.com/package/@chainlink/contracts-ccip) NPM packages.

For this example we still need to install the v4.8.0 of the [@openzeppelin/contracts](https://www.npmjs.com/package/@openzeppelin/contracts) NPM package and to `drip` some amount of **CCIP-LnM** tokens on **Ethereum Sepolia**.

To install it, follow steps specific to the development environment you will use for this Masterclass.

{% tabs %}
{% tab title="Hardhat" %}

```bash
npm i @openzeppelin/contracts@4.8.0 --save-dev
```

{% endtab %}

{% tab title="Foundry" %}

```bash
forge install OpenZeppelin/openzeppelin-contracts@v4.8.0
```

And set remappings to `@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/` in `foundry.toml` or `remappings.txt` files.
{% endtab %}

{% tab title="Remix" %}
Create a new Solidity file, paste the following content, and compile it.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import {ReentrancyGuard} from "@openzeppelin/contracts@4.8.0/security/ReentrancyGuard.sol";

contract Empty {}
```

{% endtab %}
{% endtabs %}

Now let's call the `drip` function of the CCIP-LnM token smart contract on Ethereum Sepolia

```solidity
function drip(address to) external {
  _mint(to, 1e18);
}
```

As we learned previously, to call this function, you can use Block Explorer like Etherscan, Foundry's `cast send` command, but the easiest way to get those tokens is through the [Official Chainlink Documentation](https://docs.chain.link/ccip/test-tokens#mint-tokens-in-the-documentation). Navigate to the linked documentation page and connect your wallet by clicking the "Connect Wallet" button.&#x20;

Once connected, switch to **Ethereum Sepolia** Testnet and mint 1 **CCIP-LnM** token. You should also add the **CCIP-LnM** token to your MetaMask using the button.

<figure><img src="/files/wfwNCjDa7AnmIAAU4jcr" alt=""><figcaption><p>Minting CCIP-LnM tokens</p></figcaption></figure>

## What are we building

We are building a simplified cross-chain staking dApp.&#x20;

<figure><img src="/files/yKUIsL6xEQXAkMDCkQnw" alt=""><figcaption><p>What are we building</p></figcaption></figure>

Our User Alice has some CCIP-BnM tokens on Avalanche Fuji she wants to stake inside Simplified Staking protocol on Ethereum Sepolia. She can do that on behalf of Bob or hers other wallet address as well. Simplified Staking protocol locks CCIP-BnM tokens and transfer equal amount of CCIP-LnM tokens, which act as liquidity tokens in this example, to the appropriate address.

Our protocol should use CCIP programmable token transfer to send the BnM tokens from Avalanche Fuji to Ethereum Sepolia, along with a message of where to store the generated liquid staking tokens on Ethereum Sepolia (Bob's address for example). To to that, CrossChainSender smart contract will send a cross-chain message using Chainlink CCIP to CrossChainReceiver smart contract which will call the `stake()` function of the Simplified Staking protocol and pass CCIP-BnM tokens.

After that CrossChainReceiver will send a "reply cross-chain message" saying that the specified EOA now holds X number of liquid staking tokens (CCIP-LnM), so the CrossChainSender on AvalancheFuji has an up to date state of where the tokens are.

Finally, owner of liquid staking tokens (CCIP-LnM) on Ethereum Sepolia (Bob for example), can then send them to back to Avalanche Fuji (or even a third chain Polygon Mumbai) using Chainlink CCIP token transfer, with the intention to put them into some DeFi protocol there, for example.

## Security Best Practices

For this exercise we will try to follow some of the CCIP Best Practices. For the full list you should always refer to the Chainlink Official Documenation.

### Verify destination chain <a href="#verify-destination-chain" id="verify-destination-chain"></a>

Before calling the Router's `ccipSend` function, ensure that your code allows users to send CCIP messages to trusted destination chains.

### Verify source chain <a href="#verify-source-chain" id="verify-source-chain"></a>

When implementing the `ccipReceive` method in a contract residing on the destination chain, ensure to verify the source chain of the incoming CCIP message. This verification ensures that CCIP messages can only be received from trusted source chains.

### Verify sender <a href="#verify-sender" id="verify-sender"></a>

When implementing the `ccipReceive` method in a contract residing on the destination chain, it's important to validate the sender of the incoming CCIP message. This check ensures that CCIP messages are received only from trusted sender addresses.

{% hint style="info" %}
**Note**: Depending on your use case, this verification might not always be necessary.
{% endhint %}

### Verify Router addresses <a href="#verify-router-addresses" id="verify-router-addresses"></a>

When you implement the `ccipReceive` method in the contract residing on the destination chain, validate that the `msg.sender` is the correct Router address. This verification ensures that only the Router contract can call the `ccipReceive` function on the receiver contract and is for developers that want to restrict which accounts are allowed to call `ccipReceive`.

### Setting `gasLimit` <a href="#setting-gaslimit" id="setting-gaslimit"></a>

The `gasLimit` specifies the maximum amount of gas CCIP can consume to execute `ccipReceive()` on the contract located on the destination blockchain. It is the main factor in determining the fee to send a message. Unspent gas is not refunded.

To transfer tokens directly to an EOA as a *receiver* on the destination blockchain, the `gasLimit` should be set to `0` since there is no `ccipReceive()` implementation to call.

To estimate the accurate gas limit for your destination contract, consider the following options:

* Leveraging Ethereum client RPC by applying `eth_estimateGas` on `receiver.ccipReceive()`. You can find more information on the [Ethereum API Documentation](https://ethereum.github.io/execution-apis/api-documentation/) and [Alchemy documentation](https://docs.alchemy.com/reference/eth-estimategas).
* Conducting [Foundry gas tests](https://book.getfoundry.sh/forge/gas-tracking).
* Using [Hardhat plugin for gas tests](https://github.com/cgewecke/eth-gas-reporter).
* Using a blockchain explorer to look up the gas consumption of a particular internal transaction.

### Defensive Example

Another best practice that we will implement is so called Defensive Example, the pattern which allows us to reprocess failed messages without forcing the original transaction to fail. Let's explain how it works.

#### Receiving and processing messages <a href="#receiving-and-processing-messages" id="receiving-and-processing-messages"></a>

Upon receiving a message on the destination blockchain, the `ccipReceive` function is called by the CCIP Router. This function serves as the entry point to the contract for processing incoming CCIP messages, enforcing crucial security checks through the `onlyRouter`, and `onlyAllowlisted` modifiers.

Here's the step-by-step breakdown of the process:

1. Entrance through `ccipReceive`:
   * The `ccipReceive` function is invoked with an `Any2EVMMessage` struct containing the message to be processed.
   * Security checks ensure the call is from the authorized router, an allowlisted source chain, and an allowlisted sender.
2. Processing Message:
   * `ccipReceive` calls the `processMessage` function, which is external to leverage Solidity's try/catch error handling mechanism. **Note**: The `onlySelf` modifier ensures that only the contract can call this function.
   * Inside `processMessage`, a check is performed for a simulated revert condition using the `s_simRevert` state variable. This simulation is toggled by the `setSimRevert` function, callable only by the contract owner.
   * If `s_simRevert` is false, `processMessage` calls the `_ccipReceive` function for further message processing.
3. Message Processing in `_ccipReceive`:
   * `_ccipReceive` extracts and stores various information from the message, such as the `messageId`, decoded `sender` address, token amounts, and data.
   * It then emits a `MessageReceived` event, signaling the successful processing of the message.
4. Error Handling:
   * If an error occurs during the processing (or a simulated revert is triggered), the catch block within `ccipReceive` is executed.
   * The `messageId` of the failed message is added to `s_failedMessages`, and the message content is stored in `s_messageContents`.
   * A `MessageFailed` event is emitted, which allows for later identification and reprocessing of failed messages.

#### Reprocessing of failed messages <a href="#reprocessing-of-failed-messages" id="reprocessing-of-failed-messages"></a>

The `retryFailedMessage` function provides a mechanism to recover assets if a CCIP message processing fails. It's specifically designed to handle scenarios where message data issues prevent entire processing yet allow for token recovery:

1. Initiation:
   * Only the contract owner can call this function, providing the `messageId` of the failed message and the `tokenReceiver` address for token recovery.
2. Validation:
   * It checks if the message has failed using `s_failedMessages.get(messageId)`. If not, it reverts the transaction.
3. Status Update:
   * The error code for the message is updated to `RESOLVED` to prevent reentry and multiple retries.
4. Token Recovery:
   * Retrieves the failed message content using `s_messageContents[messageId]`.
   * Transfers the locked tokens associated with the failed message to the specified `tokenReceiver` as an escape hatch without processing the entire message again.
5. **Event Emission**:
   * An event `MessageRecovered` is emitted to signal the successful recovery of the tokens.

This function showcases a graceful asset recovery solution, protecting user values even when message processing encounters issues.

## Develop ISimplifiedStaking.sol

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `contracts` folder and create a new Solidity file and name it `ISimplifiedStaking.sol.`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ISimplifiedStaking {
    function stake(uint256 amount, address onBehalfOf) external;
    function unstake(uint256 amount) external; 
}
```

{% endcode %}
{% endtab %}

{% tab title="Foundry" %}
Navigate to the `src` folder and create a new Solidity file and name it `ISimplifiedStaking.sol`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ISimplifiedStaking {
    function stake(uint256 amount, address onBehalfOf) external;
    function unstake(uint256 amount) external; 
}
```

{% endcode %}
{% endtab %}

{% tab title="Remix" %}
Navigate to the "File Explorer" tab and create a new Solidity file and name it `ISimplifiedStaking.sol`

{% code lineNumbers="true" fullWidth="false" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface ISimplifiedStaking {
    function stake(uint256 amount, address onBehalfOf) external;
    function unstake(uint256 amount) external; 
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Develop SimplifiedStaking.sol

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `contracts` folder and create a new Solidity file and name it `SimplifiedStaking.sol.`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract SimplifiedStaking is
    ISimplifiedStaking,
    ReentrancyGuard,
    OwnerIsCreator
{
    using SafeERC20 for IERC20;

    IERC20 internal immutable i_stakingToken;
    IERC20 internal immutable i_lpToken;

    event Staked(uint256 amount, address indexed onBehalfOf);
    event Unstaked(uint256 amount, address indexed onBehalfOf);

    error CantUnstakeMoreThanStaked();

    mapping(address owner => uint256 amount) public stakes;

    constructor(address stakingTokenAddress, address lpTokenAddress) {
        i_stakingToken = IERC20(stakingTokenAddress);
        i_lpToken = IERC20(lpTokenAddress);
    }

    function stake(
        uint256 amount,
        address onBehalfOf
    ) external override nonReentrant {
        i_stakingToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[onBehalfOf] += amount;

        i_lpToken.safeTransfer(onBehalfOf, amount);

        emit Staked(amount, onBehalfOf);
    }

    function unstake(uint256 amount) external override nonReentrant {
        if (amount > stakes[msg.sender]) revert CantUnstakeMoreThanStaked();

        i_lpToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[msg.sender] -= amount;

        i_stakingToken.safeTransfer(msg.sender, amount);

        emit Unstaked(amount, msg.sender);
    }

    function withdraw(uint256 amount) external onlyOwner nonReentrant {
        i_lpToken.transfer(msg.sender, amount);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Foundry" %}
Navigate to the `src` folder and create a new Solidity file and name it `SimplifiedStaking.sol`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract SimplifiedStaking is
    ISimplifiedStaking,
    ReentrancyGuard,
    OwnerIsCreator
{
    using SafeERC20 for IERC20;

    IERC20 internal immutable i_stakingToken;
    IERC20 internal immutable i_lpToken;

    event Staked(uint256 amount, address indexed onBehalfOf);
    event Unstaked(uint256 amount, address indexed onBehalfOf);

    error CantUnstakeMoreThanStaked();

    mapping(address owner => uint256 amount) public stakes;

    constructor(address stakingTokenAddress, address lpTokenAddress) {
        i_stakingToken = IERC20(stakingTokenAddress);
        i_lpToken = IERC20(lpTokenAddress);
    }

    function stake(
        uint256 amount,
        address onBehalfOf
    ) external override nonReentrant {
        i_stakingToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[onBehalfOf] += amount;

        i_lpToken.safeTransfer(onBehalfOf, amount);

        emit Staked(amount, onBehalfOf);
    }

    function unstake(uint256 amount) external override nonReentrant {
        if (amount > stakes[msg.sender]) revert CantUnstakeMoreThanStaked();

        i_lpToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[msg.sender] -= amount;

        i_stakingToken.safeTransfer(msg.sender, amount);

        emit Unstaked(amount, msg.sender);
    }

    function withdraw(uint256 amount) external onlyOwner nonReentrant {
        i_lpToken.transfer(msg.sender, amount);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Remix" %}
Navigate to the "File Explorer" tab and create a new Solidity file and name it `SimplifiedStaking.sol`

{% code lineNumbers="true" fullWidth="false" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts@4.8.0/security/ReentrancyGuard.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract SimplifiedStaking is
    ISimplifiedStaking,
    ReentrancyGuard,
    OwnerIsCreator
{
    using SafeERC20 for IERC20;

    IERC20 internal immutable i_stakingToken;
    IERC20 internal immutable i_lpToken;

    event Staked(uint256 amount, address indexed onBehalfOf);
    event Unstaked(uint256 amount, address indexed onBehalfOf);

    error CantUnstakeMoreThanStaked();

    mapping(address owner => uint256 amount) public stakes;

    constructor(address stakingTokenAddress, address lpTokenAddress) {
        i_stakingToken = IERC20(stakingTokenAddress);
        i_lpToken = IERC20(lpTokenAddress);
    }

    function stake(
        uint256 amount,
        address onBehalfOf
    ) external override nonReentrant {
        i_stakingToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[onBehalfOf] += amount;

        i_lpToken.safeTransfer(onBehalfOf, amount);

        emit Staked(amount, onBehalfOf);
    }

    function unstake(uint256 amount) external override nonReentrant {
        if (amount > stakes[msg.sender]) revert CantUnstakeMoreThanStaked();

        i_lpToken.safeTransferFrom(msg.sender, address(this), amount);

        stakes[msg.sender] -= amount;

        i_stakingToken.safeTransfer(msg.sender, amount);

        emit Unstaked(amount, msg.sender);
    }

    function withdraw(uint256 amount) external onlyOwner nonReentrant {
        i_lpToken.transfer(msg.sender, amount);
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Develop CrossChainReceiver.sol

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `contracts` folder and create a new Solidity file and name it `CrossChainReceiver.sol.`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/utils/structs/EnumerableMap.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainReceiver is CCIPReceiver, OwnerIsCreator {
    using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
    using SafeERC20 for IERC20;

    // Example error code, could have many different error codes.
    enum ErrorCode {
        // RESOLVED is first so that the default value is resolved.
        RESOLVED,
        // Could have any number of error codes here.
        BASIC
    }

    // Contains failed messages and their state.
    EnumerableMap.Bytes32ToUintMap internal s_failedMessages;

    ISimplifiedStaking internal immutable i_simplifiedStaking;

    // This is used to simulate a revert in the processMessage function.
    bool internal s_simRevert = false;

    // Mapping to keep track of allowlisted source chains.
    mapping(uint64 chainSelecotor => bool isAllowlisted)
        public allowlistedSourceChains;

    // Mapping to keep track of allowlisted senders.
    mapping(address sender => bool isAllowlisted) public allowlistedSenders;

    // Mapping to keep track of the message contents of failed messages.
    mapping(bytes32 messageId => Client.Any2EVMMessage contents)
        public s_messageContents;

    event MessageFailed(bytes32 indexed messageId, bytes reason);
    event MessageRecovered(bytes32 indexed messageId);

    error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
    error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
    error OnlySelf(); // Used when a function is called outside of the contract itself.
    error ErrorCase(); // Used when simulating a revert during message processing.
    error MessageNotFailed(bytes32 messageId);

    constructor(
        address ccipRouterAddress,
        address simplifiedStakingAddress
    ) CCIPReceiver(ccipRouterAddress) {
        i_simplifiedStaking = ISimplifiedStaking(simplifiedStakingAddress);
    }

    /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
    /// @param _sourceChainSelector The selector of the destination chain.
    /// @param _sender The address of the sender.
    modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
        if (!allowlistedSourceChains[_sourceChainSelector])
            revert SourceChainNotAllowed(_sourceChainSelector);
        if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
        _;
    }

    // @dev Modifier to allow only the contract itself to execute a function.
    /// Throws an exception if called by any account other than the contract itself.
    modifier onlySelf() {
        if (msg.sender != address(this)) revert OnlySelf();
        _;
    }

    /// @dev Updates the allowlist status of a source chain
    /// @notice This function can only be called by the owner.
    /// @param _sourceChainSelector The selector of the source chain to be updated.
    /// @param allowed The allowlist status to be set for the source chain.
    function allowlistSourceChain(
        uint64 _sourceChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedSourceChains[_sourceChainSelector] = allowed;
    }

    /// @dev Updates the allowlist status of a sender for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _sender The address of the sender to be updated.
    /// @param allowed The allowlist status to be set for the sender.
    function allowlistSender(address _sender, bool allowed) external onlyOwner {
        allowlistedSenders[_sender] = allowed;
    }

    /// @notice The entrypoint for the CCIP router to call. This function should
    /// never revert, all errors should be handled internally in this contract.
    /// @param any2EvmMessage The message to process.
    /// @dev Extremely important to ensure only router calls this.
    function ccipReceive(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        override
        onlyRouter
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        /* solhint-disable no-empty-blocks */
        try this.processMessage(any2EvmMessage) {
            // Intentionally empty in this example; no action needed if processMessage succeeds
        } catch (bytes memory err) {
            // Could set different error codes based on the caught error. Each could be
            // handled differently.
            s_failedMessages.set(
                any2EvmMessage.messageId,
                uint256(ErrorCode.BASIC)
            );
            s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
            // Don't revert so CCIP doesn't revert. Emit event instead.
            // The message can be retried later without having to do manual execution of CCIP.
            emit MessageFailed(any2EvmMessage.messageId, err);
            return;
        }
    }

    /// @notice Serves as the entry point for this contract to process incoming messages.
    /// @param any2EvmMessage Received CCIP message.
    /// @dev Transfers specified token amounts to the owner of this contract. This function
    /// must be external because of the  try/catch for error handling.
    /// It uses the `onlySelf`: can only be called from the contract.
    function processMessage(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        onlySelf
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        // Simulate a revert for testing purposes
        if (s_simRevert) revert ErrorCase();

        _ccipReceive(any2EvmMessage); // process the message - may revert as well
    }

    /// @notice Allows the owner to retry a failed message in order to unblock the associated tokens.
    /// @param messageId The unique identifier of the failed message.
    /// @param tokenReceiver The address to which the tokens will be sent.
    /// @dev This function is only callable by the contract owner. It changes the status of the message
    /// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message.
    function retryFailedMessage(
        bytes32 messageId,
        address tokenReceiver
    ) external onlyOwner {
        // Check if the message has failed; if not, revert the transaction.
        if (s_failedMessages.get(messageId) != uint256(ErrorCode.BASIC))
            revert MessageNotFailed(messageId);

        // Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message.
        s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));

        // Retrieve the content of the failed message.
        Client.Any2EVMMessage memory message = s_messageContents[messageId];

        // This example expects one token to have been sent, but you can handle multiple tokens.
        // Transfer the associated tokens to the specified receiver as an escape hatch.
        IERC20(message.destTokenAmounts[0].token).safeTransfer(
            tokenReceiver,
            message.destTokenAmounts[0].amount
        );

        // Emit an event indicating that the message has been recovered.
        emit MessageRecovered(messageId);
    }

    /// @notice Allows the owner to toggle simulation of reversion for testing purposes.
    /// @param simRevert If `true`, simulates a revert condition; if `false`, disables the simulation.
    /// @dev This function is only callable by the contract owner.
    function setSimRevert(bool simRevert) external onlyOwner {
        s_simRevert = simRevert;
    }

    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    ) internal override {
        IERC20(any2EvmMessage.destTokenAmounts[0].token).approve(
            address(i_simplifiedStaking),
            any2EvmMessage.destTokenAmounts[0].amount
        );

        i_simplifiedStaking.stake(
            any2EvmMessage.destTokenAmounts[0].amount,
            abi.decode(any2EvmMessage.data, (address))
        );
    }

    /**
     * @notice Retrieves the IDs of failed messages from the `s_failedMessages` map.
     * @dev Iterates over the `s_failedMessages` map, collecting all keys.
     * @return ids An array of bytes32 containing the IDs of failed messages from the `s_failedMessages` map.
     */
    function getFailedMessagesIds()
        external
        view
        returns (bytes32[] memory ids)
    {
        uint256 length = s_failedMessages.length();
        bytes32[] memory allKeys = new bytes32[](length);
        for (uint256 i = 0; i < length; i++) {
            (bytes32 key, ) = s_failedMessages.at(i);
            allKeys[i] = key;
        }
        return allKeys;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Foundry" %}
Navigate to the `src` folder and create a new Solidity file and name it `CrossChainReceiver.sol`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/utils/structs/EnumerableMap.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainReceiver is CCIPReceiver, OwnerIsCreator {
    using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
    using SafeERC20 for IERC20;

    // Example error code, could have many different error codes.
    enum ErrorCode {
        // RESOLVED is first so that the default value is resolved.
        RESOLVED,
        // Could have any number of error codes here.
        BASIC
    }

    // Contains failed messages and their state.
    EnumerableMap.Bytes32ToUintMap internal s_failedMessages;

    ISimplifiedStaking internal immutable i_simplifiedStaking;

    // This is used to simulate a revert in the processMessage function.
    bool internal s_simRevert = false;

    // Mapping to keep track of allowlisted source chains.
    mapping(uint64 chainSelecotor => bool isAllowlisted)
        public allowlistedSourceChains;

    // Mapping to keep track of allowlisted senders.
    mapping(address sender => bool isAllowlisted) public allowlistedSenders;

    // Mapping to keep track of the message contents of failed messages.
    mapping(bytes32 messageId => Client.Any2EVMMessage contents)
        public s_messageContents;

    event MessageFailed(bytes32 indexed messageId, bytes reason);
    event MessageRecovered(bytes32 indexed messageId);

    error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
    error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
    error OnlySelf(); // Used when a function is called outside of the contract itself.
    error ErrorCase(); // Used when simulating a revert during message processing.
    error MessageNotFailed(bytes32 messageId);

    constructor(
        address ccipRouterAddress,
        address simplifiedStakingAddress
    ) CCIPReceiver(ccipRouterAddress) {
        i_simplifiedStaking = ISimplifiedStaking(simplifiedStakingAddress);
    }

    /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
    /// @param _sourceChainSelector The selector of the destination chain.
    /// @param _sender The address of the sender.
    modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
        if (!allowlistedSourceChains[_sourceChainSelector])
            revert SourceChainNotAllowed(_sourceChainSelector);
        if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
        _;
    }

    // @dev Modifier to allow only the contract itself to execute a function.
    /// Throws an exception if called by any account other than the contract itself.
    modifier onlySelf() {
        if (msg.sender != address(this)) revert OnlySelf();
        _;
    }

    /// @dev Updates the allowlist status of a source chain
    /// @notice This function can only be called by the owner.
    /// @param _sourceChainSelector The selector of the source chain to be updated.
    /// @param allowed The allowlist status to be set for the source chain.
    function allowlistSourceChain(
        uint64 _sourceChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedSourceChains[_sourceChainSelector] = allowed;
    }

    /// @dev Updates the allowlist status of a sender for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _sender The address of the sender to be updated.
    /// @param allowed The allowlist status to be set for the sender.
    function allowlistSender(address _sender, bool allowed) external onlyOwner {
        allowlistedSenders[_sender] = allowed;
    }

    /// @notice The entrypoint for the CCIP router to call. This function should
    /// never revert, all errors should be handled internally in this contract.
    /// @param any2EvmMessage The message to process.
    /// @dev Extremely important to ensure only router calls this.
    function ccipReceive(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        override
        onlyRouter
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        /* solhint-disable no-empty-blocks */
        try this.processMessage(any2EvmMessage) {
            // Intentionally empty in this example; no action needed if processMessage succeeds
        } catch (bytes memory err) {
            // Could set different error codes based on the caught error. Each could be
            // handled differently.
            s_failedMessages.set(
                any2EvmMessage.messageId,
                uint256(ErrorCode.BASIC)
            );
            s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
            // Don't revert so CCIP doesn't revert. Emit event instead.
            // The message can be retried later without having to do manual execution of CCIP.
            emit MessageFailed(any2EvmMessage.messageId, err);
            return;
        }
    }

    /// @notice Serves as the entry point for this contract to process incoming messages.
    /// @param any2EvmMessage Received CCIP message.
    /// @dev Transfers specified token amounts to the owner of this contract. This function
    /// must be external because of the  try/catch for error handling.
    /// It uses the `onlySelf`: can only be called from the contract.
    function processMessage(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        onlySelf
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        // Simulate a revert for testing purposes
        if (s_simRevert) revert ErrorCase();

        _ccipReceive(any2EvmMessage); // process the message - may revert as well
    }

    /// @notice Allows the owner to retry a failed message in order to unblock the associated tokens.
    /// @param messageId The unique identifier of the failed message.
    /// @param tokenReceiver The address to which the tokens will be sent.
    /// @dev This function is only callable by the contract owner. It changes the status of the message
    /// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message.
    function retryFailedMessage(
        bytes32 messageId,
        address tokenReceiver
    ) external onlyOwner {
        // Check if the message has failed; if not, revert the transaction.
        if (s_failedMessages.get(messageId) != uint256(ErrorCode.BASIC))
            revert MessageNotFailed(messageId);

        // Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message.
        s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));

        // Retrieve the content of the failed message.
        Client.Any2EVMMessage memory message = s_messageContents[messageId];

        // This example expects one token to have been sent, but you can handle multiple tokens.
        // Transfer the associated tokens to the specified receiver as an escape hatch.
        IERC20(message.destTokenAmounts[0].token).safeTransfer(
            tokenReceiver,
            message.destTokenAmounts[0].amount
        );

        // Emit an event indicating that the message has been recovered.
        emit MessageRecovered(messageId);
    }

    /// @notice Allows the owner to toggle simulation of reversion for testing purposes.
    /// @param simRevert If `true`, simulates a revert condition; if `false`, disables the simulation.
    /// @dev This function is only callable by the contract owner.
    function setSimRevert(bool simRevert) external onlyOwner {
        s_simRevert = simRevert;
    }

    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    ) internal override {
        IERC20(any2EvmMessage.destTokenAmounts[0].token).approve(
            address(i_simplifiedStaking),
            any2EvmMessage.destTokenAmounts[0].amount
        );

        i_simplifiedStaking.stake(
            any2EvmMessage.destTokenAmounts[0].amount,
            abi.decode(any2EvmMessage.data, (address))
        );
    }

    /**
     * @notice Retrieves the IDs of failed messages from the `s_failedMessages` map.
     * @dev Iterates over the `s_failedMessages` map, collecting all keys.
     * @return ids An array of bytes32 containing the IDs of failed messages from the `s_failedMessages` map.
     */
    function getFailedMessagesIds()
        external
        view
        returns (bytes32[] memory ids)
    {
        uint256 length = s_failedMessages.length();
        bytes32[] memory allKeys = new bytes32[](length);
        for (uint256 i = 0; i < length; i++) {
            (bytes32 key, ) = s_failedMessages.at(i);
            allKeys[i] = key;
        }
        return allKeys;
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Remix" %}
Navigate to the "File Explorer" tab and create a new Solidity file and name it `CrossChainReceiver.sol`

{% code lineNumbers="true" fullWidth="false" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {EnumerableMap} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/utils/structs/EnumerableMap.sol";
import {ISimplifiedStaking} from "./ISimplifiedStaking.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainReceiver is CCIPReceiver, OwnerIsCreator {
    using EnumerableMap for EnumerableMap.Bytes32ToUintMap;
    using SafeERC20 for IERC20;

    // Example error code, could have many different error codes.
    enum ErrorCode {
        // RESOLVED is first so that the default value is resolved.
        RESOLVED,
        // Could have any number of error codes here.
        BASIC
    }

    // Contains failed messages and their state.
    EnumerableMap.Bytes32ToUintMap internal s_failedMessages;

    ISimplifiedStaking internal immutable i_simplifiedStaking;

    // This is used to simulate a revert in the processMessage function.
    bool internal s_simRevert = false;

    // Mapping to keep track of allowlisted source chains.
    mapping(uint64 chainSelecotor => bool isAllowlisted)
        public allowlistedSourceChains;

    // Mapping to keep track of allowlisted senders.
    mapping(address sender => bool isAllowlisted) public allowlistedSenders;

    // Mapping to keep track of the message contents of failed messages.
    mapping(bytes32 messageId => Client.Any2EVMMessage contents)
        public s_messageContents;

    event MessageFailed(bytes32 indexed messageId, bytes reason);
    event MessageRecovered(bytes32 indexed messageId);

    error SourceChainNotAllowed(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.
    error SenderNotAllowed(address sender); // Used when the sender has not been allowlisted by the contract owner.
    error OnlySelf(); // Used when a function is called outside of the contract itself.
    error ErrorCase(); // Used when simulating a revert during message processing.
    error MessageNotFailed(bytes32 messageId);

    constructor(
        address ccipRouterAddress,
        address simplifiedStakingAddress
    ) CCIPReceiver(ccipRouterAddress) {
        i_simplifiedStaking = ISimplifiedStaking(simplifiedStakingAddress);
    }

    /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.
    /// @param _sourceChainSelector The selector of the destination chain.
    /// @param _sender The address of the sender.
    modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {
        if (!allowlistedSourceChains[_sourceChainSelector])
            revert SourceChainNotAllowed(_sourceChainSelector);
        if (!allowlistedSenders[_sender]) revert SenderNotAllowed(_sender);
        _;
    }

    // @dev Modifier to allow only the contract itself to execute a function.
    /// Throws an exception if called by any account other than the contract itself.
    modifier onlySelf() {
        if (msg.sender != address(this)) revert OnlySelf();
        _;
    }

    /// @dev Updates the allowlist status of a source chain
    /// @notice This function can only be called by the owner.
    /// @param _sourceChainSelector The selector of the source chain to be updated.
    /// @param allowed The allowlist status to be set for the source chain.
    function allowlistSourceChain(
        uint64 _sourceChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedSourceChains[_sourceChainSelector] = allowed;
    }

    /// @dev Updates the allowlist status of a sender for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _sender The address of the sender to be updated.
    /// @param allowed The allowlist status to be set for the sender.
    function allowlistSender(address _sender, bool allowed) external onlyOwner {
        allowlistedSenders[_sender] = allowed;
    }

    /// @notice The entrypoint for the CCIP router to call. This function should
    /// never revert, all errors should be handled internally in this contract.
    /// @param any2EvmMessage The message to process.
    /// @dev Extremely important to ensure only router calls this.
    function ccipReceive(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        override
        onlyRouter
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        /* solhint-disable no-empty-blocks */
        try this.processMessage(any2EvmMessage) {
            // Intentionally empty in this example; no action needed if processMessage succeeds
        } catch (bytes memory err) {
            // Could set different error codes based on the caught error. Each could be
            // handled differently.
            s_failedMessages.set(
                any2EvmMessage.messageId,
                uint256(ErrorCode.BASIC)
            );
            s_messageContents[any2EvmMessage.messageId] = any2EvmMessage;
            // Don't revert so CCIP doesn't revert. Emit event instead.
            // The message can be retried later without having to do manual execution of CCIP.
            emit MessageFailed(any2EvmMessage.messageId, err);
            return;
        }
    }

    /// @notice Serves as the entry point for this contract to process incoming messages.
    /// @param any2EvmMessage Received CCIP message.
    /// @dev Transfers specified token amounts to the owner of this contract. This function
    /// must be external because of the  try/catch for error handling.
    /// It uses the `onlySelf`: can only be called from the contract.
    function processMessage(
        Client.Any2EVMMessage calldata any2EvmMessage
    )
        external
        onlySelf
        onlyAllowlisted(
            any2EvmMessage.sourceChainSelector,
            abi.decode(any2EvmMessage.sender, (address))
        ) // Make sure the source chain and sender are allowlisted
    {
        // Simulate a revert for testing purposes
        if (s_simRevert) revert ErrorCase();

        _ccipReceive(any2EvmMessage); // process the message - may revert as well
    }

    /// @notice Allows the owner to retry a failed message in order to unblock the associated tokens.
    /// @param messageId The unique identifier of the failed message.
    /// @param tokenReceiver The address to which the tokens will be sent.
    /// @dev This function is only callable by the contract owner. It changes the status of the message
    /// from 'failed' to 'resolved' to prevent reentry and multiple retries of the same message.
    function retryFailedMessage(
        bytes32 messageId,
        address tokenReceiver
    ) external onlyOwner {
        // Check if the message has failed; if not, revert the transaction.
        if (s_failedMessages.get(messageId) != uint256(ErrorCode.BASIC))
            revert MessageNotFailed(messageId);

        // Set the error code to RESOLVED to disallow reentry and multiple retries of the same failed message.
        s_failedMessages.set(messageId, uint256(ErrorCode.RESOLVED));

        // Retrieve the content of the failed message.
        Client.Any2EVMMessage memory message = s_messageContents[messageId];

        // This example expects one token to have been sent, but you can handle multiple tokens.
        // Transfer the associated tokens to the specified receiver as an escape hatch.
        IERC20(message.destTokenAmounts[0].token).safeTransfer(
            tokenReceiver,
            message.destTokenAmounts[0].amount
        );

        // Emit an event indicating that the message has been recovered.
        emit MessageRecovered(messageId);
    }

    /// @notice Allows the owner to toggle simulation of reversion for testing purposes.
    /// @param simRevert If `true`, simulates a revert condition; if `false`, disables the simulation.
    /// @dev This function is only callable by the contract owner.
    function setSimRevert(bool simRevert) external onlyOwner {
        s_simRevert = simRevert;
    }

    function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    ) internal override {
        IERC20(any2EvmMessage.destTokenAmounts[0].token).approve(
            address(i_simplifiedStaking),
            any2EvmMessage.destTokenAmounts[0].amount
        );

        i_simplifiedStaking.stake(
            any2EvmMessage.destTokenAmounts[0].amount,
            abi.decode(any2EvmMessage.data, (address))
        );
    }

    /**
     * @notice Retrieves the IDs of failed messages from the `s_failedMessages` map.
     * @dev Iterates over the `s_failedMessages` map, collecting all keys.
     * @return ids An array of bytes32 containing the IDs of failed messages from the `s_failedMessages` map.
     */
    function getFailedMessagesIds()
        external
        view
        returns (bytes32[] memory ids)
    {
        uint256 length = s_failedMessages.length();
        bytes32[] memory allKeys = new bytes32[](length);
        for (uint256 i = 0; i < length; i++) {
            (bytes32 key, ) = s_failedMessages.at(i);
            allKeys[i] = key;
        }
        return allKeys;
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Develop CrossChainSender.sol

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `contracts` folder and create a new Solidity file and name it `CrossChainSender.sol.`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainSender is OwnerIsCreator {
    using SafeERC20 for IERC20;

    mapping(address onBehalfOf => uint256 ccipLnMAmount)
        public liquidTokensAmount;

    enum PayFeesIn {
        Native,
        LINK
    }

    IRouterClient immutable i_ccipRouter;
    LinkTokenInterface immutable i_link;
    IERC20 immutable i_stakingToken;

    // Mapping to keep track of allowlisted destination chains.
    mapping(uint64 chainSelector => bool isAllowlisted)
        public allowlistedDestinationChains;

    error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
    error NotEnoughBalanceForFees(
        uint256 currentBalance,
        uint256 calculatedFees
    ); // Used to make sure contract has enough balance to cover the fees.
    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.

    event MessageSent(
        bytes32 indexed messageId,
        uint64 destinationChainSelector,
        address receiver,
        address onBehalfOf,
        address token,
        uint256 amount,
        PayFeesIn payFeesIn,
        uint256 fees
    );

    constructor(
        address ccipRouterAddress,
        address linkAddress,
        address stakingTokenAddress
    ) {
        i_ccipRouter = IRouterClient(ccipRouterAddress);
        i_link = LinkTokenInterface(linkAddress);
        i_stakingToken = IERC20(stakingTokenAddress);
    }

    receive() external payable {}

    modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
        if (!allowlistedDestinationChains[_destinationChainSelector])
            revert DestinationChainNotAllowlisted(_destinationChainSelector);
        _;
    }

    /// @dev Updates the allowlist status of a destination chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _destinationChainSelector The selector of the destination chain to be updated.
    /// @param allowed The allowlist status to be set for the destination chain.
    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedDestinationChains[_destinationChainSelector] = allowed;
    }

    function send(
        uint64 _destinationChainSelector,
        address _receiver,
        address _onBehalfOf,
        uint256 _amount,
        PayFeesIn _payFeesIn,
        uint256 _gasLimit
    )
        external
        onlyAllowlistedDestinationChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        // Set the token amounts
        Client.EVMTokenAmount[]
            memory tokenAmounts = new Client.EVMTokenAmount[](1);
        Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
            token: address(i_stakingToken),
            amount: _amount
        });
        tokenAmounts[0] = tokenAmount;

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver), // ABI-encoded receiver address
            data: abi.encode(_onBehalfOf), // ABI-encoded string
            tokenAmounts: tokenAmounts, // The amount and type of token being transferred
            extraArgs: Client._argsToBytes(
                // Additional arguments, setting gas limit and non-strict sequencing mode
                Client.EVMExtraArgsV1({gasLimit: _gasLimit, strict: false})
            ),
            // Set the feeToken, address(linkToken) means fees are paid in LINK, address(0) means fees are paid in native gas
            feeToken: _payFeesIn == PayFeesIn.LINK
                ? address(i_link)
                : address(0)
        });

        // Get the fee required to send the CCIP message
        uint256 fees = i_ccipRouter.getFee(_destinationChainSelector, message);

        if (_payFeesIn == PayFeesIn.LINK) {
            if (fees > i_link.balanceOf(address(this)))
                revert NotEnoughBalanceForFees(
                    i_link.balanceOf(address(this)),
                    fees
                );

            // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
            i_link.approve(address(i_ccipRouter), fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend(
                _destinationChainSelector,
                message
            );
        } else {
            if (fees > address(this).balance)
                revert NotEnoughBalanceForFees(address(this).balance, fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend{value: fees}(
                _destinationChainSelector,
                message
            );
        }

        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _onBehalfOf,
            address(i_stakingToken),
            _amount,
            _payFeesIn,
            fees
        );
    }

    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");

        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }

    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
    /// @param _beneficiary The address to which the tokens will be sent.
    /// @param _token The contract address of the ERC20 token to be withdrawn.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        IERC20(_token).transfer(_beneficiary, amount);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Foundry" %}
Navigate to the `src` folder and create a new Solidity file and name it `ISimplifiedStaking.sol`

{% code lineNumbers="true" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/shared/interfaces/LinkTokenInterface.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainSender is OwnerIsCreator {
    using SafeERC20 for IERC20;

    mapping(address onBehalfOf => uint256 ccipLnMAmount)
        public liquidTokensAmount;

    enum PayFeesIn {
        Native,
        LINK
    }

    IRouterClient immutable i_ccipRouter;
    LinkTokenInterface immutable i_link;
    IERC20 immutable i_stakingToken;

    // Mapping to keep track of allowlisted destination chains.
    mapping(uint64 chainSelector => bool isAllowlisted)
        public allowlistedDestinationChains;

    error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
    error NotEnoughBalanceForFees(
        uint256 currentBalance,
        uint256 calculatedFees
    ); // Used to make sure contract has enough balance to cover the fees.
    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.

    event MessageSent(
        bytes32 indexed messageId,
        uint64 destinationChainSelector,
        address receiver,
        address onBehalfOf,
        address token,
        uint256 amount,
        PayFeesIn payFeesIn,
        uint256 fees
    );

    constructor(
        address ccipRouterAddress,
        address linkAddress,
        address stakingTokenAddress
    ) {
        i_ccipRouter = IRouterClient(ccipRouterAddress);
        i_link = LinkTokenInterface(linkAddress);
        i_stakingToken = IERC20(stakingTokenAddress);
    }

    receive() external payable {}

    modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
        if (!allowlistedDestinationChains[_destinationChainSelector])
            revert DestinationChainNotAllowlisted(_destinationChainSelector);
        _;
    }

    /// @dev Updates the allowlist status of a destination chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _destinationChainSelector The selector of the destination chain to be updated.
    /// @param allowed The allowlist status to be set for the destination chain.
    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedDestinationChains[_destinationChainSelector] = allowed;
    }

    function send(
        uint64 _destinationChainSelector,
        address _receiver,
        address _onBehalfOf,
        uint256 _amount,
        PayFeesIn _payFeesIn,
        uint256 _gasLimit
    )
        external
        onlyAllowlistedDestinationChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        // Set the token amounts
        Client.EVMTokenAmount[]
            memory tokenAmounts = new Client.EVMTokenAmount[](1);
        Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
            token: address(i_stakingToken),
            amount: _amount
        });
        tokenAmounts[0] = tokenAmount;

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver), // ABI-encoded receiver address
            data: abi.encode(_onBehalfOf), // ABI-encoded string
            tokenAmounts: tokenAmounts, // The amount and type of token being transferred
            extraArgs: Client._argsToBytes(
                // Additional arguments, setting gas limit and non-strict sequencing mode
                Client.EVMExtraArgsV1({gasLimit: _gasLimit, strict: false})
            ),
            // Set the feeToken, address(linkToken) means fees are paid in LINK, address(0) means fees are paid in native gas
            feeToken: _payFeesIn == PayFeesIn.LINK
                ? address(i_link)
                : address(0)
        });

        // Get the fee required to send the CCIP message
        uint256 fees = i_ccipRouter.getFee(_destinationChainSelector, message);

        if (_payFeesIn == PayFeesIn.LINK) {
            if (fees > i_link.balanceOf(address(this)))
                revert NotEnoughBalanceForFees(
                    i_link.balanceOf(address(this)),
                    fees
                );

            // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
            i_link.approve(address(i_ccipRouter), fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend(
                _destinationChainSelector,
                message
            );
        } else {
            if (fees > address(this).balance)
                revert NotEnoughBalanceForFees(address(this).balance, fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend{value: fees}(
                _destinationChainSelector,
                message
            );
        }

        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _onBehalfOf,
            address(i_stakingToken),
            _amount,
            _payFeesIn,
            fees
        );
    }

    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");

        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }

    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
    /// @param _beneficiary The address to which the tokens will be sent.
    /// @param _token The contract address of the ERC20 token to be withdrawn.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        IERC20(_token).transfer(_beneficiary, amount);
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Remix" %}
Navigate to the "File Explorer" tab and create a new Solidity file and name it `ISimplifiedStaking.sol`

{% code lineNumbers="true" fullWidth="false" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {IERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/IERC20.sol";
import {SafeERC20} from "@chainlink/contracts-ccip/src/v0.8/vendor/openzeppelin-solidity/v4.8.0/token/ERC20/utils/SafeERC20.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
import {OwnerIsCreator} from "@chainlink/contracts-ccip/src/v0.8/shared/access/OwnerIsCreator.sol";

/**
 * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
 * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
 * DO NOT USE THIS CODE IN PRODUCTION.
 */
contract CrossChainSender is OwnerIsCreator {
    using SafeERC20 for IERC20;

    mapping(address onBehalfOf => uint256 ccipLnMAmount)
        public liquidTokensAmount;

    enum PayFeesIn {
        Native,
        LINK
    }

    IRouterClient immutable i_ccipRouter;
    LinkTokenInterface immutable i_link;
    IERC20 immutable i_stakingToken;

    // Mapping to keep track of allowlisted destination chains.
    mapping(uint64 chainSelector => bool isAllowlisted)
        public allowlistedDestinationChains;

    error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.
    error NotEnoughBalanceForFees(
        uint256 currentBalance,
        uint256 calculatedFees
    ); // Used to make sure contract has enough balance to cover the fees.
    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.
    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.

    event MessageSent(
        bytes32 indexed messageId,
        uint64 destinationChainSelector,
        address receiver,
        address onBehalfOf,
        address token,
        uint256 amount,
        PayFeesIn payFeesIn,
        uint256 fees
    );

    constructor(
        address ccipRouterAddress,
        address linkAddress,
        address stakingTokenAddress
    ) {
        i_ccipRouter = IRouterClient(ccipRouterAddress);
        i_link = LinkTokenInterface(linkAddress);
        i_stakingToken = IERC20(stakingTokenAddress);
    }

    receive() external payable {}

    modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {
        if (!allowlistedDestinationChains[_destinationChainSelector])
            revert DestinationChainNotAllowlisted(_destinationChainSelector);
        _;
    }

    /// @dev Updates the allowlist status of a destination chain for transactions.
    /// @notice This function can only be called by the owner.
    /// @param _destinationChainSelector The selector of the destination chain to be updated.
    /// @param allowed The allowlist status to be set for the destination chain.
    function allowlistDestinationChain(
        uint64 _destinationChainSelector,
        bool allowed
    ) external onlyOwner {
        allowlistedDestinationChains[_destinationChainSelector] = allowed;
    }

    function send(
        uint64 _destinationChainSelector,
        address _receiver,
        address _onBehalfOf,
        uint256 _amount,
        PayFeesIn _payFeesIn,
        uint256 _gasLimit
    )
        external
        onlyAllowlistedDestinationChain(_destinationChainSelector)
        returns (bytes32 messageId)
    {
        // Set the token amounts
        Client.EVMTokenAmount[]
            memory tokenAmounts = new Client.EVMTokenAmount[](1);
        Client.EVMTokenAmount memory tokenAmount = Client.EVMTokenAmount({
            token: address(i_stakingToken),
            amount: _amount
        });
        tokenAmounts[0] = tokenAmount;

        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(_receiver), // ABI-encoded receiver address
            data: abi.encode(_onBehalfOf), // ABI-encoded string
            tokenAmounts: tokenAmounts, // The amount and type of token being transferred
            extraArgs: Client._argsToBytes(
                // Additional arguments, setting gas limit and non-strict sequencing mode
                Client.EVMExtraArgsV1({gasLimit: _gasLimit, strict: false})
            ),
            // Set the feeToken, address(linkToken) means fees are paid in LINK, address(0) means fees are paid in native gas
            feeToken: _payFeesIn == PayFeesIn.LINK
                ? address(i_link)
                : address(0)
        });

        // Get the fee required to send the CCIP message
        uint256 fees = i_ccipRouter.getFee(_destinationChainSelector, message);

        if (_payFeesIn == PayFeesIn.LINK) {
            if (fees > i_link.balanceOf(address(this)))
                revert NotEnoughBalanceForFees(
                    i_link.balanceOf(address(this)),
                    fees
                );

            // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK
            i_link.approve(address(i_ccipRouter), fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend(
                _destinationChainSelector,
                message
            );
        } else {
            if (fees > address(this).balance)
                revert NotEnoughBalanceForFees(address(this).balance, fees);

            i_stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
            i_stakingToken.approve(address(i_ccipRouter), _amount);

            // Send the message through the router and store the returned message ID
            messageId = i_ccipRouter.ccipSend{value: fees}(
                _destinationChainSelector,
                message
            );
        }

        emit MessageSent(
            messageId,
            _destinationChainSelector,
            _receiver,
            _onBehalfOf,
            address(i_stakingToken),
            _amount,
            _payFeesIn,
            fees
        );
    }

    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.
    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.
    /// It should only be callable by the owner of the contract.
    /// @param _beneficiary The address to which the Ether should be sent.
    function withdraw(address _beneficiary) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = address(this).balance;

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        // Attempt to send the funds, capturing the success status and discarding any return data
        (bool sent, ) = _beneficiary.call{value: amount}("");

        // Revert if the send failed, with information about the attempted transfer
        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);
    }

    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.
    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.
    /// @param _beneficiary The address to which the tokens will be sent.
    /// @param _token The contract address of the ERC20 token to be withdrawn.
    function withdrawToken(
        address _beneficiary,
        address _token
    ) public onlyOwner {
        // Retrieve the balance of this contract
        uint256 amount = IERC20(_token).balanceOf(address(this));

        // Revert if there is nothing to withdraw
        if (amount == 0) revert NothingToWithdraw();

        IERC20(_token).transfer(_beneficiary, amount);
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Deploy SimplifiedStaking.sol on Ethereum Sepolia

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `scripts` folder and create new file named `deploySimplifiedStaking.ts`

<pre class="language-typescript"><code class="lang-typescript">// deploySimplifiedStaking.ts
<strong>
</strong>import { ethers, network } from "hardhat";

async function main() {
    const stakingTokenAddressEthereumSepolia = `0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05`;
    const lpTokenAddressEthereumSepolia = `0x466D489b6d36E7E3b824ef491C225F5830E81cC1`;

    const simplifiedStaking = await ethers.deployContract("SimplifiedStaking", [stakingTokenAddressEthereumSepolia, lpTokenAddressEthereumSepolia]);

    await simplifiedStaking.waitForDeployment();

    console.log(`SimplifiedStaking deployed on ${network.name} with address ${simplifiedStaking.target}`);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
    console.error(error);
    process.exitCode = 1;
});
</code></pre>

Run the deployment script:

```bash
npx hardhat run ./scripts/deploySimplifiedStaking.ts --network ethereumSepolia
```

{% endtab %}

{% tab title="Foundry" %}
Option 1)

Deploy `SimplifiedStaking.sol` smart contract by running:

```sh
forge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/SimplifiedStaking.sol:SimplifiedStaking --constructor-args 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05 0x466D489b6d36E7E3b824ef491C225F5830E81cC1 
```

#### Option 2)

Create a new smart contract under the `script` folder and name it `SimplifiedStaking.s.sol`

Note that deployment of the `SimplifiedStaking` smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry) for reference.

```solidity
// script/SimplifiedStaking.s.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import {SimplifiedStaking} from "../src/SimplifiedStaking.sol";

contract DeploySimplifiedStaking is Script {
    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        address stakingTokenAddress = 0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05;
        address lpTokenAddress = 0x466D489b6d36E7E3b824ef491C225F5830E81cC1;

        SimplifiedStaking simplifiedStaking = new SimplifiedStaking(
            stakingTokenAddress,
            lpTokenAddress
        );

        console.log(
            "SimplifiedStaking deployed to ",
            address(simplifiedStaking)
        );

        vm.stopBroadcast();
    }
}
```

Deploy `SimplifiedStaking.sol` smart contract by running:

```sh
forge script ./script/SimplifiedStaking.s.sol:DeploySimplifiedStaking -vvv --broadcast --rpc-url ethereumSepolia
```

{% endtab %}

{% tab title="Remix" %}
Open your Metamask wallet and switch to the Ethereum Sepolia network.

Open the SimplifiedStaking.sol file.

Navigate to the "Solidity Compiler" tab and click the "Compile SimplifiedStaking.sol" button.

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that `chainId` is switched to 11155111 (if not, you may need to refresh the Remix IDE page in your browser).

Under the "Contract" dropdown menu, make sure that the "SimplifiedStaking - SimplifiedStaking.sol" is selected.

Locate the orange "Deploy" button. Provide `0xFd57b4ddBf88a4e07fF4e34C487b99af2Fe82a05` (CCIP-BnM) as the `stakingTokenAddress` and `0x466D489b6d36E7E3b824ef491C225F5830E81cC1` (CCIP-LnM) as the `lpTokenAddress`.

Click the orange "Deploy"/"Transact" button.

Metamask notification will pop up. Sign the transaction.
{% endtab %}
{% endtabs %}

## Transfer some CCIP-LnM to SimplifiedStaking.sol

This amount of CCIP-LnM will serve as some kind of initial liquidity. Notice that there is a `withdraw` function that only owner can call which withdraws provided amount of CCIP-LnM tokens back from this contract to an owner's address.

You can transfer 0.5 CCIP-LnM.

<figure><img src="/files/VNmHABeORFZVS8N3Noxf" alt="" width="354"><figcaption><p>Transfer 0.5 CCIP-LnM to SimplifiedStaking.sol</p></figcaption></figure>

## Deploy CrossChainReceiver.sol on Ethereum Sepolia

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `scripts` folder and create new file named `deployCrossChainReceiver.ts`

```typescript
// deployCrossChainReceiver.ts

import { ethers, network } from "hardhat";

async function main() {
  const ccipRouterAddressEthereumSepolia = `0xd0daae2231e9cb96b94c8512223533293c3693bf`;
  const simplifiedStakingAddress = `PUT SIMPLIFIED_STAKING ADDRESS HERE`;

  const crossChainReceiver = await ethers.deployContract("CrossChainReceiver", [ccipRouterAddressEthereumSepolia, simplifiedStakingAddress]);

  await crossChainReceiver.waitForDeployment();

  console.log(`CrossChainReceiver deployed on ${network.name} with address ${crossChainReceiver.target}`);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Run the deployment script:

```bash
npx hardhat run ./scripts/deployCrossChainReceiver.ts --network ethereumSepolia
```

{% endtab %}

{% tab title="Foundry" %}
Option 1)

Deploy `CrossChainReceiver.sol` smart contract by running:

```sh
forge create --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY src/CrossChainReceiver.sol:CrossChainReceiver --constructor-args 0xd0daae2231e9cb96b94c8512223533293c3693bf <SIMPLIFIED_STAKING_ADDRESS> 
```

#### Option 2)

Create a new smart contract under the `script` folder and name it `CrossChainReceiver.s.sol`

Note that deployment of the `CrossChainReceiver` smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry) for reference.

```solidity
// script/CrossChainReceiver.s.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import {CrossChainReceiver} from "../src/CrossChainReceiver.sol";

contract DeployCrossChainReceiver is Script {
    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        address ccipRouterAddress = 0xd0daae2231e9cb96b94c8512223533293c3693bf;
        address simplifiedStakingAddress = "PUT SIMPLIFIED_STAKING ADDRESS HERE";

        CrossChainReceiver crossChainReceiver = new CrossChainReceiver(
            ccipRouterAddress,
            simplifiedStakingAddress
        );

        console.log(
            "CrossChainReceiver deployed to ",
            address(crossChainReceiver)
        );

        vm.stopBroadcast();
    }
}
```

Deploy `CrossChainReceiver` smart contract by running:

```sh
forge script ./script/CrossChainReceiver.s.sol:DeployCrossChainReceiver -vvv --broadcast --rpc-url ethereumSepolia
```

{% endtab %}

{% tab title="Remix" %}
Open your Metamask wallet and switch to the Ethereum Sepolia network.

Open the CrossChainReceiver.sol file.

Navigate to the "Solidity Compiler" tab and click the "Compile CrossChainReceiver.sol" button.

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that `chainId` is switched to 11155111 (if not, you may need to refresh the Remix IDE page in your browser).

Under the "Contract" dropdown menu, make sure that the "CrossChainReceiver - CrossChainReceiver.sol" is selected.

Locate the orange "Deploy" button. Provide `0xd0daae2231e9cb96b94c8512223533293c3693bf` as the `ccipRouterAddress` and the address of previously deployed smart contract (SimplifiedStaking.sol) as `simplifiedStakingAddress`

Click the orange "Deploy"/"Transact" button.

Metamask notification will pop up. Sign the transaction.
{% endtab %}
{% endtabs %}

## Deploy CrossChainSender.sol on Avalanche Fuji

{% tabs %}
{% tab title="Hardhat" %}
Navigate to the `scripts` folder and create new file named `deployCrossChainSender.ts`

<pre class="language-typescript"><code class="lang-typescript"><strong>// deployCrossChainSender.ts
</strong>
import { ethers, network } from "hardhat";

async function main() {
  const ccipRouterAddressAvalancheFuji = `0x554472a2720e5e7d5d3c817529aba05eed5f82d8`;
  const linkTokenAddressAvalancheFuji = `0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846`;
  const stakingTokenAddressAvalancheFuji = `0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4`; // CCIP-BnM

  const crossChainSender = await ethers.deployContract("CrossChainSender", [
    ccipRouterAddressAvalancheFuji,
    linkTokenAddressAvalancheFuji,
    stakingTokenAddressAvalancheFuji,
  ]);

  await crossChainSender.waitForDeployment();

  console.log(`CrossChainSender deployed on ${network.name} with address ${crossChainSender.target}`);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
</code></pre>

Run the deployment script:

```bash
npx hardhat run ./scripts/deployCrossChainSender.ts --network avalancheFuji
```

{% endtab %}

{% tab title="Foundry" %}
**Option 1)**

Deploy `CrossChainSender.sol` smart contract by running:

```sh
forge create --rpc-url avalancehFuji --private-key=$PRIVATE_KEY src/CrossChainSender.sol:CrossChainSender --constructor-args 0x554472a2720e5e7d5d3c817529aba05eed5f82d8 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4 
```

#### Option 2)

Create a new smart contract under the `script` folder and name it `CrossChainSender.s.sol`

Note that deployment of the `CrossChainSender` smart contract is hard coded to Ethereum Sepolia for this example, but feel free to refactor the following deployment script to support other networks. You can check [CCIP Starter Kit (Foundry version)](https://github.com/smartcontractkit/ccip-starter-kit-foundry) for reference.

```solidity
// script/CrossChainSender.s.sol

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import {CrossChainSender} from "../src/CrossChainSender.sol";

contract DeployCrossChainSender is Script {
    function run() public {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        address ccipRouterAddressAvalancheFuji = 0x554472a2720e5e7d5d3c817529aba05eed5f82d8;
        address linkTokenAddressAvalancheFuji = 0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846;
        address stakingTokenAddressAvalancheFuji = 0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4; // CCIP-BnM

        CrossChainSender crossChainSender = new CrossChainSender(
            ccipRouterAddressAvalancheFuji,
            linkTokenAddressAvalancheFuji,
            stakingTokenAddressAvalancheFuji
        );

        console.log(
            "CrossChainSender deployed to ",
            address(crossChainSender)
        );

        vm.stopBroadcast();
    }
}
```

Deploy `CrossChainSender` smart contract by running:

```sh
forge script ./script/CrossChainSender.s.sol:DeployCrossChainSender -vvv --broadcast --rpc-url avalancheFuji
```

{% endtab %}

{% tab title="Remix" %}
Open your Metamask wallet and switch to the Avalanche Fuji network.

Open the CrossChainSender.sol file.

Navigate to the "Solidity Compiler" tab and click the "Compile CrossChainSender.sol" button.

Navigate to the "Deploy & run transactions" tab and select the "Injected Provider - Metamask" option from the "Environment" dropdown menu. Make sure that `chainId` is switched to 43113 (if not, you may need to refresh the Remix IDE page in your browser).

Under the "Contract" dropdown menu, make sure that the "CrossChainSender - CrossChainSender.sol" is selected.

Locate the orange "Deploy" button. Provide `0x554472a2720e5e7d5d3c817529aba05eed5f82d8` as the `ccipRouterAddress`, `0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846` as the linkTokenAddress, and `0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4` (CCIP-BnM) as the `stakingTokenAddress`

Click the orange "Deploy"/"Transact" button.

Metamask notification will pop up. Sign the transaction.
{% endtab %}
{% endtabs %}

## Fund CrossChainSender.sol with some LINK for CCIP fees

Transfer at least 1 LINK to CrossChainSender.sol on Avalanche Fuji to cover CCIP fees.

<figure><img src="/files/OhFOtwWxtoi9mzJgrrWZ" alt="" width="354"><figcaption><p>Transfer 1 LINK to CrossChainSender</p></figcaption></figure>

## Approve CrossChainSender.sol to spend some amount of your CCIP-BnM tokens

Call `approve` function of the CCIP-BnM token smart contract and put an address of a CrossChainSender.sol smart contract as spender, and 100 as an amount.

{% embed url="<https://testnet.snowtrace.io/address/0xD21341536c5cF5EB1bcb58f6723cE26e8D8E90e4#writeContract-43113>" %}

<figure><img src="/files/QYHXIzpBXrg8oO7sRdhr" alt=""><figcaption><p>Block explorer</p></figcaption></figure>

<figure><img src="/files/dMdYMPrgeSyLMilkButM" alt=""><figcaption><p>Approve 100 CCIP-BnM to be spent by CrossChainSender.sol</p></figcaption></figure>

## Call allowlistDestinationChain function of CrossChainSender.sol on Avalanche Fuji

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;
* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.

Create a new JavaScript/TypeScript file under the `scripts` folder and name it `allowlistDestinationChain.js`/`allowlistDestinationChain.ts`

```typescript
// scripts/allowlistDestinationChain.ts

import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { CrossChainSender, CrossChainSender__factory } from "../typechain-types";

async function main() {
  if (network.name !== `avalancheFuji`) {
    console.error(`❌ Must be called from Avalanche Fuji`);
    return 1;
  }

  const privateKey = process.env.PRIVATE_KEY!;
  const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;

  const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
  const wallet = new Wallet(privateKey);
  const signer = wallet.connect(provider);

  const crossChainSenderAddress = `PUT CROSS_CHAIN_SENDER ADDRESS HERE`;
  const destinationChainSelector = `16015286601757825753`;

  const crossChainSender: CrossChainSender = CrossChainSender__factory.connect(crossChainSenderAddress, signer);

  const tx = await crossChainSender.allowlistDestinationChain(destinationChainSelector, true);

  console.log(`Transaction hash: ${tx.hash}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/allowlistDestinationChain.ts --network avalancheFuji
```

Or for JavaScript:

```sh
npx hardhat run .npx hardhat run ./scripts/allowlistDestinationChain.js --network avalancheFuji/scripts/sendMessage.js --network avalancheFuji
```

{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;
* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.

Run:

```sh
cast send <CROSS_CHAIN_SENDER_ADDRESS> --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "allowlistDestinationChain(uint64,bool)" 16015286601757825753 true
```

{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `CrossChainSender.sol` contract you previously deployed to Avalanche Fuji. Find the `allowlistDestinationChain` function and provide:

* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.
* `true` as `allowed` parameter

Hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

## Call allowlistSender function of CrossChainReceiver.sol on Ethereum Sepolia

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia;
* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;

Create a new JavaScript/TypeScript file under the `scripts` folder and name it `allowlistSender.js`/`allowlistSender.ts`

```typescript
// scripts/allowlistSender.ts

import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { CrossChainReceiver, CrossChainReceiver__factory } from "../typechain-types";

async function main() {
  if (network.name !== `ethereumSepolia`) {
    console.error(`❌ Must be called from Ethereum Sepolia`);
    return 1;
  }

  const privateKey = process.env.PRIVATE_KEY!;
  const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;

  const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
  const wallet = new Wallet(privateKey);
  const signer = wallet.connect(provider);

  const crossChainReceiverAddress = `PUT CROSS_CHAIN_SENDER ADDRESS HERE`;
  const crossChainSenderAddress = `PUT CROSS_CHAIN_SENDER ADDRESS HERE`;

  const crossChainReceiver: CrossChainReceiver = CrossChainReceiver__factory.connect(crossChainReceiverAddress, signer);

  const tx = await crossChainReceiver.allowlistSender(crossChainSenderAddress, true);

  console.log(`Transaction hash: ${tx.hash}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/allowlistSender.ts --network ethereumSepolia
```

Or for JavaScript:

```sh
npx hardhat run ./scripts/allowlistSender.js --network ethereumSepolia
```

{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia;
* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;

Run:

```sh
cast send <CROSS_CHAIN_RECEIVER_ADDRESS> --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "allowlistSender(address,bool)" <CROSS_CHAIN_SENDER_ADDRESS> true
```

{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `CrossChainReceiver.sol` contract you previously deployed to Ethereum Sepolia. Find the `allowlistSender` function and provide:

* Address of the `CrossChainSender.sol` smart contract you previously deployed on Avalanche Fuji
* `true` as `allowed` parameter

Hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

## Call allowlistSourceChain function of CrossChainReceiver.sol on Ethereum Sepolia

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia;
* 14767482510784806043, which is the CCIP Chain Selector for the Avalanche Fuji network, as the `sourceChainSelector` parameter.

Create a new JavaScript/TypeScript file under the `scripts` folder and name it `allowlistSourceChain.js`/`allowlistSourceChain.ts`

```typescript
// scripts/allowlistSourceChain.ts

import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { CrossChainReceiver, CrossChainReceiver__factory } from "../typechain-types";

async function main() {
  if (network.name !== `ethereumSepolia`) {
    console.error(`❌ Must be called from Ethereum Sepolia`);
    return 1;
  }

  const privateKey = process.env.PRIVATE_KEY!;
  const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;

  const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
  const wallet = new Wallet(privateKey);
  const signer = wallet.connect(provider);

  const crossChainReceiverAddress = `PUT CROSS_CHAIN_SENDER ADDRESS HERE`;
  const sourceChainSelector = `14767482510784806043`;

  const crossChainReceiver: CrossChainReceiver = CrossChainReceiver__factory.connect(crossChainReceiverAddress, signer);

  const tx = await crossChainReceiver.allowlistSourceChain(sourceChainSelector, true);

  console.log(`Transaction hash: ${tx.hash}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/allowlistSourceChain.ts --network ethereumSepolia
```

Or for JavaScript:

```sh
npx hardhat run ./scripts/allowlistSourceChain.js --network ethereumSepolia
```

{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia;
* 14767482510784806043, which is the CCIP Chain Selector for the Avalanche Fuji network, as the `sourceChainSelector` parameter.

Run:

```sh
cast send <CROSS_CHAIN_RECEIVER_ADDRESS> --rpc-url ethereumSepolia --private-key=$PRIVATE_KEY "allowlistSourceChain(uint64,bool)" 14767482510784806043 true
```

{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `CrossChainReceiver.sol` contract you previously deployed to Ethereum Sepolia. Find the `allowlistSourceChain` function and provide:

* 14767482510784806043, which is the CCIP Chain Selector for the Avalanche Fuji network, as the `sourceChainSelector` parameter.
* `true` as `allowed` parameter

Hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

## Send cross-chain message using CrossChainSender on Avalanche Fuji

{% tabs %}
{% tab title="Hardhat" %}
Prepare:

* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;
* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.
* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia, as `receiver`;
* The address you want to receive CCIP-LnM lpTokens, it can be your EOA address for example, as `onBehalfOf`
* The amount of CCIP-BnM tokens you want to send, let's put 100, as `amount`

Create a new JavaScript/TypeScript file under the `scripts` folder and name it `allowlistSourceChain.js`/`allowlistSourceChain.ts`

```typescript
// scripts/stake.ts

import { ethers, network } from "hardhat";
import { Wallet } from "ethers";
import { CrossChainSender, CrossChainSender__factory } from "../typechain-types";

enum PayFeesIn {
  Native,
  LINK,
}

async function main() {
  if (network.name !== `avalancheFuji`) {
    console.error(`❌ Must be called from Avalanche Fuji`);
    return 1;
  }

  const privateKey = process.env.PRIVATE_KEY!;
  const rpcProviderUrl = process.env.AVALANCHE_FUJI_RPC_URL;

  const provider = new ethers.JsonRpcProvider(rpcProviderUrl);
  const wallet = new Wallet(privateKey);
  const signer = wallet.connect(provider);

  const crossChainSenderAddress = `PUT CROSS_CHAIN_SENDER ADDRESS HERE`;
  const destinationChainSelector = `16015286601757825753`;
  const receiverAddress = `PUT CROSS_CHAIN_RECEIVER ADDRESS HERE`;
  const onBehalfOf = `PUT YOUR WALLET ADDRESS HERE`;
  const amount = 100;
  const payFeesIn = PayFeesIn.LINK;
  const gasLimit = 500_000;

  const crossChainSender: CrossChainSender = CrossChainSender__factory.connect(crossChainSenderAddress, signer);

  const tx = await crossChainSender.send(destinationChainSelector, receiverAddress, onBehalfOf, amount, payFeesIn, gasLimit);

  console.log(`Transaction hash: ${tx.hash}`);
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
```

Call the function by running the following command:

```sh
npx hardhat run ./scripts/stake.ts --network avalancheFuji 
```

Or for JavaScript:

```sh
npx hardhat run ./scripts/stake.js --network avalancheFuji 
```

{% endtab %}

{% tab title="Foundry" %}
Prepare:

* The address of the address of the `CrossChainSender.sol` smart contract you previously deployed to Avalanche Fuji;
* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.
* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia, as `receiver`;
* The address you want to receive CCIP-LnM lpTokens, it can be your EOA address for example, as `onBehalfOf`
* The amount of CCIP-BnM tokens you want to send, let's put 100, as `amount`

Run:

```sh
cast send <CROSS_CHAIN_SENDER_ADDRESS> --rpc-url avalancheFuji --private-key=$PRIVATE_KEY "send(uint64,address,address,uint256,uint8,uint256)" 16015286601757825753 <CCIP_RECEIVER_ADDRESS> <ON_BEHALF_OF_ADDRESS> 100 1 500000 
```

{% endtab %}

{% tab title="Remix" %}
Under the "Deployed Contracts" section, you should find the `CrossChainSender.sol` contract you previously deployed to Ethereum Sepolia. Find the `send` function and provide:

* 16015286601757825753, which is the CCIP Chain Selector for the Ethereum Sepolia network, as the `destinationChainSelector` parameter.
* The address of the address of the `CrossChainReceiver.sol` smart contract you previously deployed to Ethereum Sepolia, as `receiver` parameter;
* The address you want to receive CCIP-LnM lpTokens, it can be your EOA address for example, as `onBehalfOf` parameter;
* The amount of CCIP-BnM tokens you want to send, let's put 100, as `amount` parameter;
* `1` as the `payFeesIn` parameter, saying that we are paying for CCIP fees in LINK tokens
* `500000` as the `gasLimit` parameter; For production ready codebases, you would like to measure this parameter upfront to avoid spending more for fees.

Hit the "Transact" orange button.
{% endtab %}
{% endtabs %}

You can now monitor live the status of your CCIP Cross-Chain Message via [CCIP Explorer](https://ccip.chain.link/). Just paste the transaction hash into the search bar and open the message details.

<figure><img src="/files/Ych1rmnjAoTOdnUZpiGj" alt=""><figcaption><p>CCIP Explorer</p></figcaption></figure>

## Transfer LP Tokens you got to Polygon Mumbai

One of the great things with CCIP is that now we can send LP tokens to some third chain if we want to put them inside some DeFi product there.

To do that, we can now reuse Tiny token transferor contract (`CCIPTokenSender_Unsafe.sol`) from Exercise #1, redeploy it to Ethereum Sepolia, and send LP tokens to Polygon Mumbai.

Obviously, you can accomplish the same thing by interacting with the CCIP Router contract directly. To do that, check CCIP Starter Kits, both Hardhat and Foundry versions.

## Bonus exercise - Homework

Expand `CrossChainSender.sol` and `CrossChainReceiver.sol` smart contract. When `CrossChainReceiver.sol` receives a cross-chain message, after calling a `stake` function of `SimplifiedStaking.sol` smart contract, it should send a "reply cross chain message" back to `CrossChainSender.sol` specifying how many LP tokens `onBehalfOf` address got.&#x20;

`CrossChainSender.sol` upon receiving a "reply cross chain message" store that info inside the `liquidTokensAmount` mapping which defined at the top of this Solidity file:

```solidity
mapping(address onBehalfOf => uint256 ccipLnMAmount) public liquidTokensAmount;
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://cll-devrel.gitbook.io/ccip-masterclass-2/ccip-masterclass/exercise-2-cross-chain-staking.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
