4.1 Simple Token Program

In this chapter, we'll put into practice a lot of the knowledge we have covered until now.

The goal here is to see how those concepts can help us create a very simple fungible token program. A token is a digital asset that represents value and can be transferred between users. Fungible tokens are interchangeable, meaning each unit of the token holds the same value as another unit of the same type.

Our token will be very simplistic and just will have the following features:

  • Mint – Creating new tokens, only callable by the admin

  • Transfer – Transfer token, from the former owner to the new owner

There are two approaches we could have to build tokens:

  • By using public state: mappings

  • By using private state: records

Public State Approach

This is the traditional Ethereum approach, let's start with an empty program:

program public_token.aleo {
    
}

We'll track user balances using a mapping, let's call it balance, associating user addresses to their balance amount:

program public_token.aleo {
    mapping balance: address => u64;
}

Let's write a transition for minting a token. It will take two arguments: a mint quantity and a receiver address. It will then simply add the amount to the balance mapping value associated with the receiver's address. Since we're going to update the balance mapping, we'll need an async transition, calling an async function:

program public_token.aleo {
    mapping balance: address => u64;
    
    async transition mint(public receiver: address, public amount: u64) -> Future {
        return finalize_mint(receiver, amount);
    }
    async function finalize_mint(receiver: address, amount: u64){
        let old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let new_balance: u64 = old_balance + amount;
        balance.set(receiver, new_balance);
    }
}

Since we don't want every body to be able to mint our token let's add a check that the minter has the admin address. We'll store that admin address in a constant:

program public_token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    mapping balance: address => u64;
    
    async transition mint(public receiver: address, public amount: u64) -> Future {
        assert_eq(self.caller, ADMIN);
        return finalize_mint(receiver, amount);
    }
    async function finalize_mint(receiver: address, amount: u64){
        let old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let new_balance: u64 = old_balance + amount;
        balance.set(receiver, new_balance);
    }
}

Now let's add a transfer function. It will decrease sender's balance and increase receiver's balance.

program public_token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    mapping balance: address => u64;
    
    async transition mint(public receiver: address, public amount: u64) -> Future {
        assert_eq(self.caller, ADMIN);
        return finalize_mint(receiver, amount);
    }
    async function finalize_mint(receiver: address, amount: u64){
        let old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let new_balance: u64 = old_balance + amount;
        balance.set(receiver, new_balance);
    }
    
    async transition transfer(public receiver: address, public amount: u64) -> Future {
        return finalize_transfer(self.caller, receiver, amount);
    }
    async function finalize_transfer(sender: address, receiver: address, amount: u64){
        // Add to receiver balance
        let receiver_old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let receiver_new_balance: u64 = receiver_old_balance + amount;
        balance.set(receiver, receiver_new_balance);
        
        // Remove from sender balance
        let sender_old_balance: u64 = balance.get_or_use(sender, 0u64);
        let sender_new_balance: u64 = sender_old_balance - amount;
        balance.set(sender, sender_new_balance);
    }
}

No need to check that amount >= sender_old_balance since this substraction guaranties that there is no underflow let sender_new_balance: u64 = sender_old_balance - amount.

The order of the balance updates doesn't matter neither, because if the previous balance substraction fails, the whole transaction is reverted anyway.

Although, this approach has two issues:

  • It does not leverage at all the offchain execution Aleo is capable of. This makes the cost of these transfers higher than they could be.

  • All those transfers are public, anyone can track, as on a public ledger as ethereum, the transfers of tokens from an address to another.

Private State Approach

Let's start again with an empty program:

program private_token.aleo {
    
}

Instead of using a mapping, we'll store the token holdings in records owned by users. Let's define our record: as for every record, it includes a owner field, and we'll include an amount field as well.

program private_token.aleo {
    record Token{
        owner: address,
        amount: u64
    }
}

Let's now write the mint transition. As for public mint we enforce that only the ADMIN can mint:

