介绍

Move是一种安全、沙盒式和形式化验证的下一代编程语言,它的第一个用例是Diem区块链(当时名字叫Libra, 脸书团队开发的项目, 译者注),Move为其实现提供了基础。 Move 允许开发人员编写灵活管理和转移数字资产的程序,同时提供安全保护,防止对那些链上资产的攻击。不仅如此,Move 也可用于区块链世界之外的开发场景。

Move 的诞生从Rust中吸取了灵感,Move也是因为使用具有移动(move)语义的资源类型作为数字资产(例如货币)的显式表示而得名。

今天,Move 及其虚拟机正在为 多个区块链 提供动力,其中大部分仍处于早期开发阶段。

什么是Move Patterns

本书旨在讨论面向资源的语言的软件设计模式和最佳实践,尤其是Move及其风格。这部分涵盖了 Move 中广泛使用的编程模式,为什么会存在Move设计模式,主要有以下三个方面。

  • 面向资源编程 Move是一种新的编程语言,其特点是面向资源编程,对于区块链最核心的 Token 资产进行了更为贴合的处理,实现了真正意义上的数字资产化。Move与传统面向对象的编程语言有着很大不同,所以一些面向对象的设计模式在Move中是不存在的(例如Uniswap中的工厂模式)。当然,这些模式中的一些很可能也可以在其他一些基于资源的语言和生态系统中实现。

  • 状态存储机制 在Solidity中,能够定义并保存自己的状态变量,变量的值放在全局储存上,在合约中可以直接通过全局变量直接读取或者修改它。

    // A solidity examply
    // set msg.sender to owner
    contract A {
        // 定义一个状态变量
        address owner;
        function setOwner() public {
    	// 通过变量名直接修改
            owner = msg.sender;
        }
    }
    

    但是在Move中存储方式是完全不一样的,Move合约并不直接存储资源,代码中的每一个变量都是一个资源对象,是资源对象那么必须通过显示的接口去明确的调用。

    Move中对资源的访问都是通过全局存储操作接口来访问的。操作函数包括move_to或者move from或者borrow_globalborrow_global_mut等函数。从全局储存里面取出资源,或者存放到账户下去,引用资源对象或者修改,都是需要要开发者显示的去表示。

    module example::m {
        // A Coin type
        // 一种Coin类型的资源
        struct Coin has key, store{
            value: u64
        }
        // send sender a coin value of 100
        // 在sender地址下存放100个coin
        public entry fun mint(sender: &signer) {
            move_to(sender, Coin {
                value: 100
            });
        }
    }
    
  • Ability Ability是 Move 语言中的一种类型特性,用于控制对给定类型的值允许哪些操作。

    • copy复制:允许此类型的值被复制
    • drop 丢弃:允许此类型的值被弹出/丢弃,没有话表示必须在函数结束之前将这个值销毁或者转移出去。
    • store 存储:允许此类型的值存在于全局存储中或者某个结构体中
    • key 键值:允许此类型作为全局存储中的键(具有 key 能力的类型才能保存到全局存储中)

作为面向资源的编程语言,以上三个特点也是与其他语言非常不同的地方,基于资源编程的Move编程模式,也主要是围绕这些特性产生的。

此外本书不是 Move 或任何其他面向资源的语言的指南。 有关 Move 本身的书籍,请参阅 awesome-move#books。 另请参阅 awesome-move 以获取来自 Move 编程语言社区的代码和内容的精选列表。

Technical disclaimer

This book is designed to be viewed digitally with hyperlink support (such as PDF or web format). For now the full software pattern format is not followed, instead the patterns are simply defined by a short summary and examples.

License

CC BY 4.0 license button

Move Patterns: Design Patterns for Resource Based Programming © 2022 by Ville Sundell is licensed under CC BY 4.0. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/.

Capability

NameCapability
OriginLibra Project / Unknown Author
ExampleSui Move by Example / Damir Shamanaev
Depends onNone
Known to work onMove

概述

Capability是一个能够证明资源所有者特定权限的资源(注意:它是一个资源也就是一个Move中的结构体),其作用主要是用来进行访问控制。

例如当我们想限制某个资源的铸造权,管理权,函数调用权时,便可以采用Capability这种设计模式。这也是Move智能合约里面使用最广泛的一个设计模式,例如sui-framework中的TreasuryCap。这是也是已知最古老的 Move 设计模式,可追溯到 Libra 项目及其代币智能合约,其中功能用于授权铸币。

如何使用

Capability本质是一个资源对象,只是被可信任的用户持有。通常在合约中我们可以定义一个AdminCap来代表本模块的控制权限,如果某个用户持有就可以用户可信,其中资源对象内不需要任何的字段。

struct AdminCap has key, store {}

一般Capability生成在模块初始化的时候,例如Sui中的init函数,就可以赋予部署者一个Capability的资源,然后通过move_to然后储存到它的账户下。

然后当需要使用到有访问权限的函数时,此时函数就会检查调用者地址下是否存在这个Capability资源,如果存在那么说明调用者拥有正确的访问权限。

Aptos

module example::capability {
    use std::signer;

    // 定义一个OwnerCapability类型
    struct OwnerCapability has key, store {}

    // 向管理者地址下存放一个OwnerCapability资源
    public entry fun init(sender: signer) {
        assert!(signer::address_of(&sender) == @example, 0);
        move_to(&sender, OwnerCapability {})
    }

    // Only user with OwnerCapability can call this function
    // 只有具有OwnerCapability的用户才能调用此函数
    public entry fun admin_fun(sender: &signer) acquires OwnerCapability {
        assert!(exists<OwnerCapability>(signer::address_of(sender)), 1);
        let _cap = borrow_global<OwnerCapability>(signer::address_of(sender));
        // do something with the cap.
    }
}

Sui

sui中的Move与Aptos或者starcoin中的Core Move有所不同,sui封装了全局操作函数,具体可以查看sui的官方文档

module capability::m {
    use sui::transfer;
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    struct OwnerCapability has key { id: UID }

    /// A Coin Type
    struct Coin has key, store {
        id: UID,
        value: u64
    }

    /// Module initializer is called once on module publish.
    /// Here we create only one instance of `OwnerCapability` and send it to the publisher.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(OwnerCapability {
            id: object::new(ctx)
        }, tx_context::sender(ctx))
    }

    /// The entry function can not be called if `OwnerCapability` is not passed as
    /// the first argument. Hence only owner of the `OwnerCapability` can perform
    /// this action.
    public entry fun mint_and_transfer(
        _: &OwnerCapability, to: address, ctx: &mut TxContext
    ) {
        transfer::transfer(Coin {
            id: object::new(ctx),
            value: 100,
        }, to)
    }
}

总结

相较于其他语言的访问控制(例如Solidity中定一个address owner即可,或者定义一个mapping),Move中的访问控制实现上是复杂的,主要由于Move中独特的存储架构,模组不存储状态变量,需要将资源存储到一个账户下面。

但是单单对于对于访问控制来说,实现方式有很多种,例如在Move中可以使用VecMap等数据结构达到同样的效果,但是Capability这种模式跟面向资源编程的概念更加契合且容易使用。

需要注意的是,既然Capability作为一个凭证,显然是不能copy的能力的,如果别人拿到copy之后,它可以通过复制从而获得更多的Capability。同样在正常的业务逻辑下,Capability也是不能随意的丢弃也就是不能有drop能力,因为丢弃的话显然会造成一些不可逆的影响。

Wrapper

NameWrapper
OriginVille Sundell
ExampleTaoHe project
Depends onNone
Known to work onMove

概述

Wrapper是将一个对象嵌套在另一个对象的模式。例如在上个Capability的例子中,通常合约初始化时会铸造一个Capability,那么在这之后如果我想将这个Capability转让给其他人怎么办?

在Move中我们往一个地址下存放数据时,会使用到move_to函数,但是此函数接受的参数是signer,也就是没有账号的签名发起的交易是没有办法主动向地址下存放资源。所以在Move编程实践的当中就产生了Wrapper这么一个模式。

Wrapper模式指的是,如果需要主动给一个地址发送一个资源时,首先把要发送的对象放在一个Wrapper对象里面包装一下,随后用户需要主动调用接受Wrapper的函数,此时函数会通过Wrapper这个资源取出来其中被包装的对象,由于是用户主动调用的该函数,所以可以直接存放到用户下面。

这种场景总主要是因为不能直接发送,所以需要先预把对象,预存到一个地方,然后接受者再主动去确认拿取,就如同现实生活中Offer需要确认一般,所以这种模式一般也叫做Offer模式

如何使用

Aptos

为了实现资源的转移,可以在合约中可以定义一个Offer资源,类型参数接受一个泛型T,从而可以给任意类型包装,然后定义一个receipt字段进行访问控制。

用户接受Offer时,首先从Offer地址下取出来,随后在验证地址是否和Offer中的地址相同。

module example::offer {
    use std::signer;

    struct OwnerCapability has key, store {}

    /// 定义一个Offer来包装一个对象
    struct Offer<T: key + store> has key, store {
        receipt: address,
        offer: T,
    }
    /// 发送一个Offer到地址to下
    public entry fun send_offer(sender: &signer, to: address) {
        move_to<Offer<OwnerCapability>>(sender, Offer<OwnerCapability> {
            receipt: to,
            offer: OwnerCapability {},
        });
    }
    /// 地址to调用函数从而接受Offer中的对象
    public entry fun accept_role(sender: &signer, grantor: address) acquires Offer {
        assert!(exists<Offer<OwnerCapability>>(grantor), 0);
        let Offer<OwnerCapability> { receipt, offer: admin_cap } = move_from<Offer<OwnerCapability>>(grantor);
        assert!(receipt == signer::address_of(sender), 1);
        move_to<OwnerCapability>(sender, admin_cap);
    } 
}

Sui

Sui中由于资源都是一个Object,每一个对象都是属于一个Owner的,所以需要主动给用户发送资源时,只需要使用transfer::transfer(obj, to),不需要使用Offer模式

总结

Wrapper中由于定义了一个receipt字段限制了只能有指定的地址来接受Offer,在其他场景,例如需要赋予多个人一个Capability又如何解决?此外在Move中,一个账户下只能存放一种类型的资源,那么针对上面的情况如果想再存放另外一个人的Offer也是不可行的。

所以在我们实践中,可以将receipt字段替换为一个vector或者一个table来储存多个目标地址。

/// 定义一个Offer来包装一个对象
struct Wrapper<T: key + store> has key, store {
    receipt: address,
    map: VecMap<T>,
}

此外在其他场景中如果没有提供存放资源的接口,那么可以通过Wrapper来将其保存。

例如下面的Coin模块,由于Move中的特性,定义的资源只能在定义这个资源的模块内操作,所以当mint函数返回一个Coin对象时,用户不能直接使用move_to来存放。

module example::coin {
    struct Coin has key, store {
        value: u64
    }
    struct MintCapability has key, store {}

    public fun mint(amount: u64, _cap: &MintCapability): Coin {
        Coin { value: amount }
    }
}

那么此时用户可以定义一个自己的Wrapper结构将其包装,随后就可以将这个嵌套结构一同放到自己的地址下。

struct Wrapper has key, store {
        coin: Coin
}

Witness

NameWitness
OriginFastX / Sam Blackshear
ExampleSui Move by Example / Damir Shamanaev
Depends onNone
Known to work onMove

概述

witness是一种临时资源,相关资源只能被使用一次,资源在使用后被丢弃,确保不能重复使用相同的资源来初始化任何其他结构,通常用来确认一个类型的的所有权。

witness得益于Move中的类型系统。一个类型实例化的时候,它只能在定义这个类型的模块中创建。

一个简单的例子,在framework里面定义了coin合约用来定义token标准,如果想要注册token那么合约会调用publish_coin

module framework::coin {
    /// The witness patameter ensures that the function can only be called by the module defined T.
    public fun publish_coin<T: drop>(_witness: T) {
        // register this coin to the registry table
    }
}
module examples::xcoin {
    use framework::coin;
    /// The Witness type.
    struct X has drop {}
    /// Only this module defined X can call framework::publish_coin<X>
    public fun publish() {
        coin::publish_coin<X>(X {});
    }
}
module hacker::hack {
    use framework::coin;
    use examples::xcoin::X;

