5.3 Fungible Token Standard - ARC20

The standard for writing programs implementing fungible token is almost what we described in chapter 4.1, with the hybrid public/private state approach.

It includes two extra features: token metadata and approval mechanism.

Token Metadata

At the beginning of the program all the metadata for the token are defined as constants, along with a struct gathering all these informations.

program arc20.aleo {
    const name: u128 = 0u128;           // Token name
    const symbol: u64 = 0u64;           // Token symbol
    const decimals: u8 = 0u8;           // Token decimals
    const total_supply: u64 = 0u64;     // Token maximum supply
    
    struct metadata {
        name: u128,
        symbol: u64,
        decimals: u8,
        total_supply: u64,
    }

Then a transition is defined to return that metadata struct to any caller using the constants defined above:

    transition get_metadata () -> metadata {
        return metadata {
            name: name,
            symbol: symbol,
            decimals: decimals,
            total_supply: total_supply,
        };
    }

Token basic mechanisms

A mappings is then defined to track public ownership of tokens for each addresses:

    mapping account: address => u64;

And a record is defined to track private ownership of tokens by users:

    record token {
        private owner: address,
        private amount: u64,
    }

All the transfer functions described in chapter 4.1 Simple Token Program can then be defined using those objects: transfer_private, transfer_public, transfer_public_to_private, transfer_private_to_public.

async transition transfer_public(
        public receiver: address, // to the receiver
        public amount: u64, // amount to transfer
    ) -> Future {
        return finalize_transfer_public(
            self.caller,
            receiver,
            amount,
        );
    }
    async function finalize_transfer_public(
        caller: address, receiver: address, amount: u64,
    ) {
        let from_balance: u64 = account.get(caller);
        from_balance -= amount;
        account.set(caller, from_balance);

        let to_balance: u64 = account.get_or_use(receiver, 0u64);
        to_balance += amount;
        account.set(receiver, to_balance);
    }

    transition transfer_private(
        private consumed_token: token, // token record to consume
        private receiver: address, // to the receiver
        private amount: u64, // amount to transfer
    ) -> (token, token) {
        let sender_token: token = token {
            owner: consumed_token.owner,
            amount: consumed_token.amount - amount,
        };
        let receiver_token: token = token {
            owner: receiver,
            amount: amount,
        };
        return (sender_token, receiver_token);
    }

    async transition transfer_private_to_public(
        private consumed_token: token, // token record to consume
        public receiver: address, // to the receiver
        public amount: u64, // amount to transfer
    ) -> (token, Future) {
        let sender_token: token = token {
            owner: consumed_token.owner,
            amount: consumed_token.amount - amount,
        };
        let finalize_future: Future = finalize_transfer_private_to_public(
            receiver,
            amount,
        );
        return (sender_token, finalize_future);
    }
    async function finalize_transfer_private_to_public(
        receiver: address, amount: u64,
    ) {
        let to_balance: u64 = account.get_or_use(receiver, 0u64);
        to_balance += amount;
        account.set(receiver, to_balance);
    }

    async transition transfer_public_to_private(
        private receiver: address, // to the receiver
        public amount: u64, // amount to transfer
    ) -> (token, Future) {
        let receiver_token: token = token {
            owner: receiver,
            amount: amount,
        };
        let finalize_future: Future = finalize_transfer_public_to_private(
            self.caller,
            amount,
        );
        return (receiver_token, finalize_future);
    }
    async function finalize_transfer_public_to_private(
        caller: address, amount: u64,
    ) {
        let from_balance: u64 = account.get(caller);
        from_balance -= amount;
        account.set(caller, from_balance);
    }
}

Approval mechanism

An extra mechanism that's defined in this standard is approvals. The idea behind approvals is to allow other account to spend public tokens you own. In particular, this is interesting to allow programs to spend your tokens under the conditions defined in the program code.

To understand how this works, let's first define a struct: approval which includes an approver and a spender address.

    struct approval {
        approver: address,
        spender: address,
    }

We'll track the u64 amount approved by the approver to be spent by the spender using a specific mapping:

    mapping approvals: field => u64;

The key of that mapping is the hash of the corresponding approval struct, and the value is the amount approved to be spent.

Two transitions are defined to create and remove such approvals. The caller is always the approver:

    async transition approve_public(
        private spender: address, // spender
        public amount: u64, // amount spender is allowed to withdraw from approver
    ) -> Future {
        let apvl: approval = approval {
            approver: self.caller,
            spender: spender,
        };
        let apvl_hash: field = BHP256::hash_to_field(apvl);
        return finalize_approve_public(apvl_hash, amount);
    }
    async function finalize_approve_public (apvl_hash: field, amount: u64) {
        let approved: u64 = approvals.get_or_use(apvl_hash, 0u64);
        approved += amount;
        approvals.set(apvl_hash, approved);
    }

    async transition unapprove_public(
        private spender: address, // spender
        public amount: u64, // amount spender is allowed to withdraw from approver
    ) -> Future {
        let apvl: approval = approval {
            approver: self.caller,
            spender: spender,
        };
        let apvl_hash: field = BHP256::hash_to_field(apvl);
        return finalize_unapprove_public(apvl_hash, amount);
    }
    async function finalize_unapprove_public (apvl_hash: field, amount: u64) {
        let approved: u64 = approvals.get(apvl_hash);
        approved -= amount;
        approvals.set(apvl_hash, approved);
    }

A specific transition is included in the standard to spend tokens of other addresses, that have been priorly approved:

    async transition transfer_from_public(
        public approver: address, // from the approver
        public receiver: address, // to the receiver
        public amount: u64, // amount to transfer
    ) -> Future {
        let apvl: approval = approval{
            approver: approver,
            spender: self.caller,
        };
        let apvl_hash: field = BHP256::hash_to_field(apvl);
        return finalize_transfer_from_public(
            apvl_hash,
            approver,
            receiver,
            amount,
        );
    }
    async function finalize_transfer_from_public(
        apvl_hash: field, approver: address, receiver: address, amount: u64,
    ) {
        let approved: u64 = approvals.get(apvl_hash);
        approved -= amount;
        approvals.set(apvl_hash, approved);

        let from_balance: u64 = account.get(approver);
        from_balance -= amount;
        account.set(approver, from_balance);

        let to_balance: u64 = account.get_or_use(receiver, 0u64);
        to_balance += amount;
        account.set(receiver, to_balance);
    }

And that's it, this is all there is to know about the fungible token standard.

The full code for the standard can be found here.

Although an issue with this standard occures when you don't have dynamic composability. The only way to make programs leveraging such tokens, as for instance a swap between token A and token B, is to deploy such a swap program, importing both token programs, for each pair of tokens on the network.

In next chapter we'll look into the widely adopted workaround for this problem: the token registry program.

Last updated