Building and deploying a token contract on Starknet

Learn how to build a presale contract to launch your token

Tagged

Darlington Nnam

Oct 3, 2024

Quick summary

We’ve previously explained how to write and deploy a simple ERC-20 token to Starknet. But what’s the point in learning how to build an ERC-20 token, without learning how to launch it with a presale (also known as an initial coin offering - ICO)? That’s precisely why we’re going to teach you how to do this in this article!

By following the steps in this article, you’ll have written and deployed a smart contract that enables users to purchase your ERC-20 token using ETH, or another token of your choice.

WTF is an ICO?

One of the most popular ways to launch a token is through a presale, commonly referred to as an ICO. The funds then raised through the token offering should then get used in supporting the project build new products, expand the team, and much more.

For our ICO smart contract, we’re going to be needing 3 key functions:

  • Register: Users need to sign up for the presale with a small 0.001 ETH deposit
  • Claim: Allows users to redeem their tokens
  • Is_registered: Which checks the user’s registration status

The thought process for our application is a user interested in participating in the ICO needs to first register with 0.001 ETH by calling a register function, then once the ICO duration which was specified using an ICO_DURATION expires, he can now call an external function claim to claim his share of ICO tokens.

At the end of this tutorial, you should be able to:

  1. Interact with other contracts using a contract interface.
  2. Manipulate time on Starknet using the block.timestamp method.

Before going forward, we will need to understand what a contract interface is and how it can be used to interact with other contracts, as we will be needing to interact with our previously deployed argentERC20 token whilst building our ICO contract.

A contract interface provides a means for one contract to invoke or call the external function of another contract. Here's what a contract interface in Cairo 1.0 looks like.

#[abi]
    trait IENSContract {
        #[view]
        fn get_name(address: starknet::ContractAddress) -> felt252;

        #[external]
        fn set_name(address: starknet::ContractAddress, name: felt252);
    }

An interface in Cairo 1, is a trait with the [abi] macro, which specifies the function declaration (name, parameters, visibility and return value) without including the function body.

NB: It’s important to note, that unlike Cairo 0.x, the methods contained in a Cairo 1 interface must explicitly specify its visibility, by including the #[external], #[view] and #[event] macros.

Once we have a contract interface, we can now make calls to the contract’s functions using a Contract Interface Dispatcher. For each contract interface you create in Cairo, two dispatchers are automatically created and exported; a contract-dispatcher and a library-dispatcher. In this article, we are going to take a look at how you could make calls to other contracts using the contract-dispatcher.

Calling another contract using the Contract Interface Dispatcher, calls the contract's logic in its context, and may change its state in most cases. Here’s an example of how you can do this:

#[abi]
    trait IENSContract {
        #[view]
        fn get_name(address: starknet::ContractAddress) -> felt252;

        #[external]
        fn set_name(address: starknet::ContractAddress, name: felt252);
    }

Having understood how contract interfaces work, let’s move on to writing our smart contract.

Setting up Scarb

As always, we would be using the Scarb package manager for development. If you still don’t have Scarb installed at this point, refer to the documentation here. To get started, we will need to initialize a new project. Let’s call our project “ICO” To do this run:

Scarb new ICO

Creating a new file

Next up, we are going to create a new file called ICO.cairo in our src folder, where we’d be writing our contract codes.

Imports

Inside our new file, we’ll begin with the #[contract] directive which specifies that our file contains codes for a Starknet contract, after which we’ll create a module ‘ICO’ for our contract codes.

Then, we are going to be importing all the necessary library functions we might need to use in our contract.

   use src::IERC20;
   use src::IERC20::IERC20DispatcherTrait;
   use src::IERC20::IERC20Dispatcher;
  
   use starknet::ContractAddress;
   use starknet::get_block_timestamp;
   use starknet::get_contract_address;
   use starknet::get_caller_address;
   use starknet::contract_address_try_from_felt252;
   use traits::TryInto;
   use option::OptionTrait;
   use integer::u256_from_felt252;

Creating the IERC20 contract Interface

Before we begin writing our ICO contract, if you check our imports from above, you’ll notice we imported the IERC20DispatcherTrait from an IERC20.cairo file which we don’t have in our project structure yet. We are going to create this file in our /src directory.

Once you’ve done that, copy the interface contract below into it:

use starknet::ContractAddress;

#[abi]
trait IERC20 {
   #[view]
   fn get_name() -> felt252;

   #[view]
   fn get_symbol() -> felt252;

   #[view]
   fn get_total_supply() -> felt252;

   #[view]
   fn balance_of(account: ContractAddress) -> u256;

   #[view]
   fn allowance(owner: ContractAddress, spender: ContractAddress) -> u256;

   #[external]
   fn transfer(recipient: ContractAddress, amount: u256);

   #[external]
   fn transfer_from(sender: ContractAddress, recipient: ContractAddress, amount: u256);

   #[external]
   fn approve(spender: ContractAddress, amount: u256);
}

Just like we stated earlier, contract interfaces in Cairo 1.0 are traits prefixed with the #[abi] macro.

Perfect! Now we have our ERC20 contract interface, we can now move onto writing our ICO contract!

Writing the ICO Contract

We are going to be dividing this section into different major sections for better understanding:

Constants

Constants are a special type of storage variable that are fixed and as such cannot be altered after being set. For our ICO contract, we’ll have to set a few constants we’d need going forward:

First, we’ll set our registration price in WEI, which a user will need to pay to register for the presale.

const REGPRICE: felt252 = 1000000000000000;

NB: 1 ETH = 1,000,000,000,000,000,000 wei

Lastly, we’ll be setting a time duration in seconds after which users can withdraw their presale token upon registration.

const ICO_DURATION: u64 = 86400_u64;

Storage variables

Moving on, we’ll need to create some storage variables in our contract:

  1. we’ll create a storage variable that will hold the address of the token we want to conduct a presale for.
  2. we’ll create another to hold the address of the contract’s admin (the wallet address which holds the tokens for ICO).
  3. we’ll create a third (which will be a storage mapping), to track the registration status of addresses. status is a bool value that returns true if registered and false if not.
  4. we’ll also need to create a storage variable to track the claim status of an address to counter double-spending. status is a bool value that returns true if the address has claimed allocated tokens and false if not.
  5. Finally, we are going to create two storage variables to track the start and end time of our ICO.
struct Storage {
       token_address: ContractAddress,
       admin_address: ContractAddress,
       registered_address: LegacyMap::<ContractAddress, bool>,
       claimed_address: LegacyMap::<ContractAddress, bool>,
       ico_start_time: u64,
       ico_end_time: u64,
   }

Constructor Logic

With our constructor, we’ll be initializing the admin_address , token_address , ico_start_time and ico_end_time storage variables.

The ICO’s start time is obtained by using the get_block_timestamp() method which returns the current time at function execution, and we can calculate the ICO’s end time by adding the ICO_DURATION to the current time.

#[constructor]
   fn constructor(_token_address: ContractAddress, _admin_address: ContractAddress) {
       admin_address::write(_admin_address);
       token_address::write(_token_address);

       let current_time: u64 = get_block_timestamp();
       let end_time: u64 = current_time + ICO_DURATION;
       ico_start_time::write(current_time);
       ico_end_time::write(end_time);

       return ();
   }

View functions

We’ll need to create a means to check if an address is registered or not. The is_registered function takes in an address argument, and returns the registration status of that address.

   #[view]
   fn is_registered(_address: ContractAddress) -> bool {
       registered_address::read(_address)
   }

External Functions

External functions are used to modify the state of the blockchain.

The first external function we’ll be needing for our contract is the register function.

#[external]
   fn register() {
       let this_contract = get_contract_address();
       let caller = get_caller_address();
       let token = token_address::read();
       let end_time: u64 = ico_end_time::read();
       let current_time: u64 = get_block_timestamp();
       let eth_contract: ContractAddress = contract_address_try_from_felt252(0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7).unwrap();

       // check that ICO has not ended
       assert(current_time < end_time, 'ICO has been completed');
       // check that user is not already registered
       assert(is_registered(caller) == false, 'You have already registered!');
       // check that the user has beforehand approved the address of the ICO contract to spend the registration amount from his ETH balance
       let allowance = IERC20Dispatcher {contract_address: eth_contract}.allowance(caller, this_contract);
       assert(allowance >= u256_from_felt252(REGPRICE), 'approve at least 0.001 ETH!');

       IERC20Dispatcher {contract_address: token}.transfer_from(caller, this_contract, u256_from_felt252(REGPRICE));

       registered_address::write(caller, true);
       return ();
   }

The register function first checks that the ICO has not ended, the user hasn’t registered previously, and the user has beforehand approved our ICO contract to spend the registration fee from his wallet.

Once these checks pass, the contract calls the transfer_from function on the contract interface we specified earlier, which transfers the registration fee from the user’s wallet to the contract before finally changing the user’s registration status to 1.

NB: It's important to note that the user must first call the approve function on the ETH contract address, to allow the ICO contract address to spend the registration fee from his wallet.

The second function we’ll need is the claim function, which the user can call after the ICO duration is over, to claim his allocated tokens.

fn claim(_address: ContractAddress) {
       let claim_amount = u256_from_felt252(20);
       let token = token_address::read();
       let end_time: u64 = ico_end_time::read();
       let current_time: u64 = get_block_timestamp();

       // check that user is registered
       assert(is_registered(_address) == true, 'You are not eligible!');
       // check that ICO has ended
       assert(current_time > end_time, 'ICO is not yet ended!');
       // check that user has not previously claimed
       let claim_status = claimed_address::read(_address);
       assert(claim_status == false, 'You already claimed!');

       IERC20Dispatcher {contract_address: token}.transfer(_address, claim_amount);

       claimed_address::write(_address, true);
       return ();
   }

The claim function first checks that the ICO is ended, the caller is a registered address, and that the caller has not already claimed tokens previously.

If these checks pass, once again the contract calls the transfer_from function on the ERC20 contract interface, transferring the claim_amount (20 tokens), to the caller, before finally updating the claim status to prevent double-spending.

Haven got to this point, congratulations, you just completed your first ICO contract! You can find the full contract code here.

Conclusion

Hopefully, you learned how to interact with other contracts using a contract interface, and how to manipulate time on Starknet using the block.timestamp method!

If you have any questions regarding this, reach out to me @0xdarlington, I’d love to help you build on Starknet with Argent X.

For more developer resources, follow us across our socials:

Argent Twitter — @argentHq

Argent Engineering Twitter — @argentDeveloper

LinkedIn — @argentHq

Youtube — @argentHQ