Transfer to Object
In IOTA, you have the flexibility to transfer objects not only to addresses but also directly to other objects. This feature is possible because the IOTA system does not differentiate between the 32-byte ID of an address and the 32-byte ID of an object—ensuring that they do not overlap. This allows you to treat an object ID as an address during a transfer operation, establishing a parent-child relationship between the objects.
When you transfer an object to another object, the recipient object (the parent) gains control over the transferred object (the child), subject to access controls defined by the module that governs the parent object. This operation provides versatility in managing object ownership and access within smart contracts.
Transferring to an Object
The process of transferring an object to another object is similar to transferring it to an address. However, there are a few considerations:
- Ensure that the object ID you are transferring to exists and is not immutable.
- The recipient object (parent) may have specific rules or restrictions on receiving objects.
For objects with only the key
ability, the module that defines the object must implement a custom receive function.
This function is similar to custom transfer functions and is crucial for handling the transfer process correctly.
After sending an object with the key
ability, you can no longer access or use it unless specific conditions are met:
- Receiving Functions: The module of the parent object (the object to which you're sending the
key
object) must define a function to receive objects. - Child Object Handling: The module of the child object (the object being sent) must also define a function to receive the object.
- Meeting Restrictions: Both receiving functions (from the parent and child modules) must allow the transfer under their respective conditions.
- If any of the conditions or restrictions are not met, the transfer will fail, and you won’t be able to access or use the object after sending it.
Example
Below is an example of how to transfer an object to both an address and an object ID:
// 0xADD is an address
// 0x0B is an object ID
// b and c are objects
// Transfers the object `b` to the address 0xADD
iota::transfer::public_transfer(b, @0xADD);
// Transfers the object `c` to the object with object ID 0x0B
iota::transfer::public_transfer(c, @0x0B);
Both operations result in the transferred object having a new owner,
identified by the provided 32-byte address or object ID.
The process is seamless, and existing RPC methods, such as getOwnedObjects
, work identically for both addresses and object IDs.
// Get the objects owned by the address 0xADD. Returns `b`.
{
"jsonrpc": "2.0",
"id": 1,
"method": "iotax_getOwnedObjects",
"params": [
"0xADD"
]
}
// Get the objects owned by the object with object ID 0x0B. Returns `c`
{
"jsonrpc": "2.0",
"id": 1,
"method": "iotax_getOwnedObjects",
"params": [
"0x0B"
]
}
Receiving Objects
When an object c
is sent to another object p
, p
must receive c
before it can interact with it.
To receive c
, the Receiving(o: ObjectRef)
argument type is used in programmable transaction blocks (PTBs).
This argument type includes the ObjectID
, Version
, and Digest
of the object, similar to owned object arguments in PTBs. However, Receiving
PTB arguments are not passed as an owned value or mutable reference within the transaction.
The Receiving Interface in Move
The core of the receiving interface is defined in the iota::transfer
module in the IOTA framework:
module iota::transfer {
/// Represents the ability to receive an object of type `T`. Cannot be stored.
public struct Receiving<phantom T: key> has drop { ... }
/// Given mutable (i.e., locked) access to the `parent` and a `Receiving`
/// object referencing an object owned by `parent` use the `Receiving` ticket
/// and return the corresponding object.
///
/// This function has custom rules that the IOTA Move bytecode verifier enforces to ensure
/// that `T` is an object defined in the module where `receive` is invoked. Use
/// `public_receive` to receive an object with `store` outside of its defining module.
///
/// NB: &mut UID here allows the defining module of the parent type to
/// define custom access/permission policies around receiving objects sent
/// to objects of a type that it defines. You can see this more in the examples.
public native fun receive<T: key>(parent: &mut UID, object: Receiving<T>): T;
/// Given mutable (locked) access to the `parent` and a `Receiving` argument
/// referencing an object of type `T` owned by `parent` use the `object`
/// argument to receive and return the referenced owned object of type `T`.
/// The object `T` must have `store` to be received by this function, and
/// this can be called outside of the module that defines `T`.
public native fun public_receive<T: key + store>(parent: &mut UID, object: Receiving<T>): T;
...
}
Key Concepts
- Receiving Argument: Each
Receiving
argument in a PTB refers to an object of typeT
and is represented byiota::transfer::Receiving<T>
. - Receiving an Object: To receive the object, call the
iota::transfer::receive
function with a mutable reference to the parent object'sUID
. The module defining the parent object controls access and sets any restrictions on receiving the child object. - Ownership Verification: The system dynamically checks and enforces that the
UID
of the parent object actually owns the object referenced by theReceiving
parameter.
Because iota::transfer::Receiving
has only the drop
ability,
the existence of a Receiving<T>
argument represents the ability, but not the obligation,
to receive the object during the transaction.
Any object associated with a Receiving
argument remains untouched unless it is explicitly received.
Custom Receiving Rules
Similar to custom transfer policies, IOTA allows the definition of custom receivership rules for key
-only objects.
Receiving Rules Based on Object Abilities
- Module-Specific Reception: You can use the
iota::transfer::receive
function only on objects defined in the same module where thereceive
call is made, similar to theiota::transfer::transfer
function. - Public Reception: For objects that also have the
store
ability, anyone can use theiota::transfer::public_receive
function to receive them, akin to theiota::transfer::public_transfer
function.
This leads to the following matrix of permissions for receiving objects:
Child Abilities | Parent Can Restrict Access | Child Can Restrict Access |
---|---|---|
key | Yes | Yes |
key + store | Yes | No |
You can combine these restrictions to create complex rules, such as implementing soul-bound objects.
Using SDKs
When creating transactions, you interact with Receiving
transaction inputs in a similar way to other object arguments in the IOTA TypeScript SDK. For example, if you want to receive a coin object with ID 0xc0ffee
that was sent to your account at 0xcafe
, you can do the following:
- TypeScript
- Rust
... // Setup Typescript SDK as normal.
const tx = new Transaction();
tx.moveCall({
target: `${examplePackageId}::account::accept_payment`,
arguments: [tx.object("0xcafe"), tx.object("0xc0ffee")]
});
const result = await client.signAndExecuteTransaction({
transaction: tx,
});
...
... // setup Rust SDK client as normal
client
.transaction_builder()
.move_call(
sending_account,
example_package_id,
"account",
"accept_payment",
vec!["0x2::iota::IOTA"],
vec![
IotaJsonValue::from_object_id("0xcafe"),
IotaJsonValue::from_object_id("0xc0ffee") // 0xcoffee is turned into the `Receiving<...>` argument of `accept_payment` by the SDK
])
...
Additionally, similar to object arguments with an ObjectRef
constructor where you can provide an explicit object ID, version, and digest, there is also a ReceivingRef
constructor that takes the same arguments corresponding to a receiving argument.
Examples
Receiving Objects from Shared Objects
If you want to allow receiving sent objects from shared objects defined in a module, you should add dynamic authorization checks. Otherwise, anyone could receive sent objects.
Because the receive_object
function is generic over the object being received, it can only receive objects that have both key
and store
abilities.
receive_object
must also use the iota::transfer::public_receive
function to receive the object and not iota::transfer::receive
because you can only use receive
on objects defined in the current module.
In this example, a shared object (SharedObject
) holds a counter that anyone can increment,
but only the address 0xB0B
can receive objects from it:
module examples::shared_object_auth {
use iota::transfer::Receiving;
const EAccessDenied: u64 = 0;
const AuthorizedReceiverAddr: address = @0xB0B;
public struct SharedObject has key {
id: object::UID,
counter: u64,
}
public fun create(ctx: &mut TxContext) {
let s = SharedObject {
id: object::new(ctx),
counter: 0,
};
transfer::share_object(s);
}
/// Anyone can increment the counter in the shared object.
public fun increment(obj: &mut SharedObject) {
obj.counter = obj.counter + 1;
}
/// Objects can only be received from the `SharedObject` by the
/// `AuthorizedReceiverAddr`, otherwise the transaction aborts.
public fun receive_object<T: key + store>(obj: &mut SharedObject, sent: Receiving<T>, ctx: &TxContext): T {
assert!(tx_context::sender(ctx) == AuthorizedReceiverAddr, EAccessDenied);
transfer::public_receive(&mut obj.id, sent)
}
}
Receiving Objects and Adding Them as Dynamic Fields
This example defines a basic account model where an Account
object holds coin balances in dynamic fields.
The address associated with the Account
object remains consistent regardless of whether the Account
object is transferred, wrapped, or moved into a dynamic field.
module examples::account {
use iota::transfer::Receiving;
use iota::coin::{Self, Coin};
use iota::dynamic_field as df;
const EBalanceDONE: u64 = 1;
/// Account object that `Coin`s can be sent to. Balances of different types
/// are held as dynamic fields indexed by the `Coin` type's `type_name`.
public struct Account has key {
id: object::UID,
}
/// Dynamic field key representing a balance of a particular coin type.
public struct AccountBalance<phantom T> has copy, drop, store { }
/// This function will receive a coin sent to the `Account` object and then
/// join it to the balance for each coin type.
/// Dynamic fields are used to index the balances by their coin type.
public fun accept_payment<T>(account: &mut Account, sent: Receiving<Coin<T>>) {
// Receive the coin that was sent to the `account` object
// Since `Coin` is not defined in this module, and since it has the `store`
// ability we receive the coin object using the `transfer::public_receive` function.
let coin = transfer::public_receive(&mut account.id, sent);
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;
// Check if a balance of that coin type already exists.
// If it does then merge the coin we just received into it,
// otherwise create new balance.
if (df::exists_(account_uid, account_balance_type)) {
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::join(balance, coin);
} else {
df::add(account_uid, account_balance_type, coin);
}
}
/// Withdraw `amount` of coins of type `T` from `account`.
public fun withdraw<T>(account: &mut Account, amount: u64, ctx: &mut TxContext): Coin<T> {
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;
// Make sure what we are withdrawing exists
assert!(df::exists_(account_uid, account_balance_type), EBalanceDONE);
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::split(balance, amount, ctx)
}
/// Can transfer this account to a different address
/// (e.g., to an object or address).
public fun transfer_account(account: Account, to: address, _ctx: &mut TxContext) {
// Perform some authorization checks here and if they pass then transfer the account
// ...
transfer::transfer(account, to);
}
}
Soul-Bound Objects
Soul-bound objects are bound to a specific address and must remain there or be returned to it.
The following module implements this by creating a receipt that must be destroyed within the same transaction where the object is received.
The object must then be transferred back to its original location using the return_object
function.
module examples::soul_bound {
use iota::transfer::Receiving;
/// This object has `key` only -- if this had `store` we would not be
/// able to ensure it is bound to whatever address we sent it to
public struct SoulBound has key {
id: UID,
}
/// A non-store, non-drop, non-copy struct. When you receive a `SoulBound`
/// object, we'll also give you one of these. In order to successfully
/// execute the transaction you need to destroy this `ReturnReceipt` and
/// the only way to do that is to transfer it back to the same object you
/// received it from in the transaction using the `return_object` function.
public struct ReturnReceipt {
// The object ID of the object that needs to be returned.
// This field is required to prevent swapping of soul bound objects if
// multiple are present in the same transaction.
object_id: ID,
// The address (object ID) it needs to be returned to.
return_to: address,
}
/// Tried to return the wrong object.
const EWrongObject: u64 = 0;
/// Takes the object UID that owns the `SoulBound` object and a `SoulBound`
/// receiving ticket. It then receives the `SoulBound` object and returns a
/// `ReturnReceipt` that must be destroyed in the transaction by calling `return_object`.
public fun get_object(parent: &mut UID, soul_bound_ticket: Receiving<SoulBound>): (SoulBound, ReturnReceipt) {
let soul_bound = transfer::receive(parent, soul_bound_ticket);
let return_receipt = ReturnReceipt {
return_to: object::uid_to_address(parent),
object_id: object::id(&soul_bound),
};
(soul_bound, return_receipt)
}
/// Given a `SoulBound` object and a return receipt returns it to the
/// object it was received from. Verifies that the `receipt`
/// is for the given `soul_bound` object before returning it.
public fun return_object(soul_bound: SoulBound, receipt: ReturnReceipt) {
let ReturnReceipt { return_to, object_id } = receipt;
assert!(object::id(&soul_bound) == object_id, EWrongObject);
iota::transfer::transfer(soul_bound, return_to);
}
}