# 版本一 ## module-xtokens ### 修改跨链转账extrinsic call的流程 orml-trait 新增定义: ``` enum RestrictStatus { Pass, Prohibit, Delay(BlockNumber), } trait CallRestrict<AccountId, BlockNumber, Call> { fn check_restrict_status(who: &AccountId, call_type: Call) -> RestrictStatus; fn new_delay_call(who: &AccountId, call_type: Call, delay_block: BlockNumber) -> DispatchResult; fn pass_callback(who: &AccountId, call_type: Call); } trait DelayCallHandler<AccountId, BlockNumber, Call> { fn pre_delay(who: AccountId, call_type: Call) -> DispatchResult; fn pre_execute_delayed(who: &AccountId, call_type: Call) -> DispatchResult; fn execute_delayed(who: &AccountId, call_type: Call) -> DispatchResult; } ``` + `RestrictStatus::Pass`, 不做限制, 按原本流程继续执行 + `RestrictStatus::Prohibit`, 禁止该调用, 返回错误 + `RestrictStatus::Delay(BlockNumber)`, 延迟BlockNumber后执行 orml-xtokens 新增定义: ``` enum XtokensCall { TransferMultiAssets(params) TransferMultiAssetsWithFee(params) } ``` CallRestrict 由 module-restrict 实现。 orml-xtokens 需要进行restrict的call, 要在 enum XtokensCall 中进行注册, 并且修改call的执行流程, 用一套通用的流程包住原本的执行逻辑(注意不要影响到系统内部函数的调用), 以orml-xtokens 的 transfer_multi_assets call为例: ``` fn transfer_multiassets(origin, params) { let who = ensure_signed(origin)?; match restrict::check_restrict_status(who, XtokensCall::TransferMultiAssets(params)) { RestrictStatus::Pass => { // 执行原本的逻辑 xtokens::transfer(who, params)?; // 执行回调函数 restrict::pass_callback(who, XtokensCall::TransferMultiAssets(params)); }, RestrictStatus::Prohibit => { return Err; }, RestrictStatus::Delay(delay_block) => { restrict:: new_delay_call(who, XtokensCall::TransferMultiAssets(params), delay_block); } }; Ok(()) } ``` 另外会给 () 实现trait CallRestrict<XtokensCall>, 这要如果不集成 restrict 模块, 钩子挂 () 就行, orml-xtokens的 transfer_multiassets 的调用流程就和原来的一致: ``` impl CallRestrict<AccountId, BlockNumber, XtokensCall> for () { fn check_restrict_status(who: &AccountId, call_type: XtokensCall) -> RestrictStatus { // always pass RestrictStatus::Pass } fn new_delay_transfer(who: &AccountId, call_type: XtokensCall, delay_block: BlockNumber) -> DispatchResult { unimplement!() } fn pass_callback(who: &AccountId, call_type: XtokensCall) { // do nothing } } ``` ### 实现 DelayCallHandler orml-xtokens 也需要实现 trait DelayCallHandler : ``` impl DelayCallHandler<AccountId, BlockNumber, XtokensCall> for Xtokens{ fn pre_delay(who: &AccountId, call_type: XtokensCall) -> DispatchResult { // 获取需要transfer的资产,进行 reserve 处理 let assets: Vec<(CurrencyId, Balance)> = get_params(operation); for (currency_id, amount) in assets.iter() { Currencies::reserve(who, currency_id, amount)?; } } fn pre_execute_delayed(who: &AccountId, call_type: XtokensCall) -> DispatchResult { // 获取需要transfer的资产,进行 unreserve let assets: Vec<(CurrencyId, Balance)> = get_params(delayed_transfer); for (currency_id, amount) in assets.iter() { Currencies::unreserve(who, currency_id, amount)?; } } fn execute_delayed(who: &AccountId, call_type: XtokensCall) -> DispatchResult { match operation { TransferMultiAssets(params) => xtokens.transfer_multiassets(who, params), TransferMultiAssetsWithFee(params) => xtokens.transfer_multiassets_with_fee(who, params) } } } ``` ## module-restrict 新增pallet module-restrict(名称暂定) , 可以对某些pallet的call进行restrict判断,以及记录和触发执行delayed call。 ### 规则配置 module-restrict 新增对orml-xtokens 跨链转账的restrict规则 ``` struct XtokensTransferRestriction { // 如果配置为true, 始终返回 RestrictStatus::Prohibit prohibited: bool, // 如果有配置, 当xtoken transfer的资产大与 Balance 时, 返回 RestrictStatus::Delay(BlockNumber) large_transfer_delay: Option<(BlockNumber, Balance)>, // 如果有配置, BlockNumber 为触发因累积量达到上限的delay时间, Balance 为触发累积量延迟的数量 transfer_accumulation_delay: Option<(BlockNumber, Balance)>, // transfer累积量 transfer_cumulative_in_period: Balance } // option 1: 针对每条链的每个资产的跨链转账都有独立的规则和统计 XtokensTransferRestrictions: double_map Multilocation, CurrencyId => XtokensTransferRestriction // option 2: 针对每种资产的跨链转账,不区分目的地 XtokensTransferRestrictions: map CurrencyId => XtokensTransferRestriction ``` option1 和 option2 可以选择一种实现, 理论上也可以两者共存, 限制更加细化但也更复杂。 对于这些规则, 还有 extrinsic call 供 council/公投 调用: ``` fn update_xtokens_transfer_restriction(origin, ...) -> DispatchResult; ``` 按照option2, 给module-restrict实现trait CallRestrict: ``` impl CallRestrict<AccountId, BlockNumber, XtokensCall> for Restrict { fn check_restrict_status(who: &AccountId, call_type: XtokensCall) -> RestrictStatus { let (multilocation, assets): (MultiLocation, Vec<(CurrencyId, Balance)>) = get_params(operation); let mut prohibited: bool = false; let mut delay: Option<BlockNumber> = None; for (currency_id, amount) in assets { let restrictions = XtokensTransferRestrictions::get(currency_id); // 检查该资产是否已被禁止跨链 if restrictions.prohibited { pause = true; break; } // 检查该资产是否触发大额限制 if let Some(delay_block, large_transfer_delay) = restrictions.large_transfer_delay { if amount >= large_transfer_delay { // 使用delay更久的 delay = max(delay, delay_block); } } // 检查该资产是否触发累积量限制 if let Some(delay_block, transfer_accumulation_delay) = restrictions.transfer_accumulation_delay { if restrictions.transfer_cumulative_in_period + amount >= large_transfer_delay { // 使用delay更久的 delay = max(delay, delay_block); } } } match (prohibited, delay) { (true, _) => RestrictStatus::Prohibit, (false, None) => RestrictStatus::Pass, (false, Some(delay_block)) => RestrictStatus::Delay(delay_call_type:block), } } fn new_delay_call(who: &AccountId, call_type: XtokensCall, delay_block: BlockNumber) -> DispatchResult { push_delayed_call(who, delay_call_type, delay_block) } fn pass_callback(who: &AccountId, call_type: XtokensCall) { let assets = get_params(call_type).assets; for (currency_id, amount) in assets { XtokenTransferRestrictions::mutate(currency_id, |restrcition| { if restrcition.transfer_accumulation_delay.is_some() { *restrcition.transfer_cumulative_in_period += amount; } }); } } } ``` pallet restrict 还配置有 `Const XtokenTransferCumulativeResetPeriod: BlockNumber`, 每过这么多时间,将所有 XtokenTransferRestrict 配置中的 transfer_cumulative_in_period 重置为 0 ### delay-queue 定义三个存储, NextDelayIndex 和 DelayEndTime 用于维护 delay queue, DelayedXtokensTransfer 用于维护 xtokens delay transfer的调用内容。 ``` enum DelayCallType { Xtokens(XtokensCall) } NextDelayIndex: value DelayId; DelayEndTime: double_map BlockNumber, DelayId => () DelayedCalls: map DelayId => (AccountId, DelayCallType, BlockNumber); ``` 需要定义一些内部函数: ``` fn push_delayed_call(who: &AccountId, delay_call_type: DelayCallType, delay_block: BlockNumber) -> DispatchResult { // 先执行 Xtokens的pre_delay xtokens::pre_delay(who, call_type)?; // 获取 NextDelayIndex, 记录相应信息至 DelayEndTime, DelayedCalls } fn pop_delayed_call(delay_id: DelayId) -> DispatchResult { // 获取delayed call的信息 let (who, delay_call_type, _) = DelayedCalls::get(delay_id)?; // 先执行 Xtokens的 pre_execute_delayed xtokens::pre_execute_delayed(who, delay_call_type)?; // 再执行 Xtokens 的 execute_delayed xtokens::execute_delayed(who, delay_call_type)?; // 删除掉 delay_id 的相关存储 } fn on_initialized(n: BlockNumber) { // 遍历出当前已经到期的delay_id for delay_id in DelayEndTime::drain_prefix(n) { match pop_delayed_call(delay_id) { Ok(_) => // trigger event, Err(_) => // trigger event, } } } ``` 另外,存在 pop_delayed_call 失败的情况(比如用户reserved的资产被其他地方错误的多unreserve了,导致执行 pre_execute_delayed 时失败), 失败的 DelayedCalls 会留存, 可以通过cancel_delayed_call给用户进行操作 ``` fn cancel_delayed_call(origin, delay_id) -> DispatchResult { let caller = ensure_signed(origin)?; let (who, delay_call_type, expired_block) = DelayedCalls::take(delay_id)?; ensure!(who == caller); // 如果不允许用户主动取消未到delay时间的delayed call, 只能取消delay 执行失败的, 可以有如下检查: // ensure!(expired_block < System::current_block()); xtokens::pre_execute_delayed(who, delay_call_type)?; DelayEndTime::take(who, delay_id)?; } ``` ### 特殊权限 提供一些extrinsic call供 council/公投调用, 进行延迟/加速 delayed call ``` fn change_delayed_call(origin, delay_id: DelayId, when: DispatchTime) { // 验证权限, 修改DelayedCalls 和 DelayEndTime } ``` ## 额外需求(from Bryan) 1. 限制算法可以考虑用token bucket,就是每X block 增加 Y 限额,直到Z 上限, 直接配置数量改为配置公式和常量 (如果限制量不需要做到经常调整, 也可以不用这样, 还是等需要的是走council/公投去配置直接量) 2. 转账方法可能要有个参数让用户选择可以接受最多少delay,如果传0的话就是要么马上转账要么交易失败. (但这个需求需要改动xtokens 原来的call的定义来添加这个参数) 3. 我们要用方法可以暂停整个delay queue的处理。(可以做到,需要修改下sheduler机制, 现在是类似auction的简单schedule) ## 其他 1. apps,polkawallet等前端会有大的改动, subscan对delayed跨链转账的交易的展示不一定支持得很好。 # 版本2 版本1的设计,要改动限制模块的实现,和功能模块是高度耦合的,delay的实现也会复杂,还会涉及到用户资金的变动。 1. 不支持delay的限制, 当数量达到限制后, 直接禁止掉。 2. 不用需要orml-xtokens, module-restrict 不要delay相关的实现, 不挂钩子。 3. 还可以加入白名单功能, 对于白名单可以始终返回 Pass, 不做限制。 4. xtokens在通过transfer_multiassets 同事转账多个assets时, 如果其中一种资产触发Prohibit/Delay, 整笔交易都会Prohibit/Delay, 不能拆分, 也满足交易的原子性和符合交易意图。 ## module-restrict 配置简化为: ``` struct XtokensTransferRestriction { // Balance 为累积量上限 transfer_accumulation_period_limit: Option<Balance>, // transfer 累积量 transfer_cumulative_in_period: Balance } // option 1: 针对每条链的每个资产的跨链转账都有独立的规则和统计 XtokensTransferRestrictions: double_map Multilocation, CurrencyId => XtokensTransferRestriction // option 2: 针对每种资产的跨链转账,不区分目的地 XtokensTransferRestrictions: map CurrencyId => XtokensTransferRestriction ``` 同样的 option1 option2 可以取其一, 同时存在也可。 对call的限制判断通过实现 trait SignedExtension 来做到: ``` pub struct XtokenCallRestrict<T>(PhantomData<T>); impl SignedExtension for ChargeTransactionPayment<T> { // 在交易dispatch前做检查, 有错误的交易不会被打包, 前端在发送交易后就会得到错误提示 fn pre_dispatch(call: &CallOf<T>, ...) -> Result<Self::Pre, TransactionValidityError> { // 判断call是否属于orml-xtokens的跨链转账调用 match call { xtokens::Transfer | xtokens::TransferMultiAssets | xtokens::TransferMultiAssetsWithFee => { // 解析跨链转账的量 let (multilocation, assets): (MultiLocation, Vec<(CurrencyId, Balance)>) = get_params(call); for (currency_id, amount) in assets { let restrictions = XtokensTransferRestrictions::get(currency_id); // 检查该资产是否触发累积量限制 if let Some(limit) = restrictions.transfer_accumulation_period_limit { if restrictions.transfer_cumulative_in_period + amount >= limit { return TransactionValidityError; } } } }, _ => // do nothing, } } // 打包交易后执行 fn post_dispatch(pre: Option<Self::Pre>, result: &DispatchResult, ...) -> Result<(), TransactionValidityError> { // 判断call是否属于orml-xtokens的跨链转账调用 match call { xtokens::Transfer | xtokens::TransferMultiAssets | xtokens::TransferMultiAssetsWithFee => { // 判断 call 是否执行成功 if result.is_ok() { let assets = get_params(pre.call).assets; for (currency_id, amount) in assets { XtokenTransferRestrictions::mutate(currency_id, |restrcition| { if restrcition. restrictions.transfer_accumulation_period_limit.is_some() { // 累积成功transfer的数量 *restrcition.transfer_cumulative_in_period += amount; } }); } } }, _ => // do nothing, } } } ``` 这样如果达到上限, 交易不会被打包,发送给节点时就会收到报错信息。