program private_token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    record Token{
        owner: address,
        amount: u64
    }
    
    // Notice that the arguments are private
    transition mint(private receiver: address, private amount: u64) -> Token {
        assert_eq(self.caller, ADMIN);
        let token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return token;
    }
}

And lastly, let's add a transfer function. This transfer function will take as an argument an existing record, to consume it. It will output two records, one with the change, remaining from the sent amount, and the one owned by receiver.

program private_token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    record Token{
        owner: address,
        amount: u64
    }
    
    // Notice that the arguments are private
    transition mint(private receiver: address, private amount: u64) -> Token {
        assert_eq(self.caller, ADMIN);
        let token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return token;
    }
    
    transition transfer(
         old_token: Token,
         private receiver: address, 
         private amount: u64
     ) -> (Token, Token) {
        let change_amount: u64 = old_token.amount - amount;
        let change_token: Token = Token {
            owner: old_token.owner,
            amount: change_amount
        };
        let receiver_token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return (change_token, receiver_token);
    }
}

As you can see, exactly as for private transfers no need to check if old_token.amount >= amount, because the substraction will fail otherwise.

This approach is super scalable and cheap because there's no onchain computation and shared state management involved. It's also completely private, neither the amount nor the sender/receiver address gets leaked when the transaction is posted on chain. Although it has one major caveat: Since programs cannot own records, programs cannot own tokens with this approach.

Hybrid Approach

Since we want the best of both worlds: private scalable tokens, but also that can be owned by other programs, tokens on aleo implement both of those approaches at the same time.

The idea is that you can own a token either privately, or publicly. Then you can transfer it from private to public or the other way around. You can also transfer it privately when it's private, and publicly when it's public. Let's merge our previous programs and simply adding public/private suffix to our functions:

program token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    mapping balance: address => u64;
    
    record Token{
        owner: address,
        amount: u64
    }
    
    transition mint_private(private receiver: address, private amount: u64) -> Token {
        assert_eq(self.caller, ADMIN);
        let token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return token;
    }
    
    transition transfer_private(
         old_token: Token,
         private receiver: address, 
         private amount: u64
     ) -> (Token, Token) {
        let change_amount: u64 = old_token.amount - amount;
        let change_token: Token = Token {
            owner: old_token.owner,
            amount: change_amount
        };
        let receiver_token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return (change_token, receiver_token);
    }
    
    async transition mint_public(public receiver: address, public amount: u64) -> Future {
        assert_eq(self.caller, ADMIN);
        return finalize_mint(receiver, amount);
    }
    async function finalize_mint(receiver: address, amount: u64){
        let old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let new_balance: u64 = old_balance + amount;
        balance.set(receiver, new_balance);
    }
    
    async transition transfer_public(public receiver: address, public amount: u64) -> Future {
        return finalize_transfer(self.caller, receiver, amount);
    }
    async function finalize_transfer(sender: address, receiver: address, amount: u64){
        let receiver_old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let receiver_new_balance: u64 = receiver_old_balance + amount;
        balance.set(receiver, receiver_new_balance);
        
        let sender_old_balance: u64 = balance.get_or_use(sender, 0u64);
        let sender_new_balance: u64 = sender_old_balance - amount;
        balance.set(sender, sender_new_balance);
    }
}

We're still missing a function for converting private tokens to public tokens:

     async transition transfer_private_to_public(
         old_token: Token,
         private receiver: address, 
         private amount: u64
     ) -> (Token, Future) {
        let change_amount: u64 = old_token.amount - amount;
        let change_token: Token = Token {
            owner: old_token.owner,
            amount: change_amount
        };
        let transfer_future: Future = finalize_private_to_public(receiver, amount);
        return (change_token, transfer_future); // Notice we return both a record and a Future
    }
    async function finalize_private_to_public(receiver: address, amount: u64){
        // Add to receiver balance
        let receiver_old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let receiver_new_balance: u64 = receiver_old_balance + amount;
        balance.set(receiver, receiver_new_balance);
    }

