계정 등록
이전 튜토리얼에서 토큰의 초기 순환 공급을 발행하는 방법과 이벤트 표준에 따라 이벤트를 기록하는 방법을 살펴보았습니다. 그런 다음 컨트랙트를 배포하고 지갑 잔고에서 FT를 확인했습니다. 이 튜토리얼에서는 스토리지 관리 표준에 대해 알아보고, FT 컨트랙트에 계정을 등록하여 악의적인 사람이 컨트랙트에서 모든 자금을 고갈시키는 것을 방지하는 방법에 대해 알아봅니다.
소개
새로운 사람이 대체 가능한 토큰(FT)을 받을 때마다 컨트랙트의 accounts
조회 맵에 추가됩니다. 이렇게 하면 컨트랙트에 바이트가 추가됩니다. 모든 사용자가 무료로 FT를 받을 수 있도록 만들면, 해당 시스템이 쉽게 악용될 수 있습니다. 사용자는 기본적으로 소량의 FT를 많은 계정에 전송하여, 모든 컨트랙트의 자금을 "탈취"할 수 있습니다. 이러한 이유로, 사용자가 저장하는 정보와 컨트랙트에 사용하는 바이트에 대해 요금을 청구할 수 있습니다. 그러나 사용자에게 청구하는 이 방법은 모든 컨트랙트에서 작동하도록 표준화되어야 합니다.
스토리지 관리 표준 입력
스토리지 관리 표준
The storage management standard is a set of rules that govern how a contract should charge users for storage. It outlines functions and behaviors such that all contracts implementing the standard are interoperable with each other. The 3 functions you'll need to implement are:
storage_deposit
- 사용자가 일정량의 스토리지를 컨트랙트에 예치할 수 있습니다. 사용자가 초과된 금액을 예지하면, 초과한 $NEAR는 환불됩니다.storage_balance_of
- 주어진 사용자가 지불한 스토리지를 쿼리합니다.storage_balance_bounds
- 주어진 컨트랙트와 상호 작용하는 데 필요한 최소 및 최대 스토리지 양을 쿼리합니다.
With these functions outlined, you could make a reasonable assumption that the flow would be:
- 사용자가 컨트랙트에 존재하지 않는 경우, 사용하는 바이트를 충당하기 위해 일부 스토리지를 예치해야 합니다.
- 사용자가
storage_deposit
함수를 통해 $NEAR를 입금하면, 컨트랙트와 자유롭게 상호 작용할 수 있습니다.
You might be asking yourself what the deposit amount should be. There are two ways you can go about getting this information:
- 모든 개별 사용자가
storage_deposit
함수에서 차지하는 바이트를 맵에 삽입하고, 바이트를 측정한 다음, 나중에accounts
맵에서 제거하여 동적으로 계산합니다. - 컨트랙트를 초기화할 때, 가능한 한 가장 큰 계정 ID를 삽입하기 위한 바이트를 계산하고, 모든 사용자에게 동일한 금액을 청구합니다.
For the purpose of simplicity, we'll assume the second method.
컨트랙트 수정
This "bytes for longest account ID" should be stored in the contract's state such that we can pull the value during the storage_deposit
function and ensure the user attaches enough $NEAR. Open the src/lib.rs
file and add the following code to the Contract
struct. If you're just joining us now, you can find the skeleton code for this tutorial in the 3.initial-supply
folder.
Loading...
You'll now need a way to calculate this amount which will be done in the initialization function. Move to the src/internal.rs
file and add the following private function measure_bytes_for_longest_account_id
which will add the longest possible account ID and remove it while measuring how many bytes the operation took. It will then set the bytes_for_longest_account_id
field to the result.
Loading...
You'll also want to create a function for registering an account after they've paid for storage. To do this, you can simply insert them into the accounts
map with a balance of 0. This way, you know that any account currently in the map is considered "registered" and have paid for storage. Any account that attempts to receive FTs must be in the map with a balance of 0 or greater. If they aren't, the contract should throw.
Loading...
Let's also create a function to panic with a custom message if the user doesn't exist yet.
Loading...
Now when you call the internal_deposit
function, rather than defaulting the user's balance to 0
if they don't exist yet via:
let balance = self.accounts.get(&account_id).unwrap_or(0);
You can replace it with the following:
Loading...
With this finished, your internal.rs
should look as follows:
use near_sdk::{require};
use crate::*;
impl Contract {
pub(crate) fn internal_unwrap_balance_of(&self, account_id: &AccountId) -> Balance {
...
}
pub(crate) fn internal_deposit(&mut self, account_id: &AccountId, amount: Balance) {
...
}
pub(crate) fn internal_register_account(&mut self, account_id: &AccountId) {
...
}
pub(crate) fn measure_bytes_for_longest_account_id(&mut self) {
...
}
}
There's only one problem you need to solve with this. When initializing the contract, the implementation will throw. This is because you call internal_deposit
and the owner doesn't have a balance yet. To fix this, let's modify the initialization function to register the owner before depositing the FTs in their account. In addition, you should measure the bytes for the registration in this function.
Loading...
스토리지 표준 구현
With this finished, you're now ready to implement the storage management standard. If you remember, the three functions you'll be implementing, we can break each down into their core functionality and decide how to proceed.
storage_balance_bounds
- 주어진 컨트랙트와 상호 작용하는 데 필요한 최소 및 최대 스토리지 양을 쿼리합니다.
Since you're creating a fungible token contract and the storage price won't change (unless the $NEAR
cost per byte changes), the minimum and maximum storage costs should be the same. These values should be equal to the amount of bytes for the longest account ID you calculated in the measure_bytes_for_longest_account_id
function multiplied by the current $NEAR
price per byte. Switch to the src/storage.rs
file to get started.
Loading...
storage_balance_of
- 지정된 사용자가 지불한 스토리지에 대한 쿼리입니다.
As we mentioned earlier, you can tell if somebody has paid for storage by checking if they're in the accounts
map. If they are, you know that they've paid the amount returned by storage_balance_bounds
.
Loading...
storage_deposit
- 사용자가 일정량의 스토리지를 컨트랙트에 예치할 수 있습니다. 사용자가 초과된 보증금을 예치하면, 초과 $NEAR 액수에 대해 환불을 진행합니다
In order to implement this logic, you first need to get the attached deposit. You'll also need to ensure that the user hasn't already paid for storage (i.e. they're in the accounts
map). If they are, simply refund the caller for the $NEAR they attached to the call.
If the user isn't registered yet, you should get the storage cost by calling storage_balance_bounds
and make sure they've attached enough to cover that amount. Once this if finished, you can register them and refund any excess $NEAR.
Loading...
With this finished, you're ready to build and deploy the contract!
컨트랙트 배포
Since the current contract you have is already initialized, let's create a sub-account and deploy to again.
하위 계정(sub-account) 생성
Run the following command to create a sub-account storage
of your main account with an initial balance of 3 NEAR which will be transferred from the original to your new account.
near account create-account fund-myself storage.$FT_CONTRACT_ID '3 NEAR' autogenerate-new-keypair save-to-legacy-keychain sign-as $FT_CONTRACT_ID network-config testnet sign-with-legacy-keychain send
Next, you'll want to export an environment variable for ease of development:
export STORAGE_FT_CONTRACT_ID=storage.$FT_CONTRACT_ID
Build the contract as you did in the previous tutorials:
cd 3.initial-supply
cargo near build
Deploying and Initialization
It's time to deploy the contract, initialize it and mint the total supply. 다시 한 번 초기 공급량을 1000gtNEAR
로 만들어 봅시다.
cargo near deploy $STORAGE_FT_CONTRACT_ID with-init-call new_default_meta json-args '{"owner_id": "'$STORAGE_FT_CONTRACT_ID'", "total_supply": "1000000000000000000000000000"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-keychain send
이제 소유자가 지불한 스토리지를 쿼리하면 해당 스토리지가 등록된 것을 볼 수 있습니다!
near contract call-function as-read-only $STORAGE_FT_CONTRACT_ID storage_balance_of json-args '{"account_id": "'$STORAGE_FT_CONTRACT_ID'"}' network-config testnet now
이는 구조체를 반환해야 합니다. 총 금액은 대략 등록에 0.00125 $NEAR
가 들며, 사용자는 등록 비용을 지불하는 데 모두 사용했기 때문에 사용 가능한 $NEAR가 0입니다.
{ total: '1250000000000000000000', available: '0' }
컨트랙트와 상호 작용하는 데 필요한 스토리지 잔액을 쿼리할 수도 있습니다.
near contract call-function as-read-only $STORAGE_FT_CONTRACT_ID storage_balance_bounds json-args {} network-config testnet now
최소값이 최대값과 같은 위의 storage_balance_of
쿼리와 동일한 금액을 반환하는 것을 볼 수 있습니다.
{ min: '1250000000000000000000', max: '1250000000000000000000' }
결론
오늘 당신은 컨트랙트에 사용자를 등록하기 위한 로직을 살펴보고 만들었습니다. 이 로직은 스토리지 관리 표준을 준수하며 FT 컨트랙트를 작성할 때 요구 사항을 충족하도록 커스터마이징됩니다. 그런 다음 이러한 변경 사항을 빌드, 배포 및 테스트했습니다. 다음 튜토리얼에서는 FT를 다른 사용자에게 전송하는 방법에 대한 기본 사항을 살펴봅니다.
At the time of this writing, this example works with the following versions:
- rustc:
1.77.1
- near-sdk-rs:
5.1.0
(with enabledlegacy
feature) - cargo-near:
0.6.1
- near-cli-rs:
0.11.0