Discussion: `Balance` vs `U256`, should we expose only a singular balance type to users?
Problem Statement
pallet-revive uses two types for balance: a generic Balance (required by Polkadot SDK, typically set to u128) and a U256 (for Ethereum and Solidity compatibility).
The consequence is that when deploying/calling/testing/writing contracts, users have to know about both types and handle both of them.
This can be confusing, as pallet-revive defines a multiplication factor to convert between both types. For Passet Hub that's 100_000_000 (source).
You can find examples throughout all tools and our APIs:
Balance is used when e.g. communicating with a node via the E2E API or setting storage deposit limits.
U256 is used e.g. when the contract queries its own value or in a contract call trace.
Another example where this is visible is the Contracts UI:
Showing the balance of a contract:
Versus passing a "balance" into it:
A pitfall is that there is a Balance: Into<U256> (which is also needed), but I can already see people doing something like
fn transfer(v: U256) { … }
let value: Balance = 1_000;
transfer(value.into()) // this is wrong, it should be `(value * native_to_eth_ratio).into()`
Solutions
A simplification would be to decide on one type and then have ink, ink_e2e, cargo-contract, Contracts UI use only that one, doing the mapping in the background.
Unify to Balance
- ➕ @davidsemakula: "One thing that speaks for
Balanceas the one type, is that converting fromBalancetoU256is presumably infallible, whileU256toBalanceis a fallible conversion."
Unify to U256
- ➕
U256as it allows for a bigger number range (so no precision loss). - ➕
U256because when cross-contract calling to Solidity contracts using their native types makes things more seamless for the user.
Leave everything as is
There is the danger of creating a leaky abstraction, we could end up making things even more confusing. E.g. I don't know what PAPI or inkathon do at the moment.
Interacting with balances or assets is easier when using Balance
Since ink! does not support u256, how should I correctly translate uint from Solidity into ink!? If I translate it into uint128, wouldn’t that cause the VM’s output results to be inconsistent?
ink! does support U256. When you convert U256 into a smaller uint it could be possible that it fails and therefore you should use:
let balance: Balance = u256_value.try_from();
Indeed, I've found this confusing when building contracts that deal with balances.
Having the Balance type gives me the message that I should be using this type when storing or dealing with balances (as documented). But self.env().transferred_value(): U256, and then I got confused on how to properly convert between these two values.
In my ink!v6 contracts I always have this couple of functions, as I couldn't find any function or trait to convert them correctly:
fn u256_to_balance(value: U256) -> Balance {
(value / U256::from(100_000_000)).as_u128()
}
fn balance_to_u256(value: Balance) -> U256 {
U256::from(value) * U256::from(100_000_000)
}
The 100_000_000 I came up a bit by trial-and-error, but I understand it's given the difference between ethereum's decimals and the polkadot's chain one.
I agree then this is something that needs to improve. IMO consolidating to Balance would be preferable. U256 is quite bad to work with on the dApp side: it gets represented in the metadata as a (u64, u64, u64, u64)*, so it's actually really hard to work with (is it LE? BE?). Additionally, all the pallet calls use the native balance type, e.g. when calling Revive.call the value parameter is the native balance type.
*U256 is weird :o I might actually have to open an issue in polkadot-sdk, since the U256 codec is defined in scale-info. But it gets exposed as (u64,u64,u64,u64) instead. Even so, it would be confusing anyway because of the different amount of decimals.
Writing some more thoughts down here. We should make a decision soon.
What I am concerned with is compatibility with Solidity contracts. Those always use U256, which allows to express a larger number range than the u128 used on e.g. Asset Hub. So by opting for Balance as our ink! API type we are introducing incompatibility from the start.
The pallet-revive host functions all use U256. If we were to use Balance in ink!, we would have the overhead in each smart contract of converting anything regarding value from and to U256. So it incurs some Gas overhead (i.e. performance loss/user costs). I expect this overhead to be negligible though.
I also don't like this situation, to me the whole tooling around Balance in the classic Substrate frontend libraries makes a lot of sense. But my intuition right now is that ripping the band aid off and using U256 throughout would be the cleanest thing to do and would save us some headache in the long term. This change would have to pull through to Contracts UI, PAPI, cargo-contract (which has a command flag for --value), etc.
Thoughts?
For what it's worth, I ended up raising the [u64; 4] issue for U256 https://github.com/paritytech/polkadot-sdk/issues/9773 and it was fixed. In an upcoming runtime upgrade soon™, those values will be exposed as u256 (i.e. bigint in typescript instead of [bigint, bigint, bigint, bigint])
IMO yes, settle on one instead of having 2 different ones, having to convert between them. And now I see both of them a bit more balanced - I wouldn't mind either of them.
@voliva Gotcha! Nice that you created this issue!
After a bit more thought, I also think that unifying around U256 makes the most sense from a "core language" perspective.
The rationale on my side is that for the core language, the target execution environment is pallet-revive which the language only interacts with via system calls/host-functions, so I think that's the API boundary that should inform this decision from a core language perspective. Since pallet-revive only receives and returns U256 for balances at this fundamental API boundary, it makes sense that this should be the fundamental balance type for the core language.