Hardhat Installation & Deploy Liquidity Provision Contract that integrates with X-Talk Swap

Liquidity Provision Contract integrates with X-Talk Swap Contract to provide liquidity for swaps. Liquidity provision contracts are one of the examples of the use case of X-Talk Swaps.

Step 1: Initialize a New Project

  1. Create a New Directory (if you're starting fresh):

    mkdir stake-contract
    cd stake-contract
  2. Initialize a new NPM project:

    npm init -y

Step 2: Install Hardhat and Set Up the Project

  1. Install Hardhat:

    npm install --save-dev hardhat
  2. Set up the Hardhat project: Run the setup command and choose to create a TypeScript project:

    npx hardhat init

    When prompted, select to create a TypeScript project. Follow the prompts to add a .gitignore and install the project's dependencies.

Step 3: Install Necessary Plugins and Dependencies

  1. Install TypeScript-related dependencies:

    npm install --save-dev ts-node typescript @types/node @types/mocha
  2. Install OpenZeppelin Contracts:

    npm install @openzeppelin/contracts
  3. Install Ethers and Hardhat Ethers (ensure compatibility):

    npm install --save-dev @nomiclabs/hardhat-ethers ethers
    npm install hardhat-gas-reporter
    npm install @nomicfoundation/hardhat-toolbox
    npm install hardhat/config

At this stage, you project structure would look like this.

stake-contract/
├── contracts/
├── scripts/
├── test/
├── hardhat.config.js
└── package.json

Step 4: Write your smart contract

Create the Contract: Create StakeContract.sol inside the contracts directory and paste your contract code there.

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface ISwapContract {
    function transferToStake(string memory _internalId) external;
}

contract StakeV2Contract is Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    string public DESTINATION_NETWORK = "L1X";
    string public NATIVE_ASSET_SYMBOL = "ETH";
    string public NATIVE_SYMBOL = "ETH";
    address public constant NATIVE_ASSET_ADDRESS = address(0);
    uint256 public NATIVE_ASSET_DECIMAL = 18;
    address public SWAP_CONTRACT_ADDRESS = address(0);

    uint256 public depositFeePercent;

    // Withdrawal Penalties
    uint256 public withdrawalPenaltyInDays;
    uint256 public withdrawalPenaltyInPercentage;

    // Mapping to store asset config information
    mapping(address => mapping(uint256 => AssetConfiguration)) public assetConfig;

    // Mapping to store asset capping
    mapping(address => CappingConfiguration) public assetDepositCapConfig;

    // Mapping to store user staking information
    mapping(string => Stake) userStakesByTxId;

    // Store Flagged Withdrawl
    mapping(string => FlagWithdrawal) stakeFlagByTxId;

    // Authorized caller
    mapping(address => bool) public authorizedCaller;

    // Treasury contract address
    address public treasuryContract;

    // Pause Feature
    bool public isDepositPaused;
    bool public isWithdrawalPaused;

    // Structure to store staking details
    // Note: Native Asset is represented by address(0)
    struct Stake {
        uint256 startDate;
        uint256 durationInDays;
        uint256 amount;
        address assetAddress;
        address senderAddress;
        address rewardAddress;
        string assetSymbol;
        uint256 assetDecimal;
        uint256 apr;
        bool isERC20;
        bool isRedemed;
        uint256 redemedTimestamp;
        uint256 penaltyAmount;
        uint256 penaltyPercentage;
        uint256 depositFeePercent;
    }

    struct FlagWithdrawal {
        bool holdWithdrawl;
        string message;
        address caller;
    }

    struct AssetConfiguration {
        uint16 apr;
        uint16 treasuryPercentage;
    }

    struct CappingConfiguration {
        uint256 maxCap;
        uint256 totalDeposited;
    }

    event StakeDeposited(
        string internalId,
        address indexed walletAddress,
        address indexed assetAddress, // 0x0 in case of Native
        address rewardAddress,
        string assetSymbol,
        uint256 assetDecimal,
        uint256 apr,
        uint256 amount,
        uint256 durationInDays,
        uint256 depositFeePercent,
        uint256 stakeType, // 0 = Native , 1 - ERC-20
        uint256 timestamp
    );

    event StakeWithdraw(
        string internalId,
        address indexed walletAddress,
        address indexed assetAddress, // 0x0 in case of Native
        address rewardAddress,
        string assetSymbol,
        uint256 assetDecimal,
        uint256 apr,
        uint256 amount,
        uint256 amountTransferred,
        uint256 penaltyAmount,
        uint256 durationInDays,
        uint256 withdrawalPenaltyInPercentage,
        uint256 stakeType, // 0 = Native , 1 - ERC-20
        uint256 timestamp
    );

    event AuthorizationUpdated(
        address indexed sender, 
        bool status
    );

    // XTalk Events
    event XTalkMessageBroadcasted(
        bytes message,
        string destinationNetwork,
        string destinationSmartContractAddress
    );

    event SwapContractAddress ( address swapContract );

    modifier onlyAuthorizedCaller() {
        require(
            msg.sender == owner() || authorizedCaller[msg.sender] == true,
            "Not authorized"
        );
        _;
    }

    constructor(
        string memory _nativeSymbol,
        string memory _nativeAssetSymbol,
        uint256 _nativeAssetDecimal,
        address _treasuryContract,
        uint256 _depositFeePercent,
        uint256 _withdrawalPenaltyInDays,
        uint256 _withdrawalPenaltyInPercentage
    ) Ownable(msg.sender) {
        NATIVE_SYMBOL = _nativeSymbol;
        NATIVE_ASSET_SYMBOL = _nativeAssetSymbol;
        NATIVE_ASSET_DECIMAL = _nativeAssetDecimal;

        treasuryContract = _treasuryContract;
        depositFeePercent = _depositFeePercent;

        // Withdrawal Penalty configuration
        withdrawalPenaltyInDays = _withdrawalPenaltyInDays;
        withdrawalPenaltyInPercentage = _withdrawalPenaltyInPercentage;
    }

    function getStakeDetails(
        string memory internalId
    ) public view returns (Stake memory) {
        return userStakesByTxId[internalId];
    }

    function getStakeWithdrawFlaggedDetails(
        string memory internalId
    ) public view returns (FlagWithdrawal memory) {
        return stakeFlagByTxId[internalId];
    }

    // Owner can configure deposit status
    function pauseDeposit(
        bool depositStatus
    ) external onlyAuthorizedCaller {
        isDepositPaused = depositStatus;
    }

    // Owner can configure withdrawal status
    function pauseWithdrawal(
        bool withdrawalStatus
    ) external onlyAuthorizedCaller {
        isWithdrawalPaused = withdrawalStatus;
    }

    // Owner can configure withdrawal penalty in days
    function setMinimumWithdrawalDays(
        uint256 penaltyInDays
    ) external onlyAuthorizedCaller {
        withdrawalPenaltyInDays = penaltyInDays;
    }

    // Owner can configure withdrawal penalty in percentage
    function setWithdrawalPenaltyPercent(
        uint256 penaltyInPercentage
    ) external onlyAuthorizedCaller {
        require(
            penaltyInPercentage >= 0 && penaltyInPercentage <= 100,
            "Percent should be between 0 and 100"
        );
        withdrawalPenaltyInPercentage = penaltyInPercentage;
    }

    // Owner can configure staking treasury contract
    function setTreasuryAddress(
        address treasuryContractAddress
    ) external onlyAuthorizedCaller {
        treasuryContract = treasuryContractAddress;
    }

    // Owner can configure deposit fee percent
    function setDepositFeePercent(
        uint256 _depositFeePercent
    ) external onlyAuthorizedCaller {
        require(
            _depositFeePercent <= 100,
            "Deposit Fee should be less than 100"
        );
        depositFeePercent = _depositFeePercent;
    }

    // Update the Gas Price Contract address (only callable by the owner)
    function updateSwapContractAddress(
        address newAddress
    ) external onlyAuthorizedCaller {
        require(
            newAddress != address(0),
            "Address cannot be the zero address."
        );

        SWAP_CONTRACT_ADDRESS = newAddress;
        emit SwapContractAddress(newAddress);
    }

    // Function to transfer asset to treasury
    function transferToTreasury(
        address tokenAddress,
        uint256 amount
    ) external onlyAuthorizedCaller {
        require(treasuryContract != address(0), "Treasury address is not set");

        if (tokenAddress == NATIVE_ASSET_ADDRESS) {
            // Send Native to Treasury with reentrancy Protection for Native
            (bool sent, bytes memory data) = treasuryContract.call{
                value: amount
            }("");
        } else {
            // Send Native to Treasury with reentrancy Protection for ERC20
            IERC20(tokenAddress).safeTransfer(treasuryContract, amount);
        }
    }

    // Pause and Start withdraw for perticular stake ID
    function flagWithdrawRequest(
        string memory internalId,
        bool holdWithdrawl,
        string memory message
    ) public onlyAuthorizedCaller returns (bool) {
        stakeFlagByTxId[internalId].holdWithdrawl = holdWithdrawl;
        stakeFlagByTxId[internalId].message = message;
        stakeFlagByTxId[internalId].caller = msg.sender;

        return true;
    }

    // Set locking configuration
    function setAssetConfiguration(
        address assetAddress,
        uint256 durationInDays,
        uint16 apr,
        uint16 treasuryPercentage
    ) external onlyAuthorizedCaller {
        assetConfig[assetAddress][durationInDays] = AssetConfiguration(apr, treasuryPercentage);
    }

    function bulkAssetConfigurationUpdate(
        address[] memory assetAddress,
        uint256[] memory maxCap,
        uint256[] memory durationInDays,
        uint16[][] memory apr,
        uint16[][] memory treasuryPercentage
    ) public onlyAuthorizedCaller returns (bool) {
        require(
            assetAddress.length == maxCap.length &&
            apr.length == treasuryPercentage.length,
            "Array lengths must match"
        );

        for (uint256 _addressIndex = 0; _addressIndex < assetAddress.length; _addressIndex++) {
            address _assetAddress = assetAddress[_addressIndex];
            assetDepositCapConfig[_assetAddress].maxCap = maxCap[_addressIndex];

            for (uint256 _daysIndex = 0; _daysIndex < durationInDays.length; _daysIndex++) {
                uint256 _durationInDays = durationInDays[_daysIndex];
                uint16 _apr = apr[_addressIndex][_daysIndex];
                uint16 _treasuryPercentage = treasuryPercentage[_addressIndex][_daysIndex];

                assetConfig[_assetAddress][_durationInDays] = AssetConfiguration(_apr, _treasuryPercentage);
            }
        }
    }


    // Set asset cap
    function setAssetDepositCapConfig(
        address assetAddress,
        uint256 maxCap
    ) external onlyAuthorizedCaller {
        assetDepositCapConfig[assetAddress].maxCap = maxCap;
    }

    // Set asset cap
    function resetAssetDepositedAmount(
        address assetAddress,
        uint256 totalDeposited
    ) external onlyAuthorizedCaller {
        assetDepositCapConfig[assetAddress].totalDeposited = totalDeposited;
    }
    

    // Set an authorized caller
    function setAuthorizedCaller(
        address sender,
        bool status
    ) external onlyOwner {
        authorizedCaller[sender] = status;
        emit AuthorizationUpdated(sender, status);
    }

    // User can deposit stake (ERC20)
    function depositForERC20(
        uint256 durationInDays,
        address rewardAddress,
        string memory assetSymbol,
        address assetAddress,
        uint256 amount,
        uint256 decimals,
        string memory internalId
    ) external nonReentrant {
        require(isDepositPaused == false, "Deposit is paused");
        require(userStakesByTxId[internalId].amount == 0, "Stake with given internal ID already exist");
        require(amount > 0, "Amount must be greater than zero.");
        uint256 totalDeposit = amount + assetDepositCapConfig[assetAddress].totalDeposited;
        require(totalDeposit <= assetDepositCapConfig[assetAddress].maxCap, "Max capping reached. Please try with lesser amount");
        require(decimals > 0, "Decimals must be greater than 0");
        require(rewardAddress != address(0), "Invalid Reward Address");
        require(assetAddress != address(0), "Invalid Asset Address");
        require(SWAP_CONTRACT_ADDRESS != address(0), "Invalid Swap Address");
        require(isEmptyString(assetSymbol) == false, "Invalid Asset Symbol");

        // Call the internal transfer function
        require(
            transferAsset(
                assetAddress, 
                msg.sender, 
                address(this), 
                amount, 
                0
            ), 
            "Transfer failed"
        );


        // Handle Treeasury Share
        uint256 swapDistribution = calulatePercentAmount(assetConfig[assetAddress][durationInDays].treasuryPercentage, amount);
        if (swapDistribution > 0) {
            transferAssetForWithdrawal(
                assetAddress,
                address(this),
                SWAP_CONTRACT_ADDRESS,
                swapDistribution
            );
        }

        userStakesByTxId[internalId] = Stake(
            getCurrentTimestamp(),
            durationInDays,
            amount,
            assetAddress,
            msg.sender,
            rewardAddress,
            assetSymbol,
            decimals,
            assetConfig[assetAddress][durationInDays].apr,
            true,
            false,
            0,
            0,
            0,
            depositFeePercent
        );

        assetDepositCapConfig[assetAddress].totalDeposited = totalDeposit;

        bytes memory messageBytes = abi.encode(
            internalId,
            msg.sender,
            assetAddress,
            rewardAddress,
            NATIVE_SYMBOL,
            assetSymbol,
            decimals,
            assetConfig[assetAddress][durationInDays].apr,
            amount,
            durationInDays,
            depositFeePercent,
            1,
            getCurrentTimestamp()
        );

        // Broadcast cross-network message
        emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');

        emit StakeDeposited(
            internalId,
            msg.sender,
            assetAddress,
            rewardAddress,
            assetSymbol,
            decimals,
            assetConfig[assetAddress][durationInDays].apr,
            amount,
            durationInDays,
            depositFeePercent,
            1,
            getCurrentTimestamp()
        );
    }

    // User can deposit stake (Native)
    function depositForNative(
        uint256 durationInDays,
        address rewardAddress,
        string memory internalId
    ) external payable nonReentrant {
        require(isDepositPaused == false, "Deposit is paused");
        require(userStakesByTxId[internalId].amount == 0, "Stake with given internal ID already exist");
        require(rewardAddress != address(0), "Invalid Reward Address");
        require(SWAP_CONTRACT_ADDRESS != address(0), "Invalid Swap Address");
        
        uint256 amount = msg.value;
        require(amount > 0, "Amount must be greater than zero.");
        uint256 totalDeposit = amount + assetDepositCapConfig[NATIVE_ASSET_ADDRESS].totalDeposited;
        require(totalDeposit <= assetDepositCapConfig[NATIVE_ASSET_ADDRESS].maxCap, "Max capping reached. Please try with lesser amount");

        // Handle Treeasury Share
        uint256 swapDistribution = calulatePercentAmount(assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].treasuryPercentage, amount);
        if (swapDistribution > 0) {
            uint256 nativeAmount = msg.value;

            // Call the internal transfer function
            require(
                transferAsset(
                    NATIVE_ASSET_ADDRESS, 
                    msg.sender,
                    SWAP_CONTRACT_ADDRESS,
                    swapDistribution, 
                    nativeAmount
                ), 
                "Transfer failed"
            );
        }

        // Store Stake
        userStakesByTxId[internalId] = Stake(
            getCurrentTimestamp(),
            durationInDays,
            amount,
            NATIVE_ASSET_ADDRESS,
            msg.sender,
            rewardAddress,
            NATIVE_ASSET_SYMBOL,
            NATIVE_ASSET_DECIMAL,
            assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
            false,
            false,
            0,
            0,
            0,
            depositFeePercent
        );

        assetDepositCapConfig[NATIVE_ASSET_ADDRESS].totalDeposited = totalDeposit;

        bytes memory messageBytes = abi.encode(
            internalId,
            msg.sender,
            NATIVE_ASSET_ADDRESS,
            rewardAddress,
            NATIVE_SYMBOL,
            NATIVE_ASSET_SYMBOL,
            NATIVE_ASSET_DECIMAL,
            assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
            amount,
            durationInDays,
            depositFeePercent,
            0,
            getCurrentTimestamp()
        );

        // Broadcast cross-network message
        emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');
        emit StakeDeposited(
            internalId,
            msg.sender,
            NATIVE_ASSET_ADDRESS,
            rewardAddress,
            NATIVE_ASSET_SYMBOL,
            NATIVE_ASSET_DECIMAL,
            assetConfig[NATIVE_ASSET_ADDRESS][durationInDays].apr,
            amount,
            durationInDays,
            depositFeePercent,
            0,
            getCurrentTimestamp()
        );
    }

    function checkWithdrawStakeById(string memory internalId) public view returns(bool) {
        require(
            isStakeUnlocked(internalId) == true,
            "Given Stake is not available for withdrawal yet"
        );

        // Retrieve Stake
        Stake memory stake = userStakesByTxId[internalId];

        return stake.isRedemed; 
    }

    function withdrawStakeById(string memory internalId) external nonReentrant {
        require(isWithdrawalPaused == false, "Withdrawal is paused");
        require(
            stakeFlagByTxId[internalId].holdWithdrawl == false,
            "This internalId is flagged please contact admin"
        );
        require(
            isStakeUnlocked(internalId) == true,
            "Given Stake is not available for withdrawal yet"
        );

        // Retrieve Stake
        Stake memory stake = userStakesByTxId[internalId];

        // Check if already redeemed
        require(stake.isRedemed == false, "Stake is already withdrawn");

        (
            address _tokenAddress,
            uint256 _amount,
            uint256 amountAfterPenalty,
            uint256 penaltyAmount,
            uint256 depositFee,
            uint256 amountToTransfer
        ) = validateRequiredAmount(internalId);

        if (_amount > 0) {
            require(SWAP_CONTRACT_ADDRESS!= address(0), "Invalid swap address");
            ISwapContract(SWAP_CONTRACT_ADDRESS).transferToStake(internalId);
        }

        // Update Stake Status - set isRedemed to true
        userStakesByTxId[internalId].isRedemed = true;
        userStakesByTxId[internalId].penaltyAmount = penaltyAmount;
        userStakesByTxId[internalId].redemedTimestamp = getCurrentTimestamp();

        if(penaltyAmount > 0){
            userStakesByTxId[internalId].penaltyPercentage = withdrawalPenaltyInPercentage;
        }
        
        // Transfer unstaked funds to contract
        if(amountToTransfer > 0){
            transferAssetForWithdrawal(stake.assetAddress,address(this),stake.senderAddress,amountToTransfer);
        }


        bytes memory messageBytes = abi.encode(
            internalId,
            stake.senderAddress,
            stake.assetAddress, // 0x0 in case of Native
            stake.rewardAddress,
            NATIVE_SYMBOL,
            stake.assetSymbol,
            stake.assetDecimal,
            stake.apr,
            stake.amount,
            amountToTransfer,
            penaltyAmount,
            stake.durationInDays,
            withdrawalPenaltyInPercentage,
            stake.assetAddress == NATIVE_ASSET_ADDRESS ? 0 : 1, // 0 = Native , 1 - ERC-20 ,
            getCurrentTimestamp()
        );

        emit XTalkMessageBroadcasted(messageBytes, DESTINATION_NETWORK, '');
        emit StakeWithdraw(
            internalId,
            stake.senderAddress,
            stake.assetAddress, // 0x0 in case of Native
            stake.rewardAddress,
            stake.assetSymbol,
            stake.assetDecimal,
            stake.apr,
            stake.amount,
            amountToTransfer,
            penaltyAmount,
            stake.durationInDays,
            withdrawalPenaltyInPercentage,
            stake.assetAddress == NATIVE_ASSET_ADDRESS ? 0 : 1, // 0 = Native , 1 - ERC-20 ,
            getCurrentTimestamp()
        );
    }

    function validateRequiredAmount(string memory internalId) public view returns (
        address, 
        uint256,
        uint256,
        uint256,
        uint256,
        uint256
    ) {
        address _tokenAddress = userStakesByTxId[internalId].assetAddress;
        uint256 _stakeAmount = userStakesByTxId[internalId].amount;
        uint256 _depositFeePercent = userStakesByTxId[internalId].depositFeePercent;
        bool _isRedeemed = userStakesByTxId[internalId].isRedemed;
        require(_isRedeemed == false, "Stake is already withdrawn");

        (
            uint256 amountAfterPenalty,
            uint256 penaltyAmount
        ) = calculateAmountLeftAfterPenalty(internalId);
        uint256 _depositFee = calulatePercentAmount(_depositFeePercent, _stakeAmount);
        uint256 _requiredAmount = amountAfterPenalty - _depositFee;
        require(_requiredAmount > 0, "Invalid internal id");

        uint256 _balance;
        if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
            // Check native balance (ETH)
            _balance = address(this).balance;
        } else {
            // Check ERC20 token balance
            IERC20 token = IERC20(_tokenAddress);
            _balance = token.balanceOf(address(this));
        }

        uint256 _requiredSwapBalance;
        if (_balance >= _requiredAmount) {
            _requiredSwapBalance = 0;
        } else {
            uint256 swapContractBalance;
            if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
                // Check native balance (ETH) of SWAP_CONTRACT_ADDRESS
                swapContractBalance = SWAP_CONTRACT_ADDRESS.balance;
            } else {
                // Check ERC20 token balance of SWAP_CONTRACT_ADDRESS
                IERC20 token = IERC20(_tokenAddress);
                swapContractBalance = token.balanceOf(SWAP_CONTRACT_ADDRESS);
            }
            _requiredSwapBalance = _requiredAmount - _balance;
            require(swapContractBalance >= _requiredSwapBalance, "Insufficient liquidity.");
        }

        return (
            _tokenAddress, 
            _requiredSwapBalance,
            amountAfterPenalty,
            penaltyAmount,
            _depositFee,
            _requiredAmount
        );
    }

    // User can calculate amount after subtracting penalty
    function calculateAmountLeftAfterPenalty(
        string memory internalId
    ) public view returns (uint256, uint256) {
        Stake memory stake = userStakesByTxId[internalId];

        require(stake.startDate > 0, "Invalid Start Date");
        require(stake.isRedemed == false, "Already Unstaked the amount");

        uint256 penaltyAmount = 0;
        uint256 elapsedTime = getCurrentTimestamp() - stake.startDate;
        if (stake.durationInDays == 0 && elapsedTime < withdrawalPenaltyInDays * (1 days)) {
            penaltyAmount = calulatePercentAmount(
                withdrawalPenaltyInPercentage,
                stake.amount
            );
        }

        uint256 amountLeft = (stake.amount) - penaltyAmount;
        return (amountLeft, penaltyAmount);
    }

    // User can check if stake is unlocked
    function isStakeUnlocked(
        string memory internalId
    ) public view returns (bool) {
        Stake memory stake = userStakesByTxId[internalId];

        if (stake.startDate == 0) {
            return false;
        }

        uint256 elapsedTime = getCurrentTimestamp() - stake.startDate;
        if (elapsedTime >= stake.durationInDays * (1 days)) {
            return true;
        }

        return false;
    }

    // Internal Function to transfer asset ERC20 or Native
    function transferAssetForWithdrawal(
        address assetAddress,
        address from,
        address to,
        uint256 amount
    ) internal returns (bool) {
        if (assetAddress == NATIVE_ASSET_ADDRESS) {
            require(from.balance >= amount, "Insufficient Native balance");
            
            // Send Native to Treasury with reentrancy Protection for Native
            (bool sent, ) = to.call{value: amount}("");
            return sent;
        } else {
            require(IERC20(assetAddress).balanceOf(from) >= amount, "Insufficient ERC20 balance");

            // Send Native to Treasury with reentrancy Protection for ERC20
            IERC20(assetAddress).safeTransfer(to, amount);
            return true;
        }
    }

    // Internal function to transfer asset ERC20 or Native
    function transferAsset(
        address assetAddress,
        address from,
        address to,
        uint256 amount,
        uint256 nativeAmount
    ) internal returns (bool) {
        if (assetAddress == NATIVE_ASSET_ADDRESS) {
            // Ensure the correct amount of native asset is sent
            require(nativeAmount >= amount, "Incorrect amount of native asset sent");

            // Send native asset to the recipient
            (bool sent, ) = to.call{value: nativeAmount}("");
            require(sent, "Failed to send native asset");
            return true;
        } else {
            // ERC20 asset transfer
            IERC20 token = IERC20(assetAddress);
            require(token.balanceOf(from) >= amount, "Insufficient ERC20 balance");
            require(token.allowance(from, address(this)) >= amount, "Allowance is not enough");

            // Use SafeERC20 to handle transfer
            token.safeTransferFrom(from, to, amount);
            return true;
        }
    }

    // Internal function to any percent amount
    function calulatePercentAmount(
        uint256 percent,
        uint256 amount
    ) internal pure returns (uint256) {
        if (percent > 0) {
            return (amount* (percent))/ 100;
        }
        return 0;
    }

    // Internal Function for String Compare
    function isEmptyString(
        string memory refString
    ) internal pure returns (bool) {
        return bytes(refString).length == 0;
    }

    function getCurrentTimestamp() internal view returns (uint256) {
        return block.timestamp;
    }

    fallback() external{}

    receive() external payable {}
}

