0x11 | Patterns 设计模式

Patterns

设计模式 - https://move-by-example.com/core-move/patterns/index.html#patterns)

These patterns are closely related to the following Move language features :

  • Resource oriented(资源导向)

    • Resource can only be stored, transfered, destroyed or dropped restricted to the abilities.

    • Resource can only be created, readed field, modified field in the module where it is defined.(资源只能在定义它的模块中创建、读取字段、修改字段)

  • The global storage mechanism(全局存储机制)

    • The global storage can only be accessed(访问) through the move_tomove_fromborrow_globalborrow_global_mut functions.

    • Objects in the global storage can only be accessed in the module where it is defined.

Capability 模式

https://move-by-example.com/core-move/patterns/capability.html#capability)

The  Capability  pattern can be used for access controll. It is the most widely .used pattern in Move smart contracts. Capability 模式可用于访问控制,它是 Move smart contracts 中使用最广泛的模式

Why this pattern?

  • Limited by that `no storage for modules to save data.``

    • 因为 module (合约)是不能储存数据的,所有的数据都被以资源的形式储存在 account 账户下面

How to use?

  • construct a capability resource and move_to the receiptor.

  • 定义一个 Capability 的 type,比如 ownerCapability 表示是一个 module 的拥有者,把这个资源对象发送给其他人,别人有了这个资源对象,就相当于有了权限操作 module 里的内容。

注意,是一个凭证,相当于是一个权限证明,不能 Copy, 不能 Drop 丢弃! 要不就会有安全问题!

  1. 下面的例子:将 Capability 存储到用户账户下的 Storage

module examples::capability {
  use std::signer;
  const ENotPublisher: u64 = 0;
  const ENotOwner: u64 = 1;

  struct OwnerCapability has key, store {}
  public entry fun init(sender: signer) {
    assert!(signer::address_of(&sender) == @examples, ENotPublisher);
    move_to(&sender, OwnerCapability{} ); 
  }
  // Only user with Capability can call this function.
  public entry fun grant_role() acquires OwnerCapability {
    let cap = borrow_global<OwnerCapability>(signer::address_if(&signer));
    // do s.th. with the capability ...  
  }
}
  1. 将 struct 传递给其他合约,其他合约可以通过这个对象 struct 来调用响应的函数。

module examples::capability {
  // same as above ...
  
  // The returned `OwnerCapability` can be stored in other modules.
  public entry fun get_owner_cap(sender: signer): OwnerCapability {
    assert!(signer::address_of(&sender) == @examples, ENotPublisher);
    OwnerCapability {}    // retured !
  }

  // module with `OwnerCapability` can call this Function !!
  public fun grant_role_with_cap(to: address, _cap: &OwnerCapability) acquires {
    // do s.th. ....
  }

官网例子(仔细看注释)

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

    const ENotPublisher: u64 = 0;
    const ENotOwner: u64 = 1;    // Error Code.

    struct OwnerCapability has key, store {}

    /// init and publish an OwnerCapability to the module owner.
    /// 初始化并向模块所有者发布 OwnerCapability
    public entry fun init(sender: signer) {
        // 该函数的执行人需要是模块 owner,否则 Error NotPublisher
        assert!(signer::address_of(&sender) == @examples, ENotPublisher);
        // 将该资源 move_to 到 sender 地址下
        move_to(&sender, OwnerCapability {})
    }

    /// mint to `_to` with the OwnerCapability.
    public fun mint_with_capability(_amount: u64, _to: address, _cap: &OwnerCapability) {
        // mint and deposit to `_to` , mint and 存款
    }

    /// mint entry function. Only signer with OwnerCapability can call this function.  mint 入口函数,只有拥有 OwnerCapability 的签名者才能调用此函数。
    public entry fun mint(sender: &signer, to: address, amount: u64) acquires OwnerCapability {
        // 如果调用者 sender 不具备 OwnerCapability ,则 Error Not Owner
        assert!(exists<OwnerCapability>(signer::address_of(sender)), ENotOwner);
        
        // 接受 borrow_global 的返回值,borrow_global 会从全局状态中暂借出 owner 的 OwnerCapability 能力。 
        let cap = borrow_global<OwnerCapability>(signer::address_of(sender));
        mint_with_capability(amount, to, cap);
    }
}

OwnerCapability:定义了(属主 owner 有 ?) key、store 能力(为全局存储操作的键值)

Offer 模式

https://move-by-example.com/core-move/patterns/offer.html#offer)

The Offer pattern is used to transfer new objects to others, such as capability delegation, etc. Offer 模式用于将新对象传递给其他对象,例如能力委托等。

This pattern is intraoduced because the restriction that in Move, move_to requires a signer, which means that an account cannot directly send an object to another account. So we need to use the Offer pattern to achieve this. 这种模式是内部引入的,因为在 Move 中,move_to 需要 signer 签署,这意味着 A 帐户不能直接无许可地将对象发送到 B 帐户(因为需要 move_to 到 B 账户下面去,而 move_to 这个动作需要 B 账户来 sign 签署)。所以我们需要使用 Offer pattern 来实现这一点。

发送者将要发送的对象放在一个 Offer 里面,然后 receptor 接收 Offer 对象,再放到自己的账户下面。

例子 1 : 小伦想发送给小明一个 AdminCapability,但是小伦不知道小明什么时候在家 (小明无法实时 sign 签署),所以不能贸然寄给小明。所以他们俩约定了一个流程:

  • 小伦将 AdminCapability 包裹在 Offer 里,里面写上了小明的 address ( receipt: to)

  • 目标接收人(小明)如果想获取 AdminCapability 的话,需要自己去申请打开 Offer 包裹,即 claim 调用 accept_role ,然后合约会检查小明的地址 (address_of(sender) ) 是不是 Offer 包裹里写入的 address . ( 即:只有小明才能打开这个写着小明 address 的 Offer 包裹)

module examples::offer {
  struct Offer<T: key+store> has key, store {
    receipt: address,
    offer: T,
  }
  
  /// The owner can grant the `admin` role to another address `to`:
  public entry fun grant_role_offer(sender: &signer, to: address) acquires OwnerCapability {
    assert!(exists<OwnerCapability>(signer::address_of(sender)), ENotOwner);
    move_to<Offer<AdminCapability>>(sender, 
      Offer<AdminCapability> {
        receipt: to,
        offer: AdminCapability {},
      }
    );
  }

  /// The receiptor accept an `offer` from `grantor` and save it in his account.
  public entry fun accept_role(sender: &signer, grantor: address) acquires Offer {
    assert!(exists<Offer<AdminCapability>>(grantor), ENotGrantor);
    let Offer<AdminCapability> { receipt, offer: admin_cap = move_from<Offer<AdminCapability>>(grantor);
    assert!(receipt == signer::address_of(sender), ENotReceipt);  // Attention.
    move_to<AdminCapability>(sender, admin_cap);
  }
}

真正的 Wrapper —— 把别的合约里的资源 Wrapper 起来自己用。

module examples::coin {
  struct Coin has key, store {
    value: u64,
  }
  struct MintCapability has key, store {}
  public fun mint(amount: u64, _cap: &MintCapability): Coin {
    Coin { value: amount }
  }
}

module examples::wrapper {
  use examples::coin::{Self, Coin, MintCapability};
  struct MintCapabilityWrapper has key {
    mint_cap: MintCapability,
  }
  public entry fun mint(sender: &signer, amount: u64) acquires MintCapabilityWrapper {
    // check if the sender has can mint with own policy 
    // 这边省略了一些检查—— 检查 sender 是否符合政策 —— 在可 mint 的白名单里
    // ..

    // 符合 : 发放 mint 能力:
    let wrapper = borrow_global<MintCapabilityWrapper>(@examples);
    let coin = coin::mint(amount, &wrapper.mint_cap);
  }

}

官网例子,和上面的差不多。

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

    const ENotPublisher: u64 = 0;   // Error codes.
    const ENotOwner: u64 = 1;
    const ENotReceipt: u64 = 2;
    const ENotGrantor: u64 = 3;

    struct OwnerCapability has key, store {}

    struct AdminCapability has key, store {}

    // 限制 offer struct 传入的 T 需要有 key + store abilities (个人理解)
    struct Offer<T: key + store> has key, store {
        receipt: address,
        offer: T,
    }

    // 和上面 Capability 一样,初始化并向模块所有者发布 OwnerCapability
    public entry fun init(sender: signer) {
        assert!(signer::address_of(&sender) == @examples, ENotPublisher);
        move_to(&sender, OwnerCapability {})
    }

    // 将 capability 授予给 `to`,即 receipt,
    // return 一个 Offer struct with AdminCapability.
    public fun grant_role_with_capability(to: address, _cap: &OwnerCapability): Offer<AdminCapability> {
        Offer<AdminCapability> {
            receipt: to,
            offer: AdminCapability {},
        }
    }

    /// The owner can grant the admin role to another address `to`
    // owner 可以将 AdminCapability (admin role) 授予另一个地址`to`
    public entry fun grant_role_offer(sender: &signer, to: address) acquires OwnerCapability {
        assert!(exists<OwnerCapability>(signer::address_of(sender)), ENotOwner);
        // 1. 签名者把自己的 OwnerCapability 揪出来
        let cap = borrow_global<OwnerCapability>(signer::address_of(sender));
        // 2. 将 OwnerCapability 放入,取回一个 AdminCapability 能力类型的 offer
        let offer = grant_role_with_capability(to, cap);
        // 3. 将 offer ( AdminCapability能力) 塞入自己的地址(move_to )
        move_to<Offer<AdminCapability>>(sender, offer);
    }

    /// Entry function for `receiptor` to accept an offer from `grantor` and save it in their account.  
    // `receiptor` 接受来自 `grantor` 的 offer 并将其保存在他们的帐户中
    public entry fun accept_role(receiptor: &signer, grantor: address) acquires Offer {
        assert!(exists<Offer<AdminCapability>>(grantor), ENotGrantor);
        let Offer<AdminCapability> { receipt, offer: admin_cap } = move_from<Offer<AdminCapability>>(grantor); // 移出
        assert!(receipt == signer::address_of(receiptor), ENotReceipt);
        move_to<AdminCapability>(receiptor, admin_cap);
    }
}

Wrapper 模式

https://move-by-example.com/core-move/patterns/wrapper.html#wrapper

如上 Offer 类型,其局限是某个地址下只能存储一个资源,无法分发给很多人,所以实际中更常用的是 Wrapper 模式。

The Wrapper pattern is used to store an arbitrary number of a given type, or store objects defined in other modules.

Wrapper 模式用于存储任意数量的给定类型,或存储其他模块中定义的对象

Why this pattern?

  1. Limited by that an account can only have one resource of a given type.

    1. 一个账户只能拥有一个给定类型的资源。

  2. Limited by that one can't move_to or move_from a resource outside the module.

    1. 不能 move_tomove_from 模块外的资源,使用了 Wrapper 后,可以再包一层,这样就能在 module 之外操作别人的 Resource 了.......

How to use?

  1. Use a vector or a table to store the objects.

    1. 使用 vector 或者 table 数据结构来存储多个 Object,来分发给多个地址。

  2. Wrapper an object in a new struct directly.

Applications:

  • NFTGalley::NFTGalley in StarCoin framework .

  • token::TokenStore in Aptos framework.

如下示例:

  • offers: Table<address, T> : offers 里可以存放多个 address ,也就是说: 可以把比如 AdminCapability 对象分发给多个地址。

  • 重复调用 grant_admin_offer(to: address) , 将 address, AdminCapability {} 添加到 &offer_store.offers 里去

  • address 来 claim 调用 accept_role 申请 AdminCapability 时, 合约会判断这个 address 在不在 &store.offers 的名单里,如果在名单里,就 table::remove(&mut store.offers, to); 弹出一个 AdminCapability{} 给 claimer

module examples::wrapper {
    use std::signer;
    use extensions::table::{Self, Table};

    const ENotPublisher: u64 = 0;
    const ENotOwner: u64 = 1;
    const ENotReceipt: u64 = 2;
    const ENotGrantor: u64 = 3;
    const EOfferExisted: u64 = 4;

    struct OwnerCapability has key, store {}

    struct AdminCapability has key, store {}

    /// The wrapper pattern.
    struct OfferStore<phantom T> has key, store {
        offers: Table<address, T>,
    }

    public entry fun init(sender: signer) {
        assert!(signer::address_of(&sender) == @examples, ENotPublisher);
        move_to(&sender, OwnerCapability {})
    }

    /// The owner can grant the admin role to another address `to`
    public entry fun grant_admin_offer(sender: &signer, to: address) acquires OfferStore 
    {
        assert!(exists<OwnerCapability>(signer::address_of(sender)), ENotOwner);
        if (!exists<OfferStore<AdminCapability>>(signer::address_of(sender))) {
            move_to<OfferStore<AdminCapability>>(sender, OfferStore<AdminCapability> {
                offers: table::new(),
            });
        };

        let offer_store = borrow_global_mut<OfferStore<AdminCapability>>(signer::address_of(sender));
        assert!(!table::contains<address, AdminCapability>(&offer_store.offers, to), EOfferExisted);
        table::add(&mut offer_store.offers, to, AdminCapability {});
    }

    /// Entry function for `sender` to accept an offer from `grantor` and save it in their account.
    public entry fun accept_role(sender: &signer, grantor: address)
    acquires OfferStore {
        assert!(exists<OfferStore<AdminCapability>>(grantor), ENotGrantor);
        let to = signer::address_of(sender);
        let store = borrow_global_mut<OfferStore<AdminCapability>>(grantor);
        assert!(table::contains<address, AdminCapability>(&store.offers, to), EOfferExisted);
        let admin_cap = table::remove(&mut store.offers, to);
        move_to<AdminCapability>(sender, admin_cap);
    }
}

Witness 模式

Witness is a design pattern used to prove that the a resource or type in question , A , can be initiated only once after the ephemeral witness resource has been consumed. Witness 模式通常用于证明资源或类型 A 只能在短命的 Witness 资源被消耗后初始化一次。 witness 资源必须在使用后立即被 drop,确保它不能被重复用于创建 A 的多个实例。

举个栗子 : 为了确保 Coin 只能发行一次, 不能重复发行多次, 我们修建了一个铸币法阵, 这个法阵只能用天选之子小明的血液为媒介才能开启 , 此时小明被祭司挑选为 Witness 见证人 , 某天, 祭司们需要开启法阵来铸币, 就把小明抓过来放血, 放完血之后, 短命的小明就被吃掉了(drop), 这个法阵从此以后就再也不能开启了, 因为唯一的血媒小明已经被吃掉了 ( 被 drop 了 ).


在下面的示例中,witness resource (小明) 是 PEACE,而我们要控制实例化的类型 A 是 Guardian。 (Guardian 只能被初始化一次 )

witness 资源类型必须有 drop 关键字,以便此资源被传参后可以被删除。我们看到 PEACE 资源的实例被传递到 create_guardian 方法并被删除(注意 witness 之前的下划线),确保只能创建一个 Guardian 实例。

/// Module that defines a generic type `Guardian<T>` which can only be
/// instantiated with a witness.
module witness::peace {
    use sui::object::{Self, UID};
    use sui::transfer;
    use sui::tx_context::{Self, TxContext};

    /// Phantom parameter T can only be initialized in the `create_guardian`
    /// function. But the types passed here must have `drop`.
    struct Guardian<phantom T: drop> has key, store {
        id: UID
    }

    /// This type is the witness resource and is intended to be used only once.
    struct PEACE has drop {}

    /// The first argument of this function is an actual instance of the
    /// type T with `drop` ability. It is dropped as soon as received.
    public fun create_guardian<T: drop>(
        _witness: T, ctx: &mut TxContext
    ): Guardian<T> {
        Guardian { id: object::new(ctx) }
    }

    /// Module initializer is the best way to ensure that the
    /// code is called only once. With `Witness` pattern it is
    /// often the best practice.
    fun init(witness: PEACE, ctx: &mut TxContext) {
        transfer::transfer(
            create_guardian(witness, ctx),
            tx_context::sender(ctx)
        )
    }
}

phantom

在上面的例子中,Guardian 具有 key 和 store ability,这样它就是一种资源 asset,可转移(transferrable) 并可以persists in global storage. (持久化在区块链全局存储中)

我们还想把 Witness resource PEACE 传入Guardian,但是 PEACE 只有 drop ability 。回想一下我们之前关于能力约束和内部类型的讨论,规则暗示 PEACE 也应该有 key 和 value ,因为外部类型 Guardian 有。但在这种情况下,我们不想为我们的 Witness 类型添加不必要的能力,因为这样做可能会导致不良行为和漏洞。

我们可以使用关键字 phantom 来绕过这种情况。当一个类型参数要么没有在结构体定义内部使用,要么只用作另一个幻像类型参数的参数时,我们可以使用幻像关键字来请求 Move 类型系统放宽对内部类型的能力约束规则。我们看到 Guardian 没有在其任何字段中使用 T 类型,因此我们可以安全地将 T 声明为幻像类型。

One Time Witness

One Time Witness (OTW) 是 Witness 模式的一个子模式,我们利用模块 init 函数来确保只创建一个 witness 资源实例(因此类型 A 保证是单例)。

在 Sui Move 中,如果一个类型的定义具有以下属性,则该类型被认为是一个 OTW:

  • 类型以模块命名,但大写

    • module witness::peace

    • struct PEACE

  • 该类型只有 drop 能力

要获得此类型 (PEACE)的实例,您需要将其作为第一个参数添加到 Module 的  init 函数,如上例所示。 The Sui runtime will then generate the OTW struct automatically at module publish time.

Sui runtime 将在 module 发布时自动生成 OTW 结构。

https://move-by-example.com/core-move/patterns/witness.html#witness)

Witness is a pattern that is used for confirming the ownership of a type. To do so, one passes a drop instance of a type. Coin relies on this implementation. Witness 是一种用于确认类型所有权(the ownership of a type)的模式。 为此,需要传递一个类型的 drop 实例。 Coin 依赖于这个实现。

Why this pattern?

  • Benifit from the Move type system, that a type can only be created by the module that defines it.

  • 类型 type 只能由定义它的模块 module 创建。

  • 得益于 Move 的特性 —— 一个类型实例化的时候,只能在定义这个类型的 Module 里面进行实例化。

  • Witness 对象一定是创建对象的合约或者是授权的合约才能获取这个实例。

How to use?

  • Define a public function with generic type argument, and a function argument of that type.

  • 定义具有泛型类型参数的公共函数,以及该类型的函数参数。

Security programming

  • No copy ability and public constructor function for the Witness type.

  • Witness 类型没有 copy 能力和 public 构造函数。

Applications

  • A third party library can provide public functions but limit the invoking.

  • 第三方库可以提供公共功能但限制调用。

  • In an open game, a hero module authorization other modules to increase a hero's expericence.

  • 在一个开放游戏中,一个英雄模块授权(某个可信任可约)其他模块来增加一个英雄的经验。

如下例子,Hacker 想要攻击这个合约 :

  1. Hacker 知道 Publisher 还没来得及 publish_coin ,黑客想利用这个时间差抢先发币:

  2. Hacker 想利用 examples::frameworkpublish_coin()examples::xcoinX 类型来发行货币

  3. 但是得益于 Move 的特性 —— 类型实例化的时候,只能在定义这个类型的 Module 里面进行实例化。

    1. 也就是说,publish() 中的类型 T 在 Module examples::xcoin 中被实例化为 X,其只能在 examples Module 中被实例化(应该是), 所以外部 Module 无法调用 X 进行实例化

  4. publish_coinpublish_coin_v2 传值/传引用,可以达到相同的效果。

module examples::framework {
    /// Phantom parameter T can only be initialized in the `create_guardian`
    /// function. But the types passed here must have `drop`.
    // Phantom param `T` 只能在 `create_guardian()` 中初始化, 这里传递的类型必须有 `drop`。
    struct Coin<phantom T: drop> has key, store {
        value: u128,
    }

    /// The first argument of this function is an actual instance of the
    /// type T with `drop` ability. It is dropped as soon as received.
    // 该函数的第一个参数是具有“drop”能力的类型 T 的实际实例。 它一收到就被丢弃。
    public fun publish_coin<T: drop>(_witness: T) {
        // register this coin to the registry table
    }

    public fun publish_coin_v2<T: drop>(_witness: &T) {
        // register this coin to the registry table, it's also work well.
    }

}

/// Custom module that makes use of the `guardian`.
module examples::xcoin {
    use examples::framework;    // Use the `guardian` as a dependency.

    struct X has drop {}

    /// Only this module defined X can call framework::publish_coin<X>
    // 只有这个模块定义的 X 可以调用 framework::publish_coin<X>
    public fun publish() {
        framework::publish_coin<X>(X {});
    }
}

// Hack it !
module hacker::hacker {
    use examples::framework;    // Use the `guardian` as a dependency.
    use examples::xcoin::X;
    public fun publish() {
      coin::publish_coin<X>( X { } ); // Illegal, X can not be constructed here.
    }
}

Hot Potato 模式

烫手山芋,笑死 🤣

  • Hot Potato is a name for a struct that has no abilities, hence it can only be packed and unpacked in its module.

  • struct with no abilities, 它只能在其 module 中打包和解包。

  • In this struct, you must call function B after function A in the case where function A returns a potato and function B consumes it.

  • 在这个结构中,你必须在函数 A 之后调用函数 B,以防 A 返回一个 potato 而 B 消费它。

该 Design Pattern 实现的逻辑:

  • 可以控制函数们的执行顺序:

  • 比如说我做了一个基础库,在不知道调用者会干什么的时候,通过 Hot Potato 模式,让别人在预先设定的顺序下去调用我现有的函数。

如下例子:

  • 定义了一个圆圆的土豆,它既不能直接吃,又不能存起来(马铃薯会发芽),只能传递给下一个人 (函数)。

  • 如果调用 get_potato 函数,该函数返回一个 Potato {} 对象,用户是没法处理的

  • 所以用户必须在其后调用 consume_potato 来消费这个 Potato {} 对象

  • 如此,就实现了 get_potato -> consume_potato 这样的一个函数执行顺序。

module examples::hot_potato {

    /// Without any capability, the `sender` can only call `consume_potato`.
    struct Potato {}

    /// When calling this function, the `sender` will receive a `Potato` object.
    /// The `sender` can do nothing with the `Potato` such as store, drop, except
    /// passing it to `consume_potato` function.
    public fun get_potato(_sender: &signer): Potato {
        Potato {}
    }

    public fun consume_potato(_sender: &signer, potato: Potato) {
        // do nothing
        let Potato {} = potato;
    }
}