-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation #1
Conversation
lib.rs
Outdated
} | ||
let to_balance = self.balance_of(to); | ||
// Total supply is limited by u128.MAX so no overflow is possible | ||
self.data.balances.insert(to, &(to_balance + value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if relevant, but take a look at this use-ink/ink#1831 regarding overflow checks, because I think there are some subtleties
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed it looks like soon all unchecked arithmetic is going to be banned from ink! contracts. I replaced all occurrences of + and - (which were impossible to overflow "by design") with saturating_add and saturating_sub.
if amount == 0 { | ||
self.data.allowances.remove((owner, spender)); | ||
} else { | ||
self.data.allowances.insert((owner, spender), &amount); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder whether it wouldn't be slightly better to do it like:
self.data.allowances.remove((owner, spender))
in line L186 -remove
returns "previous" value.- Check if
new_allowance
(amount
) equals0
:
- if "yes", do nothing
- if "no", call
insert
.
Seems like that would be 1 access to the Mapping
less.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, it could be a little bit more efficient.
Currently there are 3 branches this code can end up with and the resulting Mapping
operations are:
- allowance < by : 1 get
- allowance = by : 1 get + 1 remove
- allowance > by : 1 get + 1 insert
In the version you propose that would be:
- allowance < by : 1 remove + 1 insert
- allowance = by : 1 remove
- allowance > by : 1 remove + 1 insert
Not sure what is the relative cost of remove
vs get
, but the total number of operations globally is the same. Obviously, we expect variant 2 to "fire" much more often than variant 1, so expected number of operations is lower in your version. But I feel like this is a miniscule gain that sacrifices quite a lot of readability in a code that is supposed to be shown to community, so I'd lean towards simplicity > efficiency in this case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the version you propose that would be:
- allowance < by : 1 remove + 1 insert
- allowance = by : 1 remove
- allowance > by : 1 remove + 1 insert
current_allowance < by
: 1 remove (no insert since we would fail early withInsufficientAllowance
current_allowance = by
: 1 removecurrent_allowance > by
: 1 remove + 1 insert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're not 100% sure Mapping.take()
is stable, lets stay with the simpler 100% safe implementation for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's one thing that I'm missing in Rust (that I had in Scala) that is ability to write tests for an abstraction and have it automatically run for all of its implementation...
Here I'm thinking about a contract (punt NOT intended) of the PSP22 standard that all of its implementations should adhere to. That would be a set of rules that the implementation has to follow. Stuff like (from the top of my head):
- transferring more than allowance results in
XYZ
error increase_allowance(by_n) + increase_allowance(by_m) === increase_allowance(by_n + by_m)
PSP22::new(m)
creates an instance of a token with all of its balance in one account -m
's.- Event emittion
- Error returned
- etc.
I can see it done in two ways:
- "chamsko" - meaning there is a set of unit/e2e tests written for
Token
here. - not so "chamsko" - written as a macro that can be invoked on any struct implementing
PSP22
trait. In which case its implementors can add the suite via some macropsp22_standard_tests!(my_psp22_impl)
and have all the tests ran against their impl to check for adhering with the standard.
What do you think?
@deuszx I like the generic tests idea, but I want to make a separate PR with tests. I updated this PR with changes based on your suggestions. I decided to add one more thing to the logic - zero value operations result in no-ops (ie transfer of 0 tokens does not generate a Transfer event) |
First draft of pure ink! implementation of PSP22 standard.
This is a double-purpose crate:
PSP22
trait (viacontract_ref!
macro)cargo contract build --release --features "contract"