    public fun publish() {
        // Illegal, X can not be constructed here.
        coin::publish_coin<X>(X {}); 
    }
}

那么如果此时有一个hacker想要抢先注册这个token,那么需要构造模块中的x提前调用publish_coin函数,但是由于Move中的类型系统限制了这种情况的发生,因为模块外部是不能构造其他模块的结构体资源。

// Move编译器报错
┌─ /sources/m.move:25:31
   │
25 │         coin::publish_coin<X>(X {}); 
   │                               ^^^^ Invalid instantiation of '(examples=0x1)::xcoin::X'.
All structs can only be constructed in the module in which they are declared

如何使用

witness在Sui中与其他Move公链有一些区别。

如果结构类型与定义它的模块名称相同且是大写,并且没有字段或者只有一个布尔字段,则意味着它是一个one-time witness类型。该类型只会在模块初始化时使用,在合约中验证是否是one-time witness类型,可以通过sui framwork中types::is_one_time_witness来验证。

例如在sui的coin库中,如果需要注册一个coin类型,那么需要调用create_currency函数。函数参数则就需要一个one-time witness类型。为了传递该类型参数,需要在模块初始化init函数参数中第一个位置传递,即:

// 注册一个M_COIN类型的通用Token
module examples::m_coin {
    use sui::coin;
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

		// 必须是模块名大写字母
    struct M_COIN has drop{}

		// 第一个位置传递
    fun init (witness: M, ctx: &mut TxContext) {
        let cap = coin::create_currency(witness, 8, ctx);
        transfer::transfer(cap, tx_context::sender(ctx));
    }
}

sui中的初始化函数只能有一个或者两个参数,且最后的参数一定是&mut TxContext类型,one-time witness类型同样是模块初始化时自动传递的。

init函数如果传递除了上述提到的以外的参数,Move编译器能够编译通过,但是部署时Sui的验证器会报错。此外如果第一个传递的参数不是one-time witness类型,同样也只会在部署时Sui验证才会报错。

总结

witness模式通常其他模式一同使用,例如Wrapper和capability模式。

除了在sui的coin标准库中使用到了wintess,以下例子同样也有使用到:

Hot Potato

NameHot Potato
OriginSui Project / Todd Nowacki
ExampleFlashLender.move
Depends onNone
Known to work onMove

概述

Hot Potato模式受益于Move中的Ability,Hot Potato是一个没有keystoredrop能力的结构,强制该结构在创建它的模块中使用掉。这种模式在闪电贷款这样的需要原子性的程序中是理想的,因为在同一交易中必须启动和偿还贷款。

struct Hot_Potato {}

相较于Solidity中的闪电贷的实现,Move中的实现是优雅的。在Solidity中会涉及到较多的动态调用,并且存在重入,拒绝服务攻击等问题。但在Move中,当函数返回了一个不具有任何的ability的potato时,由于没有drop的ability也,所以没办法储存到全局里面去,也没有办法去储存到其他结构体中。在函数结束的时也不能丢弃,所以必须解构这个资源,或者传给另外一个可以使用这个potato的一个函数。

所以通过这个方式,可以来实现函数的调用流程。模块可以在没有调用者任何背景和条件下,保证调用者一定会按照预先设定的顺序去调用函数

闪电贷本质也是一个调用顺序的问题

如何使用

Aptos

Aptos上Liqudswasp项目实现了FlashLoan,这里提取了核心的代码。

public fun flashloan<X, Y, Curve>(x_loan: u64, y_loan: u64): (Coin<X>, Coin<Y>, Flashloan<X, Y, Curve>)
    acquires LiquidityPool, EventsStore {
        let pool = borrow_global_mut<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account);
        ...
        let reserve_x = coin::value(&pool.coin_x_reserve);
        let reserve_y = coin::value(&pool.coin_y_reserve);
        // Withdraw expected amount from reserves.
        let x_loaned = coin::extract(&mut pool.coin_x_reserve, x_loan);
        let y_loaned = coin::extract(&mut pool.coin_y_reserve, y_loan);
        ...
        // Return loaned amount.
        (x_loaned, y_loaned, Flashloan<X, Y, Curve> { x_loan, y_loan })
    }

public fun pay_flashloan<X, Y, Curve>(
        x_in: Coin<X>,
        y_in: Coin<Y>,
        loan: Flashloan<X, Y, Curve>
    ) acquires LiquidityPool, EventsStore {
        ...
        let Flashloan { x_loan, y_loan } = loan;

        let x_in_val = coin::value(&x_in);
        let y_in_val = coin::value(&y_in);

        let pool = borrow_global_mut<LiquidityPool<X, Y, Curve>>(@liquidswap_pool_account);

        let x_reserve_size = coin::value(&pool.coin_x_reserve);
        let y_reserve_size = coin::value(&pool.coin_y_reserve);

        // Reserve sizes before loan out
        x_reserve_size = x_reserve_size + x_loan;
        y_reserve_size = y_reserve_size + y_loan;

        // Deposit new coins to liquidity pool.
        coin::merge(&mut pool.coin_x_reserve, x_in);
        coin::merge(&mut pool.coin_y_reserve, y_in);
        ...
    }

Sui

sui官方示例中同样实现了闪电贷。

当用户借款时调用loan函数返回一笔资金coin和一个记录着借贷金额value但没有任何abilityreceipt收据,如果用户试图不归还资金,那么这个收据将被丢弃从而报错,所以必须调用repay函数从而销毁收据。收据的销毁完全由模块控制,销毁时验证传入的金额是否等于收据中的金额,从而保证闪电贷的逻辑正确。

module example::flash_lender {
    use sui::balance::{Self, Balance};
    use sui::coin::{Self, Coin};
    use sui::object::{Self, ID, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// A shared object offering flash loans to any buyer willing to pay `fee`.
    struct FlashLender<phantom T> has key {
        id: UID,
        /// Coins available to be lent to prospective borrowers
        to_lend: Balance<T>,
        /// Number of `Coin<T>`'s that will be charged for the loan.
        /// In practice, this would probably be a percentage, but
        /// we use a flat fee here for simplicity.
        fee: u64,
    }

    /// A "hot potato" struct recording the number of `Coin<T>`'s that
    /// were borrowed. Because this struct does not have the `key` or
    /// `store` ability, it cannot be transferred or otherwise placed in
    /// persistent storage. Because it does not have the `drop` ability,
    /// it cannot be discarded. Thus, the only way to get rid of this
    /// struct is to call `repay` sometime during the transaction that created it,
    /// which is exactly what we want from a flash loan.
    struct Receipt<phantom T> {
        /// ID of the flash lender object the debt holder borrowed from
        flash_lender_id: ID,
        /// Total amount of funds the borrower must repay: amount borrowed + the fee
        repay_amount: u64
    }

    /// An object conveying the privilege to withdraw funds from and deposit funds to the
    /// `FlashLender` instance with ID `flash_lender_id`. Initially granted to the creator
    /// of the `FlashLender`, and only one `AdminCap` per lender exists.
    struct AdminCap has key, store {
        id: UID,
        flash_lender_id: ID,
    }
    
    // === Creating a flash lender ===

    /// Create a shared `FlashLender` object that makes `to_lend` available for borrowing.
    /// Any borrower will need to repay the borrowed amount and `fee` by the end of the
    /// current transaction.
    public fun new<T>(to_lend: Balance<T>, fee: u64, ctx: &mut TxContext): AdminCap {
        let id = object::new(ctx);
        let flash_lender_id = object::uid_to_inner(&id);
        let flash_lender = FlashLender { id, to_lend, fee };
        // make the `FlashLender` a shared object so anyone can request loans
        transfer::share_object(flash_lender);

        // give the creator admin permissions
        AdminCap { id: object::new(ctx), flash_lender_id }
    }

    // === Core functionality: requesting a loan and repaying it ===

    /// Request a loan of `amount` from `lender`. The returned `Receipt<T>` "hot potato" ensures
    /// that the borrower will call `repay(lender, ...)` later on in this tx.
    /// Aborts if `amount` is greater that the amount that `lender` has available for lending.
    public fun loan<T>(
        self: &mut FlashLender<T>, amount: u64, ctx: &mut TxContext
    ): (Coin<T>, Receipt<T>) {
        let to_lend = &mut self.to_lend;
        assert!(balance::value(to_lend) >= amount, ELoanTooLarge);
        let loan = coin::take(to_lend, amount, ctx);
        let repay_amount = amount + self.fee;
        let receipt = Receipt { flash_lender_id: object::id(self), repay_amount };

        (loan, receipt)
    }

    /// Repay the loan recorded by `receipt` to `lender` with `payment`.
    /// Aborts if the repayment amount is incorrect or `lender` is not the `FlashLender`
    /// that issued the original loan.
    public fun repay<T>(self: &mut FlashLender<T>, payment: Coin<T>, receipt: Receipt<T>) {
        let Receipt { flash_lender_id, repay_amount } = receipt;
        assert!(object::id(self) == flash_lender_id, ERepayToWrongLender);
        assert!(coin::value(&payment) == repay_amount, EInvalidRepaymentAmount);

        coin::put(&mut self.to_lend, payment)
    }
}

总结

Hot Potato设计模式不仅仅只适用于闪电贷的场景,还可以用来控制更复杂的函数调用顺序。

例如我们想要一个制作土豆的合约,当用户调用get_potato时,会得到一个没有任何能力的potato,我们想要用户得倒之后,按照切土豆、煮土豆最后才能吃土豆的一个既定流程来操作。所以用户为了完成交易那么必须最后调用consume_potato,但是该函数限制了土豆必须被cutcook,所以需要分别调用cut_potatocook_potatocook_potato中又限制了必须先被cut,从而合约保证了调用顺序必须为get→cut→cook→consume,从而控制了调用顺序。

module example::hot_potato {
    /// Without any capability,
    struct Potato {
        has_cut: bool,
        has_cook: bool,
    }
    /// When calling this function, the `sender` will receive a `Potato` object.
    /// The `sender` can do nothing with the `Potato` such as store, drop,
    /// or move_to the global storage, except passing it to `consume_potato` function.
    public fun get_potato(_sender: &signer): Potato {
        Potato {
            has_cut: false,
            has_cook: false,
        } 
    }

    public fun cut_potatoes(potato: &mut Potato) {
        assert!(!potato.has_cut, 0);
        potato.has_cut = true;
    }

    public fun cook_potato(potato: &mut Potato) {
        assert!(!potato.has_cook && potato.has_cut, 0);
        potato.has_cook = true;
    }

    public fun consume_potato(_sender: &signer, potato: Potato) {
        assert!(potato.has_cook && potato.has_cut, 0);
        let Potato {has_cut: _, has_cook: _ } = potato; // destroy the Potato.
    }
}

Multiple Configurations

NameMultiple Configurations
OriginVille Sundell
ExampleDiem Forum Post
Depends onNone
Known to work onMove

Summary

In the Multiple Configurations approach singletons (especially for configuration data) are provided to the module as arguments instead of storing them to any specific location.

By incorporating this design into your modules, you would automatically add a layer of configurability by giving users or admins (if any) a way to create configurations which others could use. This would be similar to Uniswap which lets anyone create markets others could participate in.

Examples

Let's say you have an auction smart contract X and an admin has created a configuration to account A. Then B could issue a transaction script to bid on C's resource like this:

X::place_bid(A, C, 1234);

By accepting parameters (such as C and the amount 1234), a transaction script could trivially hide the selection of the configuration. This depends, how flexible the final script implementation is in terms of arguments to main(), but the transaction script could look like this (quick mock-up):

script {
    use 0xFF::X;

    const CONF: address = 0xCOFFEE;

    fun main(a: address, amount: XUS) {
        X::place_bid(CONF, a, amount);
    }
}

Accountless Design

NameAccountless Design
OriginVille Sundell
ExampleDiem Forum Post
Depends onNone
Known to work onMove

Summary

Move module following the Accountless Design pattern doesn't handle storage (move_to() / move_from()) directly, instead the storage must be handled outside the module in transaction scripts. This makes the module code footprint smaller, design simpler, implementation more portable and provides a way to implement storage agnostic smart contract design on some Move powered platforms.

Examples

module 0x1::Outbox {
    use Std::Event;
    use Std::Signer;
    use Std::Vector;

    struct Item<Content: key + store> has key, store {
        from: address,
        to: address,
        content: Content
    }

    struct Outbox<Content: key + store> has key, store {
        content: vector<Item<Content>>
    }

    struct Put<phantom Content> has key, drop, store {
    }

    struct EventHandle<phantom Content: drop + store> has key, store {
        event_handle: Event::EventHandle<Content>
    }

    public fun create<Content: key + store>(account: &signer) {
        move_to<Outbox<Content>>(account, Outbox<Content> { content: Vector::empty<Item<Content>>() });
        move_to<EventHandle<Put<Content>>>(account, EventHandle<Put<Content>> { event_handle: Event::new_event_handle<Put<Content>>(account) } );
    }

    public fun put<Content: key + store>(account: &signer, from: address, to: address, content: Content) acquires EventHandle, Outbox {
        let outbox_owner = Signer::address_of(account);
        let event_handle = borrow_global_mut<EventHandle<Put<Content>>>(outbox_owner);
        let outbox = borrow_global_mut<Outbox<Content>>(outbox_owner);

        assert!(to != @0x0, 123);

        Vector::push_back<Item<Content>>(&mut outbox.content, Item<Content>{ from, to, content });
        Event::emit_event<Put<Content>>(&mut event_handle.event_handle, Put<Content> {});
    }

    public fun get<Content: key + store>(account: &signer, outbox_owner: address, index: u64): Content acquires Outbox {
        let account_addr = Signer::address_of(account);
        let outbox = borrow_global_mut<Outbox<Content>>(outbox_owner);

        let Item<Content>{from, to, content} = Vector::swap_remove<Item<Content>>(&mut outbox.content, index);

        assert!(from == account_addr || to == account_addr, 123);

        content
    }
}

Now Outbox can be used to retrieve and store resources in transaction scripts, and pass those to modules following the Script Based Design pattern.

Script Based Design

NameScript Based Design
OriginVille Sundell
ExampleDiem Forum post lost, April 18 2022
Depends onAccountless Design
Known to work onMove

Summary

Script Based Design combines Accountless Design with Move Transaction Scripts enabling a design where most of the business logic resides in transaction scripts, while keeping the most critical parts in modules. This way the transaction scripts can be developed and improved faster than a regular module would, while providing the same kind of guarantees as regular modules by keeping the most critical part (state transitions) in modules.

Examples

Transferable Witness

NameTransferable Witness
OriginSui Move by Example / Damir Shamanaev
ExampleSui Move by Example / Damir Shamanaev
Depends onCapability, Witness
Known to work onMove

Summary

A Transferable Witness is a semi-ephemeral storable witness wrapped into a disposable capability.

Examples

/// This pattern is based on combination of two others: Capability and a Witness.
/// Since Witness is something to be careful with, spawning it should be only
/// allowed to authorized users (ideally only once). But some scenarios require
/// type authorization by module X to be used in another module Y. Or, possibly,
/// there's a case where authorization should be performed after some time.
///
/// For these, rather rare, scerarios a storable witness is a perfect solution.
module examples::transferable_witness {
    use sui::transfer;
    use sui::id::{Self, VersionedID};
    use sui::tx_context::{Self, TxContext};

    /// Witness now has a `store` which allows us to store it inside a wrapper.
    struct WITNESS has store, drop {}

    /// Carries the witness type. Can only be used once to get a Witness.
    struct WitnessCarrier has key { id: VersionedID, witness: WITNESS }

    /// Send a `WitnessCarrier` to the module publisher.
    fun init(ctx: &mut TxContext) {
        transfer::transfer(
            WitnessCarrier { id: tx_context::new_id(ctx), witness: WITNESS {} },
            tx_context::sender(ctx)
        )
    }

    /// Unwrap a carrier and get the inner WITNESS type.
    public fun get_witness(carrier: WitnessCarrier): WITNESS {
        let WitnessCarrier { id, witness } = carrier;
        id::delete(id);
        witness
    }
}

Example for Sui Move is taken from the book Sui Move by Example by Damir Shamanaev.