Step 5: General and Network configuration

Create a config folder inside your project and create two configurations files viz. general.json and networkWise.json as shown below.

//config/general.json
{
    "L1XCrossChainSwap":{
        "TREASURY_ADDRESS":"YOUR_TREASURY_ADDRESS",
        "TREASURY_SHARE_PERCENT": 0,
        "SOURCE_NATIVE_FEE": 0
    }
}

List of predefined evm-compatible blockchain networks are listed in networkWise.json. You can get the list of authentic XTALK_GATEWAY_CONTRACT_ADDRESS from the table.

// config/networkWise.json
{
  "LOCALHOST": {
    "TREASURY_CONTRACT_ADDRESS": "YOUR_TREASURY_CONTRACT_ADDRESS",
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": "YOUR_CONTRACT_ADDRESS",
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": "YOUR_USDT_CONTRACT_ADDRESS",
      "USDC": "YOUR_USDC_CONTRACT_ADDRESS"
    }
  },
  "HARDHAT": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "ETH": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "AVAX": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "BSC": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": "YOUR_CONTRACT_ADDRESS",
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : "XTALK_BSC_GATEWAY_CONTRACT_ADDRESS",
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "OPTIMISM": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "ARBITRUM": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  },
  "MATIC": {
    "TREASURY_CONTRACT_ADDRESS": null,
    "L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS": null,
    "XTALK_GATEWAY_CONTRACT_ADDRESS" : null,
    "TOKEN_CONTRACT_ADDRESS": {
      "USDT": null,
      "USDC": null
    }
  }
}

Step 6: Compile and Deploy Your Contracts

  1. Compile your project:

    npx hardhat compile
  2. Write deployment scripts or tests as needed, using the setup you've created. Example Provided below.

// scripts/deploy.js

const fs = require("fs/promises");
const hre = require("hardhat");
const path = require('path');
const currentDirectory = process.cwd();

const relativePath = "../config/networkWise.json";
const absolutePath = path.resolve(currentDirectory, relativePath);

const NetworkConfig = require("../config/networkWise.json");
const GeneralConfig = require("../config/general.json");


async function main() {
  const [owner] = await ethers.getSigners();

  // Check Allowed Network
  const currentNetwork = network.name.toUpperCase();
  
  const arrNetwork = Object.keys(NetworkConfig);
  if(arrNetwork.includes(currentNetwork) == false){
    console.log("Please use network out of : ",arrNetwork.join(", "));
    return false;
  }
  console.log("Deployer Account: ",owner.address);

  if(NetworkConfig[currentNetwork]['XTALK_GATEWAY_CONTRACT_ADDRESS'] == "" || NetworkConfig[currentNetwork]['XTALK_GATEWAY_CONTRACT_ADDRESS'] == null){
    console.log("XTALK_GATEWAY_CONTRACT_ADDRESS not deployed.")
    return false;
  }
 

  const L1XStandardCrossChainSwapContract = await ethers.deployContract("L1XStandardCrossChainSwap", [
    NetworkConfig[currentNetwork]['XTALK_GATEWAY_CONTRACT_ADDRESS'],
    currentNetwork,
    GeneralConfig.L1XCrossChainSwap.TREASURY_ADDRESS,
    GeneralConfig.L1XCrossChainSwap.TREASURY_SHARE_PERCENT,
    GeneralConfig.L1XCrossChainSwap.SOURCE_NATIVE_FEE
  ]);

  NetworkConfig[currentNetwork]['L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS'] = await L1XStandardCrossChainSwapContract.getAddress(); 
  console.log("Deployed L1XStandardCrossChainSwap: ",NetworkConfig[currentNetwork]['L1X_STANDARD_CROSS_CHAIN_SWAP_ADDRESS']);

  
}

// 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;
});

At this stage, your project structure looks like this:

stake-contract/
├── config/
│   └── general.json
│   └── networkWise.json
├── contracts/
│   └── StakeContract.sol
├── scripts/
│   └── deploy.js
├── test/
├── hardhat.config.js
└── package.json
  1. Sample Hardhat Config JS File: Configure your Hardhat project by editing hardhat.config.js. Add your network in this file with relevant details. Ensure it looks like this:

//hardhat.config.js

require("@nomicfoundation/hardhat-toolbox");


const INFURA_API_KEY = "YOUR_PRIVATE_KEY";
const PRIVATE_KEY = "YOUR_PRIVATE_KEY";

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 5000,
      },
      viaIR: true,
    },
  },
  networks: {
    goerli: {
      url: `https://goerli.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
    sepolia: {
      url: `https://sepolia.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
    bscTestnet: {
      url: "https://data-seed-prebsc-1-s1.binance.org:8545/",
      accounts: [PRIVATE_KEY],
    },
    optimisticTestnet: {
      url: `https://optimism-goerli.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [PRIVATE_KEY],
    },
    devnet: {
      url: "http://localhost:8545",
      accounts: [PRIVATE_KEY],
    },
    avax: {
      // url: "https://responsive-cosmological-arrow.avalanche-mainnet.quiknode.pro/b5445a7f5dd8ca1e8d9fd20a9a53dba8ecbb9e14/ext/bc/C/rpc",
      url: "https://api.avax.network/ext/bc/C/rpc",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    },
    eth: {
      url: "https://floral-crimson-valley.quiknode.pro/3e31dca22be305511d899d3cf7fc9b370f0e0fa1",
      // url: "https://eth.llamarpc.com",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    },
    bsc: {
      // url: "https://silent-skilled-log.bsc.quiknode.pro/228836424b4f96da10efda362e8e93e240a9ab76",
      url: "https://bsc-dataseed1.ninicoin.io",
      // url: "https://bsc-mainnet.public.blastapi.io",
      // url: "https://binance.llamarpc.com",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    },
    optimism: {
      url: "https://optimism.publicnode.com",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    },
    arbitrum: {
      url: "https://winter-responsive-film.arbitrum-mainnet.quiknode.pro/9b0931ce258b9f1cdffa279f4a6e7336af7a3f9e",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    },
    matic: {
      url: "https://fabled-dry-sanctuary.matic.quiknode.pro/b76be513e5e558c0778327696462b99ae565069a",
      // url: "https://polygon-rpc.com",
      accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
    }
  },
  etherscan: {
    apiKey: {
      bscTestnet: "X6K857FHFMS7CWHPG2X2AD3I2KBCHQP3ME",
      goerli: "CNQMU2ZM1T1CBI1IY79A1EKJFIBMU8JB8M",
      optimisticGoerli: "C4YIC217NXH3EYNV1U4TNBBKWYIE1Q1E8D",
    },
    // url: "https://api-testnet.bscscan.com/",
    url: "https://api-goerli.etherscan.io/",
    // url: "https://api-goerli-optimistic.etherscan.io/"
  }
};

-- Check Endpoint for L1X TestNet Faucet Before Deployment

  1. Deployment Bash

npx hardhat run --network SOURCE_NETWORK ./scripts/deploy.js

Last updated