-
Notifications
You must be signed in to change notification settings - Fork 94
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
[r2r] Watcher rewards for ETH swaps #1658
Conversation
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.
Great work 🔥
First notes from my partial review:
mm2src/coins/lp_coins.rs
Outdated
@@ -626,6 +633,16 @@ pub struct ValidateFeeArgs<'a> { | |||
pub uuid: &'a [u8], | |||
} | |||
|
|||
#[derive(Clone, Debug)] |
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.
redundant traits
#[derive(Clone, Debug)] |
secret_hash: &[u8], | ||
spend_tx: &[u8], | ||
watcher_reward: bool, | ||
) -> Result<Vec<u8>, String>; |
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 started migrating from ERR
(string errors) couple months ago. I think we should stop adding String
as an error type to 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.
I didn't add this, it was this way already. There are many functions like this with different implementations for all coins. We can open a refactoring issue for this and assign it to someone in next sprints since it will take some time.
mm2src/mm2_main/src/lp_swap.rs
Outdated
// TODO: This can be done in a better way | ||
pub async fn watcher_reward_from_gas( | ||
coin: &MmCoinEnum, | ||
other_coin: &MmCoinEnum, | ||
gas: u64, | ||
watcher_reward: bool, | ||
) -> Result<Option<u64>, String> { | ||
if !watcher_reward { | ||
Ok(None) | ||
} else if let MmCoinEnum::EthCoin(coin) = &coin { | ||
let gas_price = try_s!(coin.get_gas_price().compat().await).as_u64(); | ||
Ok(Some(gas * gas_price)) | ||
} else if let MmCoinEnum::EthCoin(coin) = &other_coin { | ||
let gas_price = try_s!(coin.get_gas_price().compat().await).as_u64(); | ||
Ok(Some(gas * gas_price)) | ||
} else { | ||
return Err(ERRL!("At least one coin must be ETH to use watcher reward")); | ||
} | ||
} |
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.
// TODO: This can be done in a better way | |
pub async fn watcher_reward_from_gas( | |
coin: &MmCoinEnum, | |
other_coin: &MmCoinEnum, | |
gas: u64, | |
watcher_reward: bool, | |
) -> Result<Option<u64>, String> { | |
if !watcher_reward { | |
Ok(None) | |
} else if let MmCoinEnum::EthCoin(coin) = &coin { | |
let gas_price = try_s!(coin.get_gas_price().compat().await).as_u64(); | |
Ok(Some(gas * gas_price)) | |
} else if let MmCoinEnum::EthCoin(coin) = &other_coin { | |
let gas_price = try_s!(coin.get_gas_price().compat().await).as_u64(); | |
Ok(Some(gas * gas_price)) | |
} else { | |
return Err(ERRL!("At least one coin must be ETH to use watcher reward")); | |
} | |
} | |
pub async fn watcher_reward_from_gas( | |
coin: &MmCoinEnum, | |
other_coin: &MmCoinEnum, | |
gas: u64, | |
watcher_reward: bool, | |
) -> Result<Option<u64>, String> { | |
if !watcher_reward { | |
return Ok(None); | |
} | |
match (coin, other_coin) { | |
(MmCoinEnum::EthCoin(coin), _) | (_, MmCoinEnum::EthCoin(coin)) => { | |
let gas_price = try_s!(coin.get_gas_price().compat().await).as_u64(); | |
Ok(Some(gas * gas_price)) | |
}, | |
_ => Err(ERRL!("At least one coin must be ETH to use watcher reward")), | |
} | |
} |
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.
Nice, this is better
mm2src/mm2_main/src/lp_swap.rs
Outdated
pub async fn watcher_reward_amount( | ||
coin: &MmCoinEnum, | ||
other_coin: &MmCoinEnum, | ||
watcher_reward: bool, | ||
) -> Result<Option<u64>, String> { | ||
const WATCHER_REWARD_GAS: u64 = 100_000; | ||
watcher_reward_from_gas(coin, other_coin, WATCHER_REWARD_GAS, watcher_reward).await | ||
} | ||
|
||
pub async fn min_watcher_reward( | ||
coin: &MmCoinEnum, | ||
other_coin: &MmCoinEnum, | ||
watcher_reward: bool, | ||
) -> Result<Option<u64>, String> { | ||
const MIN_WATCHER_REWARD_GAS: u64 = 70_000; | ||
watcher_reward_from_gas(coin, other_coin, MIN_WATCHER_REWARD_GAS, watcher_reward).await | ||
} |
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.
Wouldn't be better checking watcher_reward
in maker/taker_swap.rs
and avoid awaiting these(watcher_reward_from_gas
, min_watcher_reward
, watcher_reward_amount
) async
functions? This way we can avoid unnecessary context switches, thread yielding, etc. I think it matters for low power devices like mobile phones.
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. I was trying to avoid repeating this block every time we call these functions:
let reward = if watcher_reward {
watcher_reward_amount(taker_coin, maker_coin)
} else {
None
}
Can you think of a better way for this?
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.
You can use then_some
on watcher_reward
.
let reward = watcher_reward.then_some(watcher_reward_amount().await?);
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.
That's what I tried first but then_some
evaluates the passed function whether the bool is true or false
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.
Then your suggestion makes more sense
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.
Last review iteration:
|
||
running 1 test | ||
**is_zcash: true | ||
test block_header::tests::test_rick_v1_block_header_des ... ok | ||
|
||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 45 filtered out; finished in 0.00s | ||
|
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 is this 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.
It's a log file that I accidentally pushed 😅
false | ||
//self.contract_supports_watchers |
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.
Why it's hardcoded as false?
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 ETH watcher rewards aren't complete yet, and watchers shouldn't be used until it's ready
mm2src/coins/eth.rs
Outdated
let address_input = get_function_input_data(&decoded_input, function, 0) | ||
.map_to_mm(ValidatePaymentError::TxDeserializationError)?; | ||
let value_input = get_function_input_data(&decoded_input, function, 1) | ||
.map_to_mm(ValidatePaymentError::TxDeserializationError)?; | ||
|
||
if address_input != Token::Address(fee_addr) { | ||
return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( | ||
"{}: ERC20 Fee tx was sent to wrong address {:?}, expected {:?}", | ||
INVALID_RECEIVER_ERR_LOG, address_input, fee_addr | ||
))); | ||
} |
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.
address_input
validation should be right after address_input
computation.
if let Some(min_reward) = input.min_watcher_reward { | ||
if tx_from_rpc.value.as_u64() < min_reward { | ||
return MmError::err(ValidatePaymentError::WrongPaymentTx(format!( | ||
"Payment tx value arg {} is less than the minimum expected watcher reward {}", | ||
tx_from_rpc.value, min_reward | ||
))); | ||
} | ||
} |
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 this check move right before or after the if tx_from_rpc.to != Some(expected_swap_contract_address)
check?
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.
Great work! first review iteration.
InvalidCoinType(String), | ||
} | ||
|
||
// This needs discussion. We need a way to determine the watcher reward amount, and a way to validate it at watcher side so |
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 have a question regarding if 2 watchers try to spend the same payment with one of those transactions failing, will the watcher pay some gas for the failed transaction/contract call? If so, we need to think of a way to avoid this in the next PRs.
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, this is one thing we need to think carefully and test thoroughly since it can cost ETH to watchers. This and similar problems can also be exploited by attackers to cause damage to the watchers. The contract function to spend the maker payment first checks if the payment is already spent at the first line, with a require
statement. As far as I understand, require
statement isn't supposed to consume any gas if it fails, but I wrote an integration test case where two watchers spend the same maker payment and the result is that the second watcher also spends some gas. I'll research/experiment with this further in the next sprint to understand it better. If it's inevitable to spend gas for failed payments, than we need to find a way to stop the watchers from ever attempting. One solution could be to use some randomized delay before the watchers spend any payments to give some space for other watchers, then check if the payment is already spent before spending it themselves.
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 have a fear that there might be no way to avoid this but to subscribe with only one watcher for every EVM related swap, but this has its own problems too if this watcher is the maker.
I have 2 questions: Problem i see is that users could not set min_volume for their ERC20 swaps and if they fail and watcher claims the payment, it could well be that it's not worth it, like if the exchanged tokens are worth 2 USD, but txfees to claim those tokens is 10 USD. |
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.
🔥
Currently there's no such configuration, but yes I should add something about it in the next PRs. I think it should not be the watchers but users who should decide for which coins they want watcher support, right? And is it necessary to configure it coin by coin, or is a configuration like "use_watcher_rewards" enough? If set false, this can disable watcher support for all ETH-based coins. And for the min_volume, it is again the user who should decide I think. As long as they know the reward amount, it should be fine. But we could also add some rules to the watchers like "the trade amount must always be larger than the reward". |
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.
thank you for the fixes
Yes, indeed, min_volume is and should be set by the users who post the orders. I was just thinking that it would make no sense for a watcher to claim payments if the txfees are higher than the claimed amount. But i see that it will not be easy to configure/implement that on watcher side, since he would need to know how much those claimed tokens are worth, so he needs to know their price, which would make the entire thing pretty complex. We should continue this discussion somewhere else though, since it's out of the scope of this PR. Sorry that i started it here in the first place. |
3578aa4
Okay, I will open some discussions about watchers on Mattermost soon, including this one. |
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.
Re-approve
Adds watcher reward functionality for ETH-ETH swaps. The problem is that in order to spend the maker payment or refund the taker payment, watchers have to spend some gas fee. With this PR, both taker and maker add a small amount of watcher reward to their corresponding payments to compensate for this cost (plus a slight profit). This reward goes to whoever spends/refunds the payments: if watcher spends/refunds it goes to the watcher, if taker/maker complete the swap themselves they take the reward back. In case of refund, taker's reward goes back to the taker and maker's reward goes back to the maker. If the taker/maker spend each other's payments, then it is simply an exchange of rewards of same value between them. The reward will be activated if either the taker or the maker coin is ETH and both of them agree to use watchers. If one of the coins is ETH, watchers will only accept payments with rewards. The updated contract with the rewards can be found here:
https://github.com/caglaryucekaya/etomic-swap/blob/ethereum-swap-watchers/contracts/EtomicSwap.sol