And a function for converting from public to private:

     async transition transfer_public_to_private(
         private receiver: address, 
         private amount: u64
     ) -> (Token, Future) {
        let receiver_token: Token = Token {
            owner: receiver,
            amount: amount
        };
        let transfer_future: Future = finalize_public_to_private(self.caller, amount);
        return (receiver_token, transfer_future); // Notice we return both a record and a Future
    }
    async function finalize_public_to_private(sender: address, amount: u64){
        // Remove from sender balance
        let sender_old_balance: u64 = balance.get_or_use(sender, 0u64);
        let sender_new_balance: u64 = sender_old_balance - amount;
        balance.set(sender, sender_new_balance);
    }

Here is our final program:

program token.aleo {
    const ADMIN: address = aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px;
    
    mapping balance: address => u64;
    
    record Token{
        owner: address,
        amount: u64
    }
    
    transition mint_private(private receiver: address, private amount: u64) -> Token {
        assert_eq(self.caller, ADMIN);
        let token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return token;
    }
    
    transition transfer_private(
         old_token: Token,
         private receiver: address, 
         private amount: u64
     ) -> (Token, Token) {
        let change_amount: u64 = old_token.amount - amount;
        let change_token: Token = Token {
            owner: old_token.owner,
            amount: change_amount
        };
        let receiver_token: Token = Token {
            owner: receiver,
            amount: amount
        };
        return (change_token, receiver_token);
    }
    
    async transition mint_public(public receiver: address, public amount: u64) -> Future {
        assert_eq(self.caller, ADMIN);
        return finalize_mint(receiver, amount);
    }
    async function finalize_mint(receiver: address, amount: u64){
        let old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let new_balance: u64 = old_balance + amount;
        balance.set(receiver, new_balance);
    }
    
    async transition transfer_public(public receiver: address, public amount: u64) -> Future {
        return finalize_transfer(self.caller, receiver, amount);
    }
    async function finalize_transfer(sender: address, receiver: address, amount: u64){
        let receiver_old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let receiver_new_balance: u64 = receiver_old_balance + amount;
        balance.set(receiver, receiver_new_balance);
        
        let sender_old_balance: u64 = balance.get_or_use(sender, 0u64);
        let sender_new_balance: u64 = sender_old_balance - amount;
        balance.set(sender, sender_new_balance);
    }
    
    async transition transfer_private_to_public(
         old_token: Token,
         private receiver: address, 
         private amount: u64
     ) -> (Token, Future) {
        let change_amount: u64 = old_token.amount - amount;
        let change_token: Token = Token {
            owner: old_token.owner,
            amount: change_amount
        };
        let transfer_future: Future = finalize_private_to_public(receiver, amount);
        return (change_token, transfer_future); // Notice we return both a record and a Future
    }
    async function finalize_private_to_public(receiver: address, amount: u64){
        // Add to receiver balance
        let receiver_old_balance: u64 = balance.get_or_use(receiver, 0u64);
        let receiver_new_balance: u64 = receiver_old_balance + amount;
        balance.set(receiver, receiver_new_balance);
    }

    async transition transfer_public_to_private(
         private receiver: address, 
         private amount: u64
     ) -> (Token, Future) {
        let receiver_token: Token = Token {
            owner: receiver,
            amount: amount
        };
        let transfer_future: Future = finalize_public_to_private(self.caller, amount);
        return (receiver_token, transfer_future); // Notice we return both a record and a Future
    }
    async function finalize_public_to_private(sender: address, amount: u64){
        // Remove from sender balance
        let sender_old_balance: u64 = balance.get_or_use(sender, 0u64);
        let sender_new_balance: u64 = sender_old_balance - amount;
        balance.set(sender, sender_new_balance);
    }
}

In next chapter, we'll deploy that program onchain.

Last updated