-
Notifications
You must be signed in to change notification settings - Fork 3
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
Fast Transfers #147
base: main
Are you sure you want to change the base?
Fast Transfers #147
Conversation
a6fc3bc
to
527e148
Compare
b092ed6
to
c1cd7f3
Compare
fa617c4
to
a86a62e
Compare
pub recipient: OmniAddress, | ||
pub fee: Fee, | ||
pub msg: String, | ||
pub storage_deposit_amount: Option<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.
And could you add comment, when the storage_deposit should be provided and for what.
|
||
let OmniAddress::Near(recipient) = fast_transfer.recipient.clone() else { | ||
env::panic_str("ERR_INVALID_STATE") | ||
}; |
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 ft_on_transfer
function, we burned the tokens. Since an error occurred here, we need to roll back the token burn.
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 would never happen because this method is only called if recipient is on Near
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 any case, the error could have occurred during the previous check. In that case, the tokens would already be burned, and we wouldn’t roll back the operation. The balance wouldn’t match.
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.
Actually if it's a bridged token (the only one we burn) bridge balance would be zero. Meaning if relayer sends an incorrect transaction the tokens will be burned and the refund to the relayer will fail. There will be no loss of funds from the bridge perspective, right? @karim-en
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.
Added tests and a fix here 88a9781
Not sure if more elegant solution is possible
near/omni-bridge/src/lib.rs
Outdated
.add_transfer_message(transfer_message, relayer_id.clone()) | ||
.saturating_add(required_balance); | ||
|
||
self.update_storage_balance(relayer_id, required_balance, env::attached_deposit()); |
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.
env::attached_deposit()
-- this value isn’t always zero, is it?
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.
Changed to 0
token_id: token_id.clone(), | ||
recipient: fast_fin_transfer_msg.recipient.clone(), | ||
amount: U128(amount.0 + fast_fin_transfer_msg.fee.fee.0), | ||
fee: fast_fin_transfer_msg.fee, |
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.
Who will ultimately receive the native fee? The Fast Transfer Relayer or the General Relayer? Are we at risk of spending the same funds twice?
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 recipient is on Near, the fast relayer will get the fee after finalizing the original transfer.
If recipient is on another chain, it's a bit tricky. The fee is claimed only after transfer is finalized on the 3rd chain. And right now it can be another relayer. Perhaps we need to split the fee between the two relayers in this scenario @karim-en ?
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 we can allow sign_transfer
only for the fast relayer in this case
Edit: but then fast relayer will be able to finalize the original transfer while keeping the 3rd-chain message unsigned. Need to think more
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.
Actually, in case recipient is on another chain we need a solution for both normal and fast transfers. So perhaps it's not quite in scope of this PR. As of now, the relayer that finalizes the transfer on the destination chain will receive the fees
I might also consider adding an option to opt out of fast-transfer. |
near/omni-bridge/src/lib.rs
Outdated
) | ||
.then( | ||
Self::ext(env::current_account_id()) | ||
.with_static_gas(VERIFY_PROOF_CALLBACK_GAS) |
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.
The const VERIFY_PROOF_CALLBACK_GAS
isn't correct
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 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.
Fixed 1f38b04
@kiseln Please add integration tests to this new feature |
near/omni-bridge/src/lib.rs
Outdated
@@ -875,6 +1022,14 @@ impl Contract { | |||
pub fn get_current_destination_nonce(&self, chain_kind: ChainKind) -> Nonce { | |||
self.destination_nonces.get(&chain_kind).unwrap_or_default() | |||
} | |||
|
|||
#[private] | |||
pub fn ft_resolve_transfer(&mut self, amount: U128) -> 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.
The naming could be confusing (in the explorer) because the nep141 implementation also has a ft_resolve_transfer
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.
Renamed 1f38b04
near/omni-bridge/src/lib.rs
Outdated
pub fn ft_resolve_transfer(&mut self, amount: U128) -> U128 { | ||
match env::promise_result(0) { | ||
PromiseResult::Successful(_) => U128(0), | ||
PromiseResult::Failed => 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.
This refund is dangerous due to the undetermined behavior of the ft_trnasfer_call
, so let's always return zero.
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.
Okay, just to confirm, if ft_transfer_call
reverts it's sender who will lose money and not the relayer, right? So senders will be responsible for the target of ft_transfer_call
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.
Changed to 0 1f38b04
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.
Yes.
In the follow-up PR, we can try to solve the refund issue, which was mentioned multiple times in the audits.
What we should take into account is:
- The
ft_transfer_call
can fail only if the storage wasn't deposited or due to malicious contract, otherwise it returns value. - The mint call
ft_transfer_call
if there is a message, so it has the same behaviour. - The
ft_transfer
can fail only if the storage wasn't deposited.
Since we already verify the storage deposit, then we can do the refund only if the ft_transfer_call
returned a refund value.
near/omni-bridge/src/lib.rs
Outdated
) | ||
.then( | ||
Self::ext(env::current_account_id()) | ||
.with_static_gas(FAST_TRANSFER_CALLBACK_GAS) |
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.
.with_static_gas(FAST_TRANSFER_CALLBACK_GAS) | |
.with_static_gas(FAST_TRANSFER_CALLBACK_GAS + FT_TRANSFER_CALL_GAS) |
This could be not enough to do the ft_transfer_call
in the callback
Can you please also add tests for this flow (failed and success)
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 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.
Fixed 88a9781
near/omni-bridge/src/lib.rs
Outdated
@@ -55,6 +56,8 @@ const DEPLOY_TOKEN_GAS: Gas = Gas::from_tgas(50); | |||
const BURN_TOKEN_GAS: Gas = Gas::from_tgas(10); | |||
const MINT_TOKEN_GAS: Gas = Gas::from_tgas(5); | |||
const SET_METADATA_GAS: Gas = Gas::from_tgas(10); | |||
const RESOLVE_TRANSFER_GAS: Gas = Gas::from_tgas(3); | |||
const FAST_TRANSFER_CALLBACK_GAS: Gas = Gas::from_tgas(40); |
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 can be reduced if the FT_TRANSFER_CALL_GAS
has been added
.then( | ||
Self::ext(env::current_account_id()) | ||
.with_static_gas(RESOLVE_TRANSFER_GAS) | ||
.resolve_transfer(fast_transfer.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.
.resolve_transfer(fast_transfer.amount), | |
.resolve_transfer(fast_transfer.amount.0 - fast_transfer.fee.fee.0), |
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 we ever use the amount in resolve_transfer
it will be for the purpose of rolling back ft_transfer_call
so full amount is more appropriate
token: OmniAddress::Near(fast_transfer.token_id.clone()), | ||
amount: fast_transfer.amount.clone(), | ||
recipient: fast_transfer.recipient.clone(), | ||
fee: fast_transfer.fee.clone(), |
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.
What about creating an invalid TransferMessage with a very very large fee? Finalizing it on the destination chain, and then claiming our excessively large fee. In that case, a lot of Ether, for example, would be minted for the relayer without reason.
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.
Fantastic find - this is a severe problem that we need to address. Also, replacing sender to the relayer's Near address we accidentally change the native fee from another token to NEAR.
First, we need to allow claiming fee only after the original message is finalized and fees validated.
Second, we either need to preserve original sender when creating message or look it up in some other way when claiming the fee.
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 added restriction on claiming fee in 333be17
082a011
to
88a9781
Compare
Description
Fast transfer is initiated by sending tokens to the bridge contract with a proper message for
ft_on_transfer
.Recipient on Near
Recipient on other chain
Implementation
msg
has been changed forft_on_transfer
to resolve betweeninit_transfer
andfast_fin_transfer
.Concerns