【Sui】1つトランザクションで関数を1度だけしか呼ばないように制限する方法

Sam (Sui Moveの開発者)さんが、最近話題のSPAMのコントラクトに面白い関数の仕掛けがあるとXで投稿していたので、それについて書いていきます。
その前に。Don't Trust. Verify
これはクリプトに投資する上でもっとも大切な言葉だと思っています。
世にあるmemeトークンやNFT、スマートコントラクトを読まずに大事なお金を使うのはとてもリスクが高いため、できるだけコントラクトを読むようにしています。わからない点は自分でコードを書いて検証していきたいです。
SPAM の increment_user_counter
Samさん曰く、 increment_user_counter
はPTBで不正に(?)何回も呼べないようにうまく制限しているらしいです。確かに1回のトランザクションで何回もこの関数呼べてしまったら、spamの意味ないですからね😇
spam
のコードには面白い仕掛けがある:https://github.com/juzybits/polymedia-spam/blob/main/src/sui/sources/spam.move#L88...
increment_user_counterは、1つのPTBにつき最大1回の呼び出しを保証するために、意図的に*反*構成的である。 通常、このような関数は
&mut UserCounterを受け取り、直接インクリメントします。これにより、同じPTB内で
increment_user_counterを複数回呼び出したり、ループ内でこのコードを呼び出す Move を書くことができます。しかし、このコードは
UserCounterの所有権を取得し、それをインクリメントしてから送信者に戻すことで、それを巧みに防いでいる。なかなかいい感じだ。https://move-book.com/programmability/index.html! 関連する考え:
increment_user_counterは1回/PTBだけ呼び出すことができるが、同じPTBは他の関数を自由に呼び出すことができる。increment_user_counter
への呼び出しを、いずれにせよ送信しようとしていたPTBに付加することによって)"パッシブ・スパミング "が流行することは間違いなく想像できる。

該当のコードはこちら
通常は以下のように書くと思うが、これだとPTBで何回も呼ばれてしまう
entry fun increment_user_counter(user_counter: &mut UserCounter) {
assert!(user_counter.epoch == ctx.epoch(), EWrongEpoch);
user_counter.tx_count = user_counter.tx_count + 1;
}

検証するため、以下のコードを書いてデプロイした
単純なカウンターのコントラクト
module onigiri::counter_test {
public struct Counter has key {
id: UID,
value: u64,
}
fun init(ctx: &mut TxContext) {
transfer::transfer(Counter {
id: object::new(ctx),
value: 0,
}, ctx.sender())
}
public entry fun increment_normal(counter: &mut Counter) {
counter.value = counter.value + 1;
}
public entry fun increment_once(mut counter: Counter, ctx: &TxContext) {
counter.value = counter.value + 1;
transfer::transfer(counter, ctx.sender());
}
}

increment_normal
はPTBで何回も呼ぶことができた
(これがspamでできたら最高だった😂)
sui client ptb \
--assign counter @0x6888c38926f171d8a3e40d5bf8c3ec591e9dd3b844c0cb5d8f36fea4319e1b13 \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_normal counter \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_normal counter \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_normal counter \
--gas-budget 50000000
実際のトランザクション

一方、 increment_once
はPTBで複数回呼ぼうとすると、エラーが発生した
エラー内容
PTB execution failed due to CommandArgumentError { arg_idx: 0, kind: InvalidValueUsage } in command 1. Transaction digest is: GGuoevC7qyTVKnqx1t4aknZmLjYhzvVkm5saH6J6KryB
sui client ptb \
--assign counter @0x6888c38926f171d8a3e40d5bf8c3ec591e9dd3b844c0cb5d8f36fea4319e1b13 \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_once counter \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_once counter \
--move-call 0x1fff040c25ceb22513e93339f39bacacf66a1d35dcfe86bc28f2517306bb8694::counter_test::increment_once counter \
--gas-budget 50000000
[warn] Client/Server api version mismatch, client api version : 1.24.0, server api version : 1.24.1
PTB execution failed due to CommandArgumentError { arg_idx: 0, kind: InvalidValueUsage } in command 1. Transaction digest is: GGuoevC7qyTVKnqx1t4aknZmLjYhzvVkm5saH6J6KryB
実際のトランザクション

両関数の違い
increment_normal
は counter: &mut Counter
を受け取り、値を増やしているのに対し、
increment_once
は mut counter: Counter
を受け取り、値を増やした上で、オブジェクトを送信者に送信している
疑問点
まだMoveを学習し始めたばかりなのでよくわからない点が2つ。
-
関数定義で
mut counter: Counter
という引数の定義の仕方は初見だったため少し驚いた。いまだにこの部分の意味がよくわかっていないのは、Move, Rustの基礎知識が抜けているから。 -
オブジェクトを送信者に戻すことでPTBで複数回呼ばれることを防いでいるようだが、なぜこれで防げるのかがわかっていない

お布施大歓迎です💧
$SUI でも $SPAM でも PRIME MACHIN でも😍
0x26d6693f212015b60492c165f45d46429baeb5cd366f85a2419d2eeab0b76518

疑問点1について
引数の mut counter: Counter
は、 Move 2024 から導入されたものだった。
mutキーワードはタプルのデストラクチャリングや関数の引数で、ミュータブル変数を宣言するために使われます。
mut キーワードをつけないと immutable になり、変数への変更ができなくなる。
だが、
counter: &mut Counter
と mut counter: Counter
の違いはなんだろうか?

Rustの束縛と参照と可変性がわかっていないことが原因だった。Rustをすっ飛ばしてMoveを学ぶのは少しミス😇