-
Notifications
You must be signed in to change notification settings - Fork 7
feat/aml: locking accounts #66
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
base: main
Are you sure you want to change the base?
Conversation
let value = "example".to_string(); | ||
let poc = PanicOnClone::from_ref(&value); | ||
assert!(ptr::eq(&**poc, &value)); | ||
assert_eq!(&**poc, &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.
No test for panic behavior?
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 think this struct needs some unit tests... too many special cases.
AccountNotFound(AccountId), | ||
|
||
#[error("account '{0}' is locked")] | ||
AccountLocked(AccountId), |
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.
If I understand correctly what "locked" means here, I think the proper language is "frozen"... we "freeze" bank accounts.
async fn is_account_locked(&self, account_id: &AccountIdRef) -> anyhow::Result<bool>; | ||
async fn defuse_is_account_locked( | ||
&self, | ||
defuse_contract_id: &AccountId, | ||
account_id: &AccountIdRef, | ||
) -> anyhow::Result<bool>; | ||
|
||
async fn force_lock_account(&self, account_id: &AccountIdRef) -> anyhow::Result<bool>; | ||
async fn defuse_force_lock_account( | ||
&self, | ||
defuse_contract_id: &AccountId, | ||
account_id: &AccountIdRef, | ||
) -> anyhow::Result<bool>; | ||
async fn force_unlock_account(&self, account_id: &AccountIdRef) -> anyhow::Result<bool>; | ||
async fn defuse_force_unlock_account( | ||
&self, | ||
defuse_contract_id: &AccountId, | ||
account_id: &AccountIdRef, | ||
) -> anyhow::Result<bool>; |
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 really don't think it's necessary to duplicate these methods... just one of them is enough, and specifying the smart contracts in the tests is alright.
@@ -31,3 +32,23 @@ pub trait AccountManager { | |||
/// NOTE: MUST attach 1 yⓃ for security purposes. | |||
fn invalidate_nonces(&mut self, nonces: Vec<AsBase64<Nonce>>); | |||
} | |||
|
|||
#[ext_contract(ext_force_account_locker)] | |||
pub trait AccountForceLocker: AccessControllable { |
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.
Besides using the word freeze instead of lock, I think this trait's name should be something like AccountLockable. If we agree on using freeze, then it's AccountFreezable.
#[autoimpl(AsMut using self.0)] | ||
#[near(serializers = [borsh])] | ||
#[repr(transparent)] // needed for `transmute()` below | ||
pub struct PanicOnClone<T: ?Sized>(T); |
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.
Perhaps write some docs that explain the use-case of this struct, i.e., what problem is solves with a little example.
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.
/// This struct is used as a tool to make it possible to derive borsh serialization in such a way where serialization can take over a reference and include it as if it's owned by the struct/enum being serialized. The reference can be taken in the function from_ref()
.
.amount_for(&ft), | ||
123, | ||
); | ||
borsh::to_vec(&versioned).expect("unale to serialize versioned account") |
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.
typo: unable
// which we want to avoid. | ||
// | ||
// So, since we allow locked accounts to receive incoming transfers, it means | ||
// that `mt_transfer_call()` to locked recepients will always result in full |
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.
typo: recepients
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.
Done with the first review round.
Cargo.lock
Outdated
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.
Cargo.lock is not up to date. Please run a full build then commit it again.
@@ -1,2 +1,3 @@ | |||
pub mod r#as; |
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.
Can we call this something that doesn't use a literal prefix? Maybe ser_as?
} | ||
impl<R> ReadExt for R where R: Read {} | ||
|
||
pub struct TeeReader<R, W> { |
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.
/// A reader that writes everything it reads to the specified writer. Can bee seen as a passthrough.
/// The struct itself does not do any kind of caching. Any apparent caching is possibly done by the inner read and writer.
@@ -48,3 +48,48 @@ pub trait MultiTokenForceWithdrawer: MultiTokenWithdrawer + AccessControllable { | |||
msg: Option<String>, | |||
) -> PromiseOrValue<Vec<U128>>; | |||
} | |||
|
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.
/// MultiTokenCore
functions, but a version that bypasses the locked/frozen state of an account.
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.
/// All functions must be #[private]
or only authorized callers
/// Attached deposit of 1yN is required for security purposes. | ||
/// | ||
/// NOTE: this still allows for force withdrawals/transfers | ||
fn force_lock_account(&mut self, account_id: AccountId) -> bool; |
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.
Instead of returning a bool here (and in unlock), we should probably just panic with a message "already locked".
@@ -21,7 +22,7 @@ pub trait AccountManager { | |||
/// i.e. this key can't be used to make any actions unless it's re-created. | |||
/// | |||
/// NOTE: MUST attach 1 yⓃ for security purposes. | |||
fn remove_public_key(&mut self, public_key: &PublicKey); | |||
fn remove_public_key(&mut self, public_key: PublicKey); |
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.
Don't we usually prefer passing a reference?
#[inline] | ||
pub fn as_unlocked_mut(&mut self) -> Option<&mut T> { | ||
if self.locked { | ||
pub const fn as_unlocked_mut(&mut self) -> Option<&mut T> { |
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.
Perhaps a better name for all "as_unlocked*" functions is "get*".
#[inline] | ||
pub fn unlock(&mut self) -> Option<&mut T> { | ||
if !self.locked { | ||
pub const fn as_unlocked_or_mut(&mut self, force: bool) -> Option<&mut T> { |
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.
get_force_mut
?
#[inline] | ||
pub const fn is_locked(&self) -> bool { | ||
self.locked | ||
} | ||
|
||
#[must_use] | ||
#[inline] | ||
pub const fn as_locked(&self) -> Option<&T> { |
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.
Do we really need this function? What's the purpose of a function that gets while asserting the lock is activated? It should be either "I don't care about the lock" or "Get only if unlocked". This feels more like technical jargon to me.
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.
This applies to all "as_locked" functions.
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.
This Lock
was initially made when we were writing the very first version of defuse contract, which was implementing "escrow swap" functionality. It turned out that in some particular cases you expect the lock to be locked, because some actions might be permitted only when it's locked.
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.
Can we remove these now to make the API simpler to understand? If we need these functions in the future, we can assess whether we need them and add them if we do.
impl<T, As> BorshSerializeAs<Lock<T>> for Lock<As> | ||
where | ||
As: BorshSerializeAs<T>, | ||
{ | ||
#[inline] | ||
fn serialize_as<W>(source: &Lock<T>, writer: &mut W) -> io::Result<()> | ||
where | ||
W: io::Write, | ||
{ | ||
Lock { | ||
locked: source.locked, | ||
value: AsWrap::<&T, &As>::new(&source.value), | ||
} | ||
.serialize(writer) | ||
} | ||
} | ||
|
||
impl<T, As> BorshDeserializeAs<Lock<T>> for Lock<As> | ||
where | ||
As: BorshDeserializeAs<T>, | ||
{ | ||
#[inline] | ||
fn deserialize_as<R>(reader: &mut R) -> io::Result<Lock<T>> | ||
where | ||
R: io::Read, | ||
{ | ||
Lock::<AsWrap<T, As>>::deserialize_reader(reader).map(|v| Lock { | ||
locked: v.locked, | ||
value: v.value.into_inner(), | ||
}) | ||
} | ||
} |
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.
We need some serialization tests to assert the outcome.
/// | ||
/// This is safe to assume that legacy [`Account`] doesn't start with | ||
/// this prefix, since the first 4 bytes in legacy [`Account`] were used | ||
/// to detote the length of `prefix: Box<[u8]>` in [`LookupMap`] for |
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.
"denote"?
impl BorshDeserializeAs<Lock<Account>> for MaybeVersionedAccountEntry { | ||
fn deserialize_as<R>(reader: &mut R) -> io::Result<Lock<Account>> | ||
where | ||
R: io::Read, | ||
{ | ||
let mut buf = Vec::new(); | ||
// There will always be 4 bytes for u32: | ||
// * either `VERSIONED_MAGIC_PREFIX`, | ||
// * or u32 for `Account.nonces.prefix` | ||
let prefix = u32::deserialize_reader(&mut reader.tee(&mut buf))?; | ||
|
||
if prefix == VERSIONED_MAGIC_PREFIX { | ||
VersionedAccountEntry::deserialize_reader(reader) | ||
} else { | ||
Account::deserialize_reader( | ||
// prepend already consumed part of the reader | ||
&mut buf.chain(reader), | ||
) | ||
.map(Into::into) | ||
} | ||
.map(Into::into) | ||
} | ||
} |
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 defining the serialization manually anyway, isn't it better to define the serialization schema without PanicOnClone? We're crossing the barrier of "everything is just derived" to "we have to customize it"... let's go all the way and serialize the enum manually, and deserialize it manually as well. We're already doing that. It would be really awesome if we could throw out PanicOnClone.
|
||
#[rstest] | ||
#[test] | ||
fn legacy_upgrade(random_seed: Seed) { |
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.
This is an upgrade test. For such complex serialization schemes, we need fixed value tests too (serialized hex values, back and forth in both cases). This helps in avoiding bugs like the Nest thing, where we need that the complicated scheme ended with something we understand.
To illustrate what I see here: Imagine a function f(x) and its inverse g(x). The test here is kind of testing f(g(x)) = 1
. But we need a test for what f(x) alone does, and what g(x) alone does too, because there are trivial cases where f(g(x)) = 1
is always true, but both f and g are broken.
IMHO, this format of testing must exist in any non-standard serialization plan.
#[error("public key already exists")] | ||
PublicKeyExists, | ||
#[error("public key '{1}' already exists for account '{0}'")] | ||
PublicKeyExists(AccountId, PublicKey), |
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 was hesitating, but I'll say it. I love this change. I love to always specify the parameters in errors to make debugging easier.
See docs for
AccountForceLocker
: