chainsafe / chainbridge-substrate Goto Github PK
View Code? Open in Web Editor NEWLicense: GNU Lesser General Public License v3.0
License: GNU Lesser General Public License v3.0
To be compatible with existing chains we should avoid using genesis to configure the bridge params. They can default to zero, as long a methods are provided to configure them as root.
Use map-set for storing data that we only care about the key. The Relayers are tracked based on their account ID, which will be the key for Storage Set.
This would have an added benefit for the underlying database implementation to optimize the storage size since we are storing less data.
Basically, we change the value from bool
type to ()
type which effectively makes the StorageMap
a Set.
From
/// Tracks current relayer set
#[pallet::storage]
#[pallet::getter(fn relayers)]
pub type Relayers<T: Config> =
StorageMap<_, Blake2_256, T::AccountId, bool, ValueQuery>;
To
/// Tracks current relayer set
#[pallet::storage]
#[pallet::getter(fn relayers)]
pub type Relayers<T: Config> =
StorageMap<_, Blake2_256, T::AccountId, (), OptionQuery>;
and is_relayer
function will just check if the Relayers storage contains the key of a certain AccountId.
fn is_relayer(who: &T::AccountId) -> bool {
Relayers::<T>::contains_key(who)
}
No changes needed to the existing tests
All existing test should pass as is.
We need an example pallet that can handler ERC721 tokens.
A runtime should build when this pallet is included. Issue relates to extra method required in EnsureOrigin
that is enabled when the runtime-benchmarks
feature is set.
Build fails with error
error[E0046]: not all trait items implemented, missing: `successful_origin`
--> /home/willem/.cargo/registry/src/github.com-1ecc6299db9ec823/frame-system-3.0.0/src/lib.rs:786:1
|
786 | / impl<
787 | | O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>,
788 | | AccountId,
789 | | > EnsureOrigin<O> for EnsureRoot<AccountId> {
... |
801 | | }
802 | | }
| |_^ missing `successful_origin` in implementation
|
= help: implement the missing item: `fn successful_origin() -> OuterOrigin { todo!() }`
It is much harder to handle any kind of error in Substrate than it is to do it on Ethereum smart contracts. While on Ethereum one can simple use the revert opcode and be sure that all storage reverts to the previous state the same is not true for Subtrate. All the storage changes that occurred before this error will remain intact.
The best practice in Substrate is to follow the “verify first, write last” principle. During execution you should make all the checks are using the ensure macro and that when you start modifying storage, no error can be thrown after.
While the outlined approach sounds clean, it can get very complicated when inter-pallet communication is thrown into the mix. This is especially true when multiple different external pallets need be called in one function. In such cases it becomes extremely hard to do all the verifications before changing storage.
Examples
There are a few places where this problem may occur:
chainbridge/src/lib.rs:L487-L506
pub fn transfer_fungible(
dest_id: ChainId,
resource_id: ResourceId,
to: Vec<u8>,
amount: U256,
) -> DispatchResult {
ensure!(
Self::chain_whitelisted(dest_id),
Error::<T>::ChainNotWhitelisted
);
let nonce = Self::bump_nonce(dest_id);
Self::deposit_event(RawEvent::FungibleTransfer(
dest_id,
nonce,
resource_id,
amount,
to,
));
Ok(())
}
This means that these functions should be called either before any storage changes (outside of current pallet) or the same checks should be done in the outside pallet, before changing the state. Even though example pallets are out of scope, in this example you can see that the actual transfer is happening before calling the transfer_fungible function:
example-pallet/src/lib.rs:L72-L80
pub fn transfer_native(origin, amount: BalanceOf<T>, recipient: Vec<u8>, dest_id: bridge::ChainId) -> DispatchResult {
let source = ensure_signed(origin)?;
ensure!(<bridge::Module<T>>::chain_whitelisted(dest_id), Error::<T>::InvalidTransfer);
let bridge_id = <bridge::Module<T>>::account_id();
T::Currency::transfer(&source, &bridge_id, amount.into(), AllowDeath)?;
let resource_id = T::NativeTokenId::get();
<bridge::Module<T>>::transfer_fungible(dest_id, resource_id, recipient, U256::from(amount.saturated_into()))
}
That may lead to tokens actually being transferred to the Bridge, but the event to transfer them outside is not emitted.
chainbridge/src/lib.rs:L421-L429
let complete = votes.try_to_complete(<RelayerThreshold>::get(), <RelayerCount>::get());
<Votes<T>>::insert(src_id, (nonce, prop.clone()), votes.clone());
Self::deposit_event(RawEvent::VoteFor(src_id, nonce, who.clone()));
if complete {
Self::finalize_execution(src_id, nonce, prop)
} else {
Ok(())
}
inside of finalize_execution function, an external pallet is called (this pallet is not implemented yet) which can potentially also fail, and even if that happens, the proposal will still be considered as executed.
In order to handle the case where multiple relayers attempt to submit a create_proposal
extrinsic in the same block, we should consider creation and approval the same action.
This can be deprecated in favour of the moduleId account Id.
TBD: How can we allocate to this accountId in genesis?
Presently whitelist is only checked on transfer out. It would be trivial to also check when accepting a proposal and would likely be expected behaviour.
In order to support administration methods aside from sudo/root, we should add an (optional) origin to the chainbridge pallet trait. This allows a primary origin check to be provided, with root being the fallback.
This is to prevent u32/u64 overflow when values reach the minimum/maximum number
Substrate does not have default overflow/underflow protection. In order to avoid this kind of bugs, it’s better to use safe functions like checked_add() and checked_sub(). In the current codebase math operations are mostly either incrementing (+1) or making operations with values that shouldn’t overflow/underflow logically:
chainbridge/src/lib.rs:L64
} else if self.votes_against.len() > (total - threshold) as usize {
It’s not really a big issue here, but if the threshold is bigger than the total, we’ll have underflow.
To distinguish this repo from other repositories that has similar names, it would be best to use a more descriptive name. At a glance we will have an idea that it is not a replacement code for the repo with the similar name but rather a module meant to plug with some other bigger projects.
pallet-chainbridge
pallet-chainbridge
construct_runtime!(
...
Chainbridge: pallet_chainbridge,
);
If there are not enough votes for a proposal to pass, the proposal will hang in the system forever and can be voted and executed later. The problem is that it’s impossible to cancel this proposal in order to send a new one and be sure that the old one will never be executed.
We've restricted the maximum balance size to 128bits here:
We should be able to modify this to allow for a conversion from whatever size the BalanceOf is to a U256. Requires further investigation.
On the substrate side, when votes are happening, the following function is called to define whether the vote is completed or not:
chainbridge/src/lib.rs:L60-L70
fn try_to_complete(&mut self, threshold: u32, total: u32) -> bool {
if self.votes_for.len() >= threshold as usize {
self.status = ProposalStatus::Approved;
true
} else if self.votes_against.len() > (total - threshold) as usize {
self.status = ProposalStatus::Rejected;
true
} else {
false
}
}
The problem is that after that call, the result of the vote is not checked. Only the fact that it has finished is taken into account.
While in most cases it’s true that the result of the completed vote can be predicted, there are some edge-cases when it’s not true. For example, when total or threshold variables are changed. That may lead to executing the proposal that should be canceled and vise versa.
Once we have specified how to handle this we need to update cancel_execution()
which currently just emits an event.
We need to specify appropriate fees for calls.
For execution of proposals we can use something like this: https://github.com/centrifuge/substrate-pallet-multi-account/blob/master/src/lib.rs#L489-L492
Presently there is no event checks in the tests. These should be added.
Some useful code here: https://github.com/centrifuge/substrate-pallet-multi-account/blob/3a40d9a1107816ee28989bf4bbbe3f0ab6a1822d/src/lib.rs#L833-L842
The issue I was facing was when multiple events fire in a single call, only the last is returned by expect_event
. We should extend the functionality to support n
most recent events.
Events can be collected like this:
let events: Vec<Event> = system::Module::<Test>::events().iter().map(|e| e.event.clone()).collect();
To improve readability and usability migrate to newer substrate frame macros
There is already significant progress here #94
Currently everything is contained in one source file, to give the pallet a bit more structure we can split off types etc..
Some reference code: https://github.com/parami-protocol/parami-blockchain/tree/main/pallets/chainbridge
This epic is meant to house all issues related to the update/upgrade of the ChainBridge pallet. This epic is not limited to a certain number of tasks, as more tasks may be added in as they are discovered/determined to be relevant.
Presently we store the account IDs of all validators who have voted for all proposal. This is inefficient in two ways:
To address these we should:
Use indexing to identify relayers. Rather than store the accountIds, we can simply assign indices to the relayers, and at some checkpoint allow relayers to be added/removed the and indices updated accordingly.
Consider some pruning method to remove completed proposals. Perhaps we can remove a proposal immediately after completion?
There's a lot of share code between the two pallets for mocking/testing. We should move this into a shared crate.
More examples will help explain what's possible with chainbridge. We should include more examples such as token transfer out of the chain, and executing a call that requires signed origin.
With this, we should have relayers check the existence of the call on chain prior to calling acknowledge_proposal
and provide only the hash of the call if it already exists on chain.
Similarly, for rejecting a proposal we do not need to require the call be provided at all.
We should use T::Balance
for amounts on fungible transfers.
In Substrate it’s only possible to calculate voting results only when a new vote is submitted. But if total or threshold changes, it should be possible to check the results of the current votings and call finalize_execution(or cancel_execution) without submitting a vote.
Almost the same problem we have in solidity. Even though it’s possible to execute the proposal without making a vote, in order to do that, status of the proposal should be ProposalStatus.Passed which can only be assigned when making a vote.
Recommendation
Add public function that checks if a vote can be completed and executed.
It doesn't make much sense for the chainId to change. It should be immutable from genesis.
Use updated benchmark macros
Hello,
I am getting this error while building this repo:-
error[E0283]: type annotations needed
--> example-pallet/src/lib.rs:80:85
|
80 | <bridge::Module<T>>::transfer_fungible(dest_id, resource_id, recipient, U256::from(amount.saturated_into()))
| ^^^^^^^^^^ cannot infer type for type parameter `T` declared on the trait `From`
|
= note: cannot satisfy `U256: From<_>`
= note: required by `std::convert::From::from`
I think if we add with saturated into then it will work something like this:-
<bridge::Module<T>>::transfer_fungible(dest_id, resource_id, recipient, U256::from(amount.saturated_into::<u128>()))
We should use our custom types, and include the source ID for proposal related events.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.