Whisk ChangesWith the release of Whisk, Protocol 23, the token interface has seen some changes to incorporate the
MuxedAddress type into the transfer function. Please see the Rust SDK documentation for more details.Run the Example
First go through the Setup process to get your development environment configured, then clone thesoroban-examples repository:
hello_world directory, and use cargo test.
Code
The source code for this token example is broken into several smaller modules. This is a common design pattern for more complex smart contracts.
- lib
- admin
- allowance
- balance
- contract
- metadata
- storage_types
token/src/lib.rs
How it Works
Tokens created on a smart contract platform can take many different forms, include a variety of different functionalities, and meet very different needs or use-cases. While each token can fulfill a unique niche, there are some “normal” features that almost all tokens will need to make use of (e.g., payments, transfers, balance queries, etc.). In an effort to minimize repetition and streamline token deployments, Soroban implements the Token Interface, which provides a uniform, predictable interface for developers and users. Creating a Soroban token compatible contract from an existing Stellar asset is very easy, it requires deploying the built-in Stellar Asset Contract. This example contract, however, demonstrates how a smart contract token might be constructed that doesn’t take advantage of the Stellar Asset Contract, but does still satisfy the commonly used Token Interface to maximize interoperability.Separation of Functionality
You have likely noticed that this example contract is broken into discrete modules, with each one responsible for a siloed set of functionality. This common practice helps to organize the code and make it more maintainable. For example, most of the token logic exists in thecontract.rs module. Functions like mint, burn, transfer, etc. are written and programmed in that file. The Token Interface describes how some of these functions should emit events when they occur. However, keeping all that event-emitting logic bundled in with the rest of the contract code could make it harder to track what is happening in the code, and that confusion could ultimately lead to errors.
Instead, we have a separate soroban_token_sdk::events module that takes away all the headache of emitting events when other functions run. Here is the event emitted when a token is minted:
contract.rs module whenever the mint function is invoked:
Standardized Interface, Customized Behavior
This example contract follows the standardized Token Interface, implementing all of the same functions as the Stellar Asset Contract (SAC). This gives wallets, users, developers, etc. a predictable interface to interact with the token. Even though we are implementing the same interface of functions, that doesn’t mean we have to implement the same behavior inside those functions. While this example contract doesn’t actually modify any of the functions that would be present in a deployed instance of the SAC, that possibility remains open to the contract developer. By way of example, perhaps you have an NFT project, and the artist wants to have a small royalty paid every time their token transfers hands:transfer interface is still in use, and is still the same as other tokens, but we’ve customized the behavior to address a specific need. Another use-case might be a tightly controlled token that requires authentication from an admin before any transfer, allowance, etc. function could be invoked.
Tests
Open thetoken/src/test.rs file to follow along.
token/src/test.rs
Env, which is the Soroban environment that the contract will run in.
create_token function to ease the repetition of having to register our token contract. The resulting token client is then used to invoke the contract during each test.
impl block that has been annotated with the #[contractimpl] attribute will have a corresponding function in the test’s generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract, the contract type is named Token, and the client type is named TokenClient.
The six tests created for this example contract test a range of possible conditions and ensure the contract responds appropriately to each one:
test()- This function makes use of a variety of the built-in token functions to test the “predictable” way an asset might be interacted with by a user, as well as an administrator.test_burn()- This function ensures aburn()invocation decreases a user’s balance, and that aburn_from()invocation decreases a user’s balance as well as consuming another user’s allowance of that balance.transfer_insufficient_balance()- This function ensures atransfer()invocation panics when thefromuser doesn’t have the balance to cover it.transfer_from_insufficient_allowance()- This function ensures a user with an existing allowance for someone else’s balance cannot make atransfer()greater than that allowance.decimal_is_over_eighteen()- This function tests that constructing a token with too high of a decimal precision will not succeed.test_zero_allowance()- This function makes sure that atransfer_from()with an zero balance doesn’t create an empty allowance.
Build the Contract
To build the contract, use thestellar contract build command.
.wasm file should be outputted in the target directory:
Run the Contract
If you havestellar-cli installed, you can deploy the contract and invoke its functions.
Deploy
- macOS/Linux
- Windows (PowerShell)
Invoke
- macOS/Linux
- Windows (PowerShell)