Contracts
Bako ID is a series of Smart Contracts on Fuel that enables the registration and management of @bakoid
handles. Handles on Bako ID are registered in a structure that allows both accounts and contracts to have their own identity. After registration, the handle owner has full control over it, allowing them to manage all information and resolutions associated with the handle.
Architecture Overview
Bako ID is built on two main contracts: the Registry and the Manager. The Registry serves as the entry point for registering new handles, working alongside the Manager to validate ownership during interactions with the handle and to lock primary resolvers.
Registry
The Registry is responsible for implementing the methods that allow direct interaction with handles, as well as serving as the entry point for the logic behind handle registration. The main Registry implements the following ABI:
abi Registry {
#[storage(write, read), payable]
fn register(name: String, resolver: Identity, period: u16);
#[storage(write, read)]
fn set_metadata_info(name: String, key: String, value: Metadata);
#[storage(write, read)]
fn set_owner(name: String, owner: Identity);
#[storage(write, read)]
fn set_resolver(name: String, resolver: Identity);
}
Register
Responsible for registering a new handle, the method reverts when:
The Asset ID ≠ ETH - Reverts with
RegistryContractError::IncorrectAssetId
The handle has already been minted - Reverts with
RegistryContractError::AlreadyMinted
The handle contains invalid characters (valid: A-Z, a-z, 0-9, _) - Reverts with
NameValidationError
The Asset ID amount ≠ the handle price - Reverts with
RegistryContractError::InvalidAmount
If not reverted, the method interacts with the Token to mint the NFT and registers the record in the Manager.
Set Metadata
Method responsible for adding new metadata to the handle, which directly interacts with the Token contract implementing SRC-7 to store the data.
Only the handle owner has permission to execute the method. If called by another account, the method will revert with RegistryContractError::NotOwner
.
Set owner and Set resolver
Both methods are responsible for modifying the handle's addresses. They interact directly with the Manager contract, which updates the handle's structure while ensuring that the resolver address is correctly locked.
As with the Set Metadata method, only the owner has permission to execute these methods. Otherwise, the method reverts with RegistryContractError::NotOwner
.
Manager
This contract, together with the Registry, forms the core of Bako ID. It is responsible for storing new handles, managing a handle's addresses, and locking resolver addresses.
To ensure security, methods are implemented to manage the Admin, which in the context of Bako ID refers to the Registry contract. Only the contract owner has permission to manage the administrator addresses.
Storage
For this contract, which is responsible for storing and managing the handles, a storage structure has been designed to work with handle data in a secure and efficient manner.
The association between the handle and its data is managed through a hash calculated from the name: handle_hash = sha256(handle)
.
The storage is divided into three parts. The first part is records_data
, a Map where the key represents the handle_hash
, and the value is a structure of RecordData
:
records_data: StorageMap<b256, RecordData> = StorageMap {}
struct RecordData {
pub owner: Identity,
pub resolver: Identity,
pub period: u16,
pub timestamp: u64,
}
The records_name
is a Map where the key is the handle_hash
, and the value is a String representing the name:
records_name: StorageMap<b256, StorageString> = StorageMap {}
records_resolver
is responsible for locking primary resolvers. It represents a Map where the key is of type Identity
and the value is the handle_hash
.
A value can only be inserted if it is available, ensuring that primary resolvers are not overwritten.
records_resolver: StorageMap<Identity, b256> = StorageMap {}
ABI
abi Manager {
#[storage(read, write)]
fn set_record(name: String, data: RecordData);
#[storage(read, write)]
fn set_resolver(name: String, resolver: Identity);
#[storage(read, write)]
fn set_owner(name: String, owner: Identity);
}
Set Record
Responsible for inserting the data of a new handle into Bako ID, this method reverts when:
The
msg_sender
is not an admin or owner of the Manager: reverts withManagerError::OnlyOwner
The handle has already been minted: reverts with
ManagerError::RecordAlreadyExists
After the validation process, if no reverts occur, the handle is inserted into the storage.
Set Resolver
This method alters the resolver address in the handle's record and ensures the lockup between the address and handle. The execution is reverted when:
The
msg_sender
is not an admin or owner of the Manager: reverts withManagerError::OnlyOwner
The handle has not been minted: reverts with
ManagerError::RecordNotFound
The resolver address is locked: reverts with
ManagerError::ResolverAlreadyInUse
If no reverts occur, the method:
Alters the resolver in
storage.records_data
Removes the lockup of the old resolver address
Locks the new resolver address in
storage.records_resolver
Token
This contract follows the Fuel standards by implementing the Native Asset ABIs to represent an NFT, along with the Admin and Ownership library.
The NFT is uniquely represented by a single handle. To ensure the token is unique, its Asset ID is calculated based on the contract and its Sub ID, representing the handle's name:
asset_id = sha256(TOKEN_CONTRACT_ID ++ handle_hash);
The method mint(recipient: Identity, sub_id: Option<SubId>, amount: u64)
of the contract ensures that only one token exists per handle. It is reverted when:
The
msg_sender
is not an admin:AdminError::NotAdmin
The
sub_id
is not anOption::Some
:MintError::CannotMintMoreThanOneNFTWithSubId
The
amount
is greater than 1:MintError::CannotMintMoreThanOneNFTWithSubId
The total supply for the AssetId is already filled:
MintError::NFTAlreadyMinted
The primary functionality of the Token in the protocol is to store the handle's metadata, enabling interaction with both on-chain and off-chain information. For the keys, the standard defined in SRC-9: Metadata keys is followed, along with additional default keys defined in the TS SDK.
Resolver
The Resolver contract provides methods for on-chain resolution of handle addresses and names, such as resolving the address of a name and the name of an address.
It implements two ABIs:
AddressResolver
, responsible for resolving the handle's address.NameResolver
, responsible for resolving the name of an address.
abi AddressResolver {
#[storage(read)]
fn addr(name: String) -> Option<Identity>;
#[storage(read)]
fn owner(name: String) -> Option<Identity>;
}
abi NameResolver {
#[storage(read)]
fn name(addr: Identity) -> Option<String>;
}
All methods interact with the Manager using the getter methods defined in the ManagerInfo
ABI.
fn addr(name: String) -> Option<Identity>
`
fn addr(name: String) -> Option<Identity>
`The method for searching a resolver address through the handle returns an Option::Some
if found, and an Option::None
if not found.
fn foo(contract_id: ContractId) {
let resolver_abi = abi(AddressResolver, contract_id.bits());
let handle_address = resolver_abi.addr(String::from_ascii_str("@bako_id"));
require(handle_address.is_some(), "Handle address not found.");
}
fn name(addr: Identity) -> Option<
String>`
fn name(addr: Identity) -> Option<
String>`The method for searching a name through the resolver returns an Option::Some
if found, and an Option::None
if not found.
fn foo(contract_id: ContractId) {
let resolver_abi = abi(NameResolver, contract_id.bits());
let b256_address: b256 = 0x0000000000000000000000000000000000000000000000000000000000000000;
let handle_name = resolver_abi.name(
Identity::Address(Address::from(b256_address));
);
require(handle_address.is_some(), "Handle name not found.");
}
Last updated