# 版本一
## 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,
}
}
}
```
这样如果达到上限, 交易不会被打包,发送给节点时就会收到报错信息。