MultiToken合约
名词解释
Lock white list
代币锁定白名单
如果某个Token在创建的时候,将某个系统合约加入Lock white list,就意味着这个系统合约能够在一定条件下通过Lock方法在MultiToken合约中锁定用户的代币。
Transfer Callback
发生转账后的回调
这个信息写在TokenInfo的external_info字段(这是一个字典)中,一共有三种类型,每种类型的key不一样:
每次调用
Transfer和TransferFrom后执行的回调,key为aelf_transfer_callback每次调用
Lock后执行的回调,key为aelf_lock_callback每次调用
Unlock后执行的回调,key为aelf_unlock_callback
value都可以解析为CallbackInfo类型,也就是指定在完成相应操作后,可以针对指定合约的指定方法发送一个inline交易:
代币相关
创建 - Create
关于代币的信息,关注TokenInfo这一个结构:
message TokenInfo {
// The symbol of the token.f
string symbol = 1;
// The full name of the token.
string token_name = 2;
// The current supply of the token.
int64 supply = 3;
// The total supply of the token.
int64 total_supply = 4;
// The precision of the token.
int32 decimals = 5;
// The address that created the token.
aelf.Address issuer = 6;
// A flag indicating if this token is burnable.
bool is_burnable = 7;
// The chain id of the token.
int32 issue_chain_id = 8;
// The amount of issued tokens.
int64 issued = 9;
// The external information of the token.
ExternalInfo external_info = 10;
}symbol
Token标识
"ELF"
在MultiToken合约中,symbol会作为Token的唯一标识
token_name
Token名
"elf token"
supply
Token当前供应量
< 1, 000, 000, 000
也就是已经通过Issue方法发行的数量
total_supply
Token总供应量
1, 000, 000, 000
ELF共计发行10亿枚
decimals
Token的小数点精确位数
8
如果decimals为2,意味着合约内的100实际上代表1个;ELF的decimals为8,意味着合约内读取到的100000000代表一个ELF。
issuer
发行者
经济系统(Economic)合约地址
实际上,ELF会在创世区块中由Economic合约完成发行,Issue方法的直接调用者是Economic合约(具体而言是通过IssueNativeToken方法)。
is_burnable
是否能够被销毁
true
ELF是能够被销毁的。
issue_chain_id
代币能够在aelf的哪一条链上进行发行
9992731(主链ChainId)
ELF只能在主链发行(并且会在一开始完成发行)。
issued
Token发行量
1, 000, 000, 000
external_info
Token额外信息
-
可以理解为NFT的Metadata,当以上属性无法涵盖一类Token的所包含的信息的时候,就需要使用external_info这一个属性来辅助存储。实际上,这个属性的类型是一个map。这个字段可以通过ResetExternalInfo方法修改(权限在Issuer那里)
任何aelf账户在手续费充足的情况下,都可以创建symbol满足要求的新的代币类型。创建代币本身,就是在MultiToken的State.TokenInfos中添加一个TokenInfo实例,其key为symbol。当前对symbol的规则如下:
如果创建者账户在白名单(
State.CreateTokenWhiteListMap)中,可以创建长度不限的包含字母A-Z和数字0-9的symbol;如果创建者账户不在白名单中,可以创建长度10以内的包含字母A-Z的symbol。
发行 - Issue
在创建代币的时候,可以指定Issuer(见上面的表格)。被指定为Issuer的账户可以对该代币做初始化的发行。随着代币的发行,TokenInfo的supply和issued会增加,随后从State.Balances中改变代币接收者对于该代币的余额。顺便,Issuer是可以使用ChangeTokenIssuer方法更换的。
转账 - Transfer & TransferFrom
Transfer和TransferFrom方法都会导致State.Balances中两个账户的余额的更新,区别在于TransferFrom的入参多一个From参数,标识这一次转账,余额被减少的一方账户是哪个——Tranfer交易中,这个账户永远是交易发起者(Context.Sender)。TransferFrom执行成功需要一个前提:From账户需要对TransferFrom交易的发起人进行额度的授权(Approve)。
销毁 - Burn
如果TokenInfo中的isBurnable字段为true,那这类Token就可以被销毁掉。当指定数量的代币被销毁时,会减少TokenInfo的Supply字段。
授权和取消授权 - Approve & UnApprove
在区块链中,为了减少账户频繁的Transfer操作,允许一个账户A对另一个账户B进行指定代币的额度上的预授权:此后,B可以随时通过TransferFrom方法从A账户中转走一部分指定代币,直到达到授权的额度上限。授权额度会记录在State.Allowances中。UnApprove方法用于取消授权。
锁仓和解锁 - Lock & Unlock
这两个方法只是为其他系统合约(AssertSystemContractOrLockWhiteListAddress判断)实现锁定用户代币提供方便的:当合约试图锁定用户的一笔代币时,只需要跨合约调用MultiToken合约的Lock方法即可;之后可以调用Unlock方法把用户的代币返还给用户。MultiToken合约实际上会为每一笔锁仓分配一个虚拟地址(引入LockId算出fromVirtualAddress -> virtualAddress),避免不同用户锁仓的资金被混起来。
跨链转账
在aelf主网中,Token只可以在主网中进行创建。创建完成后,TokenInfo并不会自动同步到各个侧链上,只能去手动完成同步:
首先在主网上通过
ValidateTokenInfoExists方法构造一个证明,这个证明会随着主侧链的Indexing机制,自动把Merkle Root从主链同步到侧链上。然后在侧链上,通过
CrossChainCreateToken方法,验证某个TokenInfo的确存在于主链之上,就可以将这个TokenInfo信息添加进侧链的MultiToken合约上。
将主链的TokenInfo信息同步到侧链以后,主侧链之间、兄弟侧链之间就可以完成跨链转账了(通过CrossChainTransfer方法和CrossChainReceiveToken方法)。具体的过程见aelf跨链机制和侧链 。
ACS1:手续费相关
在收费过程中,涉及ChargeTransactionFees和ClaimTransactionFees两个方法,这两个方法对应的交易都是自动生成的。ChargeTransactionFees通过交易执行Pre-plugin生成,ClaimTransactionFees通过系统交易生成器生成。详见ACS1 Method Fees收取过程 。除了ChargeTransactionFees和ClaimTransactionFees,与收费逻辑相关的方法还有:
InitialCoefficients:初始化计算费用的公式的参数
SetSymbolsToPayTxSizeFee:设置能用于交手续费的token list
UpdateCoefficientsForSender:更新ACS1的size fee计算公式的参数
IPreExecutionPlugin接口定义中有一个方法IsStopExecuting,也是用于判断如果Pre-plugin交易执行失败了,是否应该停止原交易的执行。ACS5(见下面)可以直接让Pre-plugin抛出AssertionException,会导致交易直接执行失败,就没有把精力放在实现这个接口中。但是ACS1不同,ACS1的Pre-plugin生成的ChargeTransactionFees交易是要执行成功的,如果一个交易到了打包的时候才执行失败(就是过了预验证),依然会将它打包,并扣掉手续费,只不过在扣掉手续费的同时,不执行原交易了而已。ACS1和ACS5当然可以混用,且ACS1会先执行(因为ExecutionPluginForMethodFeeModule早于ExecutionPluginForCallThresholdModule注册),在执行ACS5之前先把交易费给收了。如果某个交易被判定为被Pre-plugin交易阻止了后续执行,那依然会保留其执行结果,修改StateDb。在这个场景下,就是会坚持完成ACS1手续费的收取。(见PlainTransactionExecutingService.GetTransactionResult,在满足IsExecutionStoppedByPrePlugin的情况下,会把事件放进交易结果的Logs里并更新布隆过滤器。)
ACS5:合约方法调用阈值相关
实现了ACS5的合约,可以针对这个合约中的每个方法设置一个调用门槛,比如要求调用者的X代币余额必须不小于Y(或者要求调用者X的代币余额和对合约的授权额度都必须不小于Y)。ACS5中定义了两个方法:
SetMethodCallingThreshold,用于设置合约某个方法调用门槛
GetMethodCallingThreshold:用于获取合约某个方法设置的调用门槛
另外定义了一个MethodCallingThreshold结构:
当任意其他合约实现了ACS5标准的时候,在执行该合约的方法时,会通过MethodCallingThresholdPreExecutionPlugin.GetPreTransactionsAsync生成一个Pre-plugin交易(跟ACS1的Pre-plugin机制一样)。这个交易的信息为:
From:原交易的Sender的地址
To:MultiToken合约的地址
MethodName:CheckThreshold
Params:一个CheckThresholdInput实例:
sender:要验谁的账户
symbol_to_threshold:token类型和这个token对应调用门槛额度的map,只要用户符合其中一个门槛,就算通过
is_check_allowance:除了检查账户代币余额,还要不要检查该账户对要执行的合约的授权额度(见Approve / UnApprove方法)
CheckThreshold就是MultiToken合约实际上用来检验合约调用门槛的方法。
这个方法入参中的SymbolToThreshold,是在生成CheckThreshold交易的时候,在MethodCallingThresholdPreExecutionPlugin.GetPreTransactionsAsync中读取要执行的合约的方法获得的。
根据SymbolToThreshold逐一判断满足余额要求的代币列表,记为meetBalanceSymbolList;
再从meetBalanceSymbolList中逐一判断满足授权额度满足要求的代币列表,如果其中有任何一个满足(或者不需要检查授权额度),就把meetThreshold设置为true;
如果meetThreshold不为true,就抛出AssertionException,从而让原交易一起失败。
ACS8:资源费用相关
实现了ACS8的合约(其实不能称之为实现,只要某个合约的proto文件中声明自己的base有acs8.proto即可),每一个方法的执行,都会扣这个合约地址的资源币。这个场景下可能被扣的资源币有:WRITE,READ,STORAGE,TRAFFIC。由于扣费的依据是原交易实际执行过程中消耗的资源(比如读取了几个key,写入了几个key等等),因此ACS8机制的核心逻辑是在Post-plugin交易中:在原交易执行完毕后,会生成一笔交易,在所有的inline交易执行完毕后执行。由于Pre-plugin和Post-plugin的机制都十分相似,ACS1和ACS5的文档中已经做了很多介绍,这一节只简单指出ACS8相关的代码位置。
Post-plugin交易:ChargeResourceToken
交易生成:ResourceConsumptionPostExecutionPlugin.GetPostTransactionsAsync交易参数:
From:原交易要调用的合约的地址
To:MultiToken合约地址
MethodName:ChargeResourceToken
Params:一个ChargeResourceTokenInput实例:
cost_dic:根据原交易实际执行过程中消耗的资源计算出来的各个代币的费用
caller:原交易的Sender
其中,cost_dic的值,跟ACS1机制生成ChargeTransactionFees交易时获得SymbolsToPayTxSizeFee字段的值的逻辑类似,涉及计算公式的设置、更新和链下计算。最终通过ResourceTokenFeeService.CalculateFeeAsync方法计算得出费用。ChargeResourceToken方法本身的实现就比较简单,根据input生成一个账单bill,遍历账单信息,抛出ResourceTokenCharged事件即可。这个ResourceTokenCharged事件会在ResourceTokenChargedLogEventProcessor.ProcessAsync中处理,把一个TotalResourceTokensMaps实例的数据通过TotalResourceTokensMapsProvider.SetTotalResourceTokensMapsAsync方法放进ExecutedData。
实际完成手续费收取的交易:DonateResourceToken
在下一个区块生产过程中,BP会通过DonateResourceTransactionGenerator.GenerateTransactionsAsync生成一个DonateResourceToken交易。从TotalResourceTokensMapsProvider.GetTotalResourceTokensMapsAsync方法获得totalResourceTokensMaps实例,作为DonateResourceToken交易的入参。其他节点在收到区块后,使用DonateResourceTokenValidationProvider.ValidateBlockAfterExecuteAsync验证DonateResourceToken交易的执行结果是否与本地的资源币费用计算结果相符。在DonateResourceToken执行过程中,有可能会发现合约的资源币余额不足,此时会往State.OwningResourceToken中记录一下这个合约的欠款,同时抛出一个ResourceTokenOwned事件。合约存在欠款怎么办?以后不让它执行了就可以。这个逻辑是使用一个Pre-plugin交易实现的。
Pre-plugin交易:CheckResourceToken
在执行原交易之前,通过ResourceConsumptionPreExecutionPlugin.GetPreTransactionsAsync方法生成。这个交易只用于检查合约是否存在欠款。如果有欠款,会抛出一个AssertionException,那么这个合约的所有交易就都不能执行了。这就意味着合约的开发者(或运营者)需要及时给合约地址中垫付资源币。垫付资源币可以是直接把资源币打到合约地址中,但是这样的话,垫付的账户就无法取回自己垫付的资源币了。为了记录某个账户垫付了多少资源币,增加了AdvanceResourceToken和TakeResourceTokenBack方法。
垫付和拿回资源币:AdvanceResourceToken & TakeResourceTokenBack
AdvanceResourceToken用于垫付资源币,使用之前不需要Approve(毕竟MultiToken合约可以自己改State.Balances)。一共两个逻辑:基于要垫付的资源币的数量修改State.AdvancedResourceToken,然后完成DoTransfer,把资源币余额打过去。TakeResourceTokenBack中,基于State.AdvancedResourceToken查询Sender的垫付数据,然后把资源币返还给垫付账户(返还数量为input中指定数量),当然不可以多拿,否则会报错:Can't take back that more.。
Last updated