Skip to content
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

Merged
merged 27 commits into from
Mar 7, 2023
Merged

[r2r] Watcher rewards for ETH swaps #1658

merged 27 commits into from
Mar 7, 2023

Conversation

caglaryucekaya
Copy link

@caglaryucekaya caglaryucekaya commented Feb 15, 2023

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

@caglaryucekaya caglaryucekaya changed the title [wip] Watcher rewards for ETH swaps [r2r] Watcher rewards for ETH swaps Feb 15, 2023
Copy link
Member

@onur-ozkan onur-ozkan left a 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:

@@ -626,6 +633,16 @@ pub struct ValidateFeeArgs<'a> {
pub uuid: &'a [u8],
}

#[derive(Clone, Debug)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

redundant traits

Suggested change
#[derive(Clone, Debug)]

secret_hash: &[u8],
spend_tx: &[u8],
watcher_reward: bool,
) -> Result<Vec<u8>, String>;
Copy link
Member

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.

Copy link
Author

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.

Comment on lines 719 to 737
// 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"));
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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")),
}
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is better

Comment on lines 701 to 717
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
}
Copy link
Member

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.

Copy link
Author

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?

Copy link
Member

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?);

Copy link
Author

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

Copy link
Member

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

Copy link
Member

@onur-ozkan onur-ozkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last review iteration:

Comment on lines 1 to 7

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

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

Copy link
Author

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 😅

Comment on lines +1110 to +1111
false
//self.contract_supports_watchers
Copy link
Member

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?

Copy link
Author

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

Comment on lines 4069 to 4079
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
)));
}
Copy link
Member

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.

Comment on lines +3435 to +3442
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
)));
}
}
Copy link
Member

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?

Copy link
Collaborator

@shamardy shamardy left a 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.

mm2src/coins/eth/v2_activation.rs Show resolved Hide resolved
mm2src/mm2_main/src/lp_swap/maker_swap.rs Show resolved Hide resolved
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
Copy link
Collaborator

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.

Copy link
Author

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.

Copy link
Collaborator

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.

@caglaryucekaya caglaryucekaya changed the title [r2r] Watcher rewards for ETH swaps [wip] Watcher rewards for ETH swaps Feb 23, 2023
@caglaryucekaya caglaryucekaya changed the title [wip] Watcher rewards for ETH swaps [r2r] Watcher rewards for ETH swaps Feb 23, 2023
@cipig
Copy link
Member

cipig commented Feb 23, 2023

I have 2 questions:
Can the watchers decide which coins/chains they support and which not?
Can the watcher configure min_volumes?

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.
atm, ERC20 swaps cost 15 USD, see https://etherscan.io/gastracker ("Uniswap V3: Swap" has ~ the same cost as ADEX swaps).
image

shamardy
shamardy previously approved these changes Feb 23, 2023
Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥

@caglaryucekaya
Copy link
Author

@cipig

Can the watchers decide which coins/chains they support and which not?
Can the watcher configure min_volumes?

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".

onur-ozkan
onur-ozkan previously approved these changes Feb 24, 2023
Copy link
Member

@onur-ozkan onur-ozkan left a 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

@cipig
Copy link
Member

cipig commented Feb 24, 2023

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".

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.
I wonder though how we could make sure that watchers are not operating at a loss. Since this problem is mainly related to ERC20 (and maybe some other EVM chains like BEP20) i thought that a watcher could configure to not get involved with those chains at all. Or is the user paying for all the involved txfees?

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.

@caglaryucekaya caglaryucekaya dismissed stale reviews from onur-ozkan and shamardy via 3578aa4 February 27, 2023 15:50
@caglaryucekaya
Copy link
Author

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.

Okay, I will open some discussions about watchers on Mattermost soon, including this one.

Copy link
Collaborator

@shamardy shamardy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approve

@shamardy shamardy merged commit 32d2ed5 into dev Mar 7, 2023
@shamardy shamardy deleted the eth-swap-watchers branch March 7, 2023 15:29
@ca333 ca333 mentioned this pull request Mar 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants