Bitkub Chain SDK Compatible Smart Contract

Welcome to this tutorial on creating a Bitkub Chain SDK-compatible smart contract. In this guide, we'll walk you through building a smart contract designed for integration with our Bitkub Chain SDK. This tutorial will focus on outlining the essential structure of a compatible function, along with some additional practical examples.


Prerequisites

  • A basic understanding of blockchain technology and smart contracts.

  • Familiarity with the Solidity programming language.

  • A Solidity-compatible IDE. For this tutorial, we will be using Remix IDE to develop, compile, and deploy our smart contracts.


Bare Minimum Compatible Smart Contract

Let's start with a basic example to grasp the core concepts.

// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0 <0.8.20;

contract BKCSDKExample1 {
    uint256 public myUint256Var = 7216;
    address public myAddressVar;

    function mySDKMethod1(
        uint256 var_,
        address bitkubNext_
    ) external {
        myUint256Var = var_;
        myAddressVar = bitkubNext_;
    }
}

Explanation of the Contract Code

Let’s break down the smart contract code step by step, highlighting each part in detail.

1. SPDX Identifier

At the very top, we see the line:

// SPDX-License-Identifier: MIT

This line is the SPDX License Identifier, which specifies the type of license under which this contract is distributed. In this case, it's the MIT license.

💡 Note: If you don’t want to specify a license, use UNLICENSED (not Unlicense) to ensure clarity that the contract is not licensed.

2. Pragma Solidity

pragma solidity >=0.8.0 <0.8.20;

This contract is built using Solidity version 0.8.x, specifically any version from 0.8.0 up to but not including 0.8.20.

💡 Note: It’s important to note that Bitkub Chain currently supports Solidity versions up to 0.8.19 (at the time of writing), so we specify this range.

3. State Variables

uint256 public myUint256Var = 7216;
address public myAddressVar;

Here, we define two state variables:

  • myUint256Var: An unsigned integer (uint256) initialized with the value 7216. This will be updated when the function is called.

  • myAddressVar: An address type variable to store Ethereum-like addresses.

These variables will hold the data passed through the function.

💡 Note: If these state variables were uninitialized, their values would default to zero. In Solidity, uninitialized state variables take on zero-like values (e.g., uint256 is 0 and address is 0x000...000).

4. Function Declaration

function mySDKMethod1(
    uint256 var_,
    address bitkubNext_
) external {
    myUint256Var = var_;
    myAddressVar = bitkubNext_;
}

This is the function where the actual logic occurs. Let's examine its components:

  • Function Parameters: The function accepts two parameters:

    • uint256 var_: This parameter is inputable from the SDK and will be passed into the function when called.

    • address bitkubNext_: This parameter indicates the user interacting with the function. A compatible function on Bitkub Chain must have its last parameter as an address.

  • External Keyword:

    The external keyword signifies that this function can only be called by a transaction. This means it can be triggered by an external account or even another contract.

💡 Important: It doesn’t necessarily have to be another contract— a contract can also call itself in a circular fashion. However, this function cannot be called internally by other functions in the same contract without using this.mySDKMethod1()

5. Function Logic: Assigning Values to State Variables

myUint256Var = var_;
myAddressVar = bitkubNext_;

In the function body, we simply assign the input values (from the parameters var_ and bitkubNext_) to the corresponding global state variables myUint256Var and myAddressVar. This updates the state of the contract.

6. Observing State Variables

buint256 public myUint256Var = 7216;
address public myAddressVar;

Since both variables are marked public, we can observe their values externally at any time. In Solidity, marking a state variable as public automatically creates a getter function, allowing anyone to read its value.

💡 Note: If these variables were not marked as public, they would be private by default, meaning they could not be observed externally through normal means.


Key Points

  • SPDX License Identifier: Use UNLICENSED if you don’t want to declare a license.

  • Solidity Version: Ensure compatibility with 0.8.x, and be mindful of the chain’s supported version limits (up to 0.8.19).

  • Two State Variables: Used to hold the values passed through the function.

  • Function with Two Parameters: Only the first parameter is inputable from the SDK, while the second must always be an address (indicating the interacting user).

  • External Functions: Can only be called by transactions and must be called with this internally.

  • State Variables: Public state variables are observable externally, while private ones cannot be seen without getters.

This simple contract provides a bare minimum example of what a Bitkub Chain-compatible smart contract should look like, with a focus on SDK compatibility and proper state variable management.


Exploring More: Building a More Practical and Sophisticated Smart Contract

// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0 <0.8.20;

import "./ITarget.sol";

contract BKCSDKExample2 {
    address public constant SDK_CALL_HELPER_ROUTER = 0x96f4C25E4fEB02c8BCbAdb80d0088E0112F728Bc;

    uint256 public myUint256Var = 7216;
    string[] public myStringArrVar; // appendable array
    mapping(address => uint8) public myAddrToUint8Map;

    modifier onlySDKCallHelperRouter() {
        require(msg.sender == SDK_CALL_HELPER_ROUTER, "BKCSDKExample2: restricted only sdk call helper router");
        _;
    }

    event MySDKMethod1Executed(address indexed bitkubNext, address indexed var1, uint256 var2, string var3);

    function mySDKMethod1(
        address var1_,
        uint256 var2_,
        string memory var3_,
        address bitkubNext_
    ) external onlySDKCallHelperRouter {
        myAddrToUint8Map[var1_] = uint8(block.timestamp % type(uint8).max);
        myUint256Var = var2_;
        myStringArrVar.push(var3_);

        emit MySDKMethod1Executed(bitkubNext_, var1_, var2_, var3_);
    }

    event MySDKMethod2Executed(address indexed bitkubNext, address[] addressArr);

    function mySDKMethod2(
        address[] memory addressArr_,
        address bitkubNext_
    ) public onlySDKCallHelperRouter {
        for (uint256 i = 0; i < addressArr_.length; i++) {
            myAddrToUint8Map[addressArr_[i]] = uint8((block.timestamp + i) % type(uint8).max);
        }

        emit MySDKMethod2Executed(bitkubNext_, addressArr_);
    }

    // Ethers.js v6
    // const { AbiCoder } = require('ethers');
    // const abiCoder = new AbiCoder();

    // const res = abiCoder.encode(
    //     ['address[]'],
    //     [['0xd9145CCE52D386f254917e481eB44e9943F39138', '0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8']],
    // );
    // console.log(res);

    function mySDKMethod2(
        bytes memory abiEncodedAddressArr_,
        address bitkubNext_
    ) external onlySDKCallHelperRouter {
        address[] memory addressArr;
        (addressArr) = abi.decode(abiEncodedAddressArr_, (address[]));

        mySDKMethod2(addressArr, bitkubNext_);
    }

    function mySDKMethod3(address target_, uint256 a_, address) external onlySDKCallHelperRouter returns (uint256) {
        return ITarget(target_).setA(a_);
    }
}

Explanation of the Contract Code

1. Addition of a Modifier

modifier onlySDKCallHelperRouter() {
    require(msg.sender == SDK_CALL_HELPER_ROUTER, "BKCSDKExample2: restricted only sdk call helper router");
    _;
}

This modifier ensures that only the SDK CallHelper Router can call functions with this modifier. It is designed to prevent unauthorized users from interacting with specific functions, which is critical for securing the contract's operations.

2. SDK CallHelper Router Address

address public constant SDK_CALL_HELPER_ROUTER = 0x96f4C25E4fEB02c8BCbAdb80d0088E0112F728Bc;

The SDK CallHelper Router address is stored in a constant variable in this example, which holds the router address for Bitkub Chain testnet. This ensures that only calls originating from this address are allowed to invoke functions protected by the onlySDKCallHelperRouter modifier.

💡 Note: The SDK CallHelper Router address may change if the contract is upgraded or moved to a different address. In such cases, it would be better to store the address in a changeable state variable. However, for simplicity, we are using a constant in this example.


Method 1: Simulating Work and Event Emission

function mySDKMethod1(
    address var1_,
    uint256 var2_,
    string memory var3_,
    address bitkubNext_
) external onlySDKCallHelperRouter {
    myAddrToUint8Map[var1_] = uint8(block.timestamp % type(uint8).max);
    myUint256Var = var2_;
    myStringArrVar.push(var3_);

    emit MySDKMethod1Executed(bitkubNext_, var1_, var2_, var3_);
}

In this method, we simulate work being done by:

  • Writing to state variables such as myAddrToUint8Map and myStringArrVar.

  • Emitting an event to signal that the function has executed successfully, with relevant data.

An SDK-compatible function can receive multiple input variables, as shown here. The function takes a combination of an address, a uint256 value, and a string, demonstrating the flexibility of inputs that SDK functions can handle.


Method 2: Handling Arrays and Compatibility with the SDK

function mySDKMethod2(
    address[] memory addressArr_,
    address bitkubNext_
) public onlySDKCallHelperRouter {
    for (uint256 i = 0; i < addressArr_.length; i++) {
        myAddrToUint8Map[addressArr_[i]] = uint8((block.timestamp + i) % type(uint8).max);
    }

    emit MySDKMethod2Executed(bitkubNext_, addressArr_);
}

Here, we have a function that accepts an array of addresses (address[] memory). Unfortunately, the SDK system does not yet support arrays or structs as parameters, making this function incompatible with the SDK at the time of writing.

Workaround: Using bytes and Decoding

To work around the current SDK limitations, we provide an overloaded version of this function:

function mySDKMethod2(
    bytes memory abiEncodedAddressArr_,
    address bitkubNext_
) external onlySDKCallHelperRouter {
    address[] memory addressArr;
    (addressArr) = abi.decode(abiEncodedAddressArr_, (address[]));

    mySDKMethod2(addressArr, bitkubNext_);
}

This version accepts a bytes type input, decodes it into an array, and then calls the original array-based function. This allows us to work within the SDK's constraints while still preparing the contract for future support of arrays. By overloading the function, we ensure that once array support is available, we can switch seamlessly to the array-based implementation.

Encoding Arrays into bytes using JavaScript

If you're working with the SDK and need to encode an array of addresses into bytes, you can use Ethers.js to achieve this. Here's the relevant JavaScript code to encode an array into bytes format:

const { AbiCoder } = require('ethers');
const abiCoder = new AbiCoder();

// Example array of addresses
const addressArr = ['0xd9145CCE52D386f254917e481eB44e9943F39138', '0xd8b934580fcE35a11B58C6D73aDeE468a2833fa8'];

// Encoding the address array into bytes
const encoded = abiCoder.encode(['address[]'], [addressArr]);
console.log(encoded);
  • This script uses the AbiCoder from Ethers.js to encode the array of addresses into the bytes format.

  • The resulting encoded value is what you would pass as the abiEncodedAddressArr_ parameter to the Solidity function.

  • This approach can also be applied to functions that take structs, arrays of structs, or even nested structs as inputs (--structs are essentially tuples, and nested structs are tuples within tuples!)


Method 3: Interacting with Another Contract

function mySDKMethod3(address target_, uint256 a_, address) external onlySDKCallHelperRouter returns (uint256) {
    return ITarget(target_).setA(a_);
}

In this third method, we demonstrate how to call another contract from within an SDK-compatible function. The parameter a_ is passed to the setA function of the target contract, and the target contract returns a_ + 1.

This shows how contracts can interact with each other in Solidity, which is a common use case in more complex decentralized applications. The ability to make external calls is key for creating sophisticated systems that involve multiple contracts working together.


Method 4 and 5: transferFrom Using SDK Transfer Router

function mySDKMethod4(
    address tokenAddr_,
    address recipient_,
    uint256 amount_,
    address bitkubNext_
) external onlySDKCallHelperRouter {
    SDK_TRANSFER_ROUTER.transferKAP20(tokenAddr_, recipient_, amount_, bitkubNext_);
}
function mySDKMethod5(
    address tokenAddr_,
    address recipient_,
    uint256 tokenId_,
    address bitkubNext_
) external onlySDKCallHelperRouter {
    SDK_TRANSFER_ROUTER.transferKAP721(tokenAddr_, recipient_, tokenId_, bitkubNext_);
}

In the fourth method, we demonstrate how to replicate the transferFrom functionality of ERC20 tokens within our SDK system by calling the transferKAP20 method on the SDK Transfer Router.

💡 Note: The SDK Transfer Router address should not be constant in a production environment, as it may change if the contract is upgraded or moved to a new address. In such cases, it would be better to store the address in a modifiable state variable to allow flexibility. However, for simplicity, we are using a constant in this example.

Approval Requirement

Before this function can execute successfully, you must first approve the contract to spend the tokens on behalf of the user (bitkubNext_) using the SDK. This is similar to how an ERC20 token requires approval before calling transferFrom. If the token is not approved, the function will revert, just like with ERC20 token transfers.

As for the fifth method, it is the same as the fourth but for KAP721 tokens.


Key Points

  • Modifier for SDK CallHelper Router: Ensures only the SDK CallHelper Router can call certain functions.

  • Related Addresses: Stored in constant variables for simplicity in this example, though they can be made flexible in production contracts.

  • Event Emission: Helps in simulating work and provides a way to track function execution.

  • Handling Arrays: While arrays are not yet supported in the SDK, we provide a workaround using bytes and decoding, with JavaScript code to handle the encoding.

  • Contract Interactions: Demonstrates how to call another contract from within an SDK-compatible function, a key feature in complex smart contracts.

Last updated