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

Add check for account age to apply restrictions #2801

Merged
merged 15 commits into from May 3, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -63,6 +63,7 @@
public class AccountAgeWitnessService {
private static final Date RELEASE = Utilities.getUTCDate(2017, GregorianCalendar.NOVEMBER, 11);
public static final Date FULL_ACTIVATION = Utilities.getUTCDate(2018, GregorianCalendar.FEBRUARY, 15);
public static final long SAFE_ACCOUNT_AGE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.MARCH, 15).getTime();
Copy link
Member

Choose a reason for hiding this comment

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

I think we should re-use the static from AccountAgeRestrictions


public enum AccountAge {
LESS_ONE_MONTH,
Expand Down
63 changes: 57 additions & 6 deletions core/src/main/java/bisq/core/payment/PaymentAccountUtil.java
Expand Up @@ -26,6 +26,7 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand All @@ -37,18 +38,68 @@

@Slf4j
public class PaymentAccountUtil {
public static boolean isAnyPaymentAccountValidForOffer(Offer offer, Collection<PaymentAccount> paymentAccounts) {
for (PaymentAccount paymentAccount : paymentAccounts) {
if (isPaymentAccountValidForOffer(offer, paymentAccount))

public static boolean isRiskyBuyOfferWithImmatureAccountAge(Offer offer, AccountAgeWitnessService accountAgeWitnessService) {
long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getMakersAccountAge(offer, new Date());
return offer.isBuyOffer() &&
PaymentMethod.hasChargebackRisk(offer.getPaymentMethod()) &&
accountCreationDate > AccountAgeWitnessService.SAFE_ACCOUNT_AGE_DATE;
}

public static boolean isSellOfferAndAnyTakerPaymentAccountForOfferMature(Offer offer,
Collection<PaymentAccount> takerPaymentAccounts,
AccountAgeWitnessService accountAgeWitnessService) {
if (offer.isBuyOffer() || !PaymentMethod.hasChargebackRisk(offer.getPaymentMethod()))
return true;

for (PaymentAccount takerPaymentAccount : takerPaymentAccounts) {
if (isTakerPaymentAccountForOfferMature(offer, takerPaymentAccount, accountAgeWitnessService))
return true;
}
return false;
}

private static boolean isTakerPaymentAccountForOfferMature(Offer offer,
PaymentAccount takerPaymentAccount,
AccountAgeWitnessService accountAgeWitnessService) {
long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getMyAccountAge(takerPaymentAccount.getPaymentAccountPayload());
return isTakerPaymentAccountValidForOffer(offer, takerPaymentAccount) &&
accountCreationDate <= AccountAgeWitnessService.SAFE_ACCOUNT_AGE_DATE;
}

public static boolean hasMakerAnyMatureAccountForBuyOffer(Collection<PaymentAccount> makerPaymentAccounts,
AccountAgeWitnessService accountAgeWitnessService) {
for (PaymentAccount makerPaymentAccount : makerPaymentAccounts) {
if (hasMakerMatureAccountForBuyOffer(makerPaymentAccount, accountAgeWitnessService))
return true;
}
return false;
}

private static boolean hasMakerMatureAccountForBuyOffer(PaymentAccount makerPaymentAccount,
AccountAgeWitnessService accountAgeWitnessService) {
if (!PaymentMethod.hasChargebackRisk(makerPaymentAccount.getPaymentMethod()))
return true;

long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getMyAccountAge(makerPaymentAccount.getPaymentAccountPayload());
return accountCreationDate <= AccountAgeWitnessService.SAFE_ACCOUNT_AGE_DATE;
}

public static boolean isAnyTakerPaymentAccountValidForOffer(Offer offer, Collection<PaymentAccount> takerPaymentAccounts) {
for (PaymentAccount takerPaymentAccount : takerPaymentAccounts) {
if (isTakerPaymentAccountValidForOffer(offer, takerPaymentAccount))
return true;
}
return false;
}

public static ObservableList<PaymentAccount> getPossiblePaymentAccounts(Offer offer, Set<PaymentAccount> paymentAccounts) {
public static ObservableList<PaymentAccount> getPossiblePaymentAccounts(Offer offer,
Set<PaymentAccount> paymentAccounts,
AccountAgeWitnessService accountAgeWitnessService) {
ObservableList<PaymentAccount> result = FXCollections.observableArrayList();
result.addAll(paymentAccounts.stream()
.filter(paymentAccount -> isPaymentAccountValidForOffer(offer, paymentAccount))
.filter(paymentAccount -> isTakerPaymentAccountValidForOffer(offer, paymentAccount))
.filter(paymentAccount -> isTakerPaymentAccountForOfferMature(offer, paymentAccount, accountAgeWitnessService))
.collect(Collectors.toList()));
return result;
}
Expand All @@ -61,7 +112,7 @@ public static String getInfoForMismatchingPaymentMethodLimits(Offer offer, Payme
"Payment method from offer: " + offer.getPaymentMethod().toString();
}

public static boolean isPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) {
public static boolean isTakerPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) {
return new ReceiptValidator(offer, paymentAccount).isValid();
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/bisq/core/payment/PaymentAccounts.java
Expand Up @@ -39,7 +39,7 @@ class PaymentAccounts {
private final BiFunction<Offer, PaymentAccount, Boolean> validator;

PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService service) {
this(accounts, service, PaymentAccountUtil::isPaymentAccountValidForOffer);
this(accounts, service, PaymentAccountUtil::isTakerPaymentAccountValidForOffer);
}

PaymentAccounts(Set<PaymentAccount> accounts, AccountAgeWitnessService service,
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/bisq/core/payment/payload/PaymentMethod.java
Expand Up @@ -316,4 +316,20 @@ public int compareTo(@NotNull Object other) {
public boolean isAsset() {
return this.equals(BLOCK_CHAINS_INSTANT) || this.equals(BLOCK_CHAINS);
}

public static boolean hasChargebackRisk(PaymentMethod paymentMethod) {
String id = paymentMethod.getId();
return id.equals(PaymentMethod.SEPA_ID) ||
id.equals(PaymentMethod.SEPA_INSTANT_ID) ||
id.equals(PaymentMethod.INTERAC_E_TRANSFER_ID) ||
id.equals(PaymentMethod.CLEAR_X_CHANGE_ID) ||
id.equals(PaymentMethod.REVOLUT_ID) ||
id.equals(PaymentMethod.NATIONAL_BANK_ID) ||
id.equals(PaymentMethod.SAME_BANK_ID) ||
id.equals(PaymentMethod.SPECIFIC_BANKS_ID) ||
id.equals(PaymentMethod.CHASE_QUICK_PAY_ID) ||
id.equals(PaymentMethod.POPMONEY_ID) ||
id.equals(PaymentMethod.MONEY_BEAM_ID) ||
id.equals(PaymentMethod.UPHOLD_ID);
}
}
Expand Up @@ -38,6 +38,7 @@
import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerVerifiesPeersAccountAge;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesAndSignsDepositTx;
import bisq.core.util.Validator;

Expand Down Expand Up @@ -127,6 +128,7 @@ public void handleTakeOfferRequest(TradeMessage tradeMessage, NodeAddress sender
CheckIfPeerIsBanned.class,
MakerVerifyTakerAccount.class,
VerifyPeersAccountAgeWitness.class,
SellerVerifiesPeersAccountAge.class,
MakerVerifyTakerFeePayment.class,
MakerCreateAndSignContract.class,
SellerAsMakerCreatesAndSignsDepositTx.class,
Expand Down
Expand Up @@ -30,6 +30,7 @@
import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndFinalizePayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerVerifiesPeersAccountAge;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs;
import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignAndPublishDepositTx;
import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx;
Expand Down Expand Up @@ -140,6 +141,7 @@ private void handle(PublishDepositTxRequest tradeMessage, NodeAddress sender) {
CheckIfPeerIsBanned.class,
TakerVerifyMakerAccount.class,
VerifyPeersAccountAgeWitness.class,
SellerVerifiesPeersAccountAge.class,
TakerVerifyMakerFeePayment.class,
TakerVerifyAndSignContract.class,
TakerPublishFeeTx.class,
Expand Down
Expand Up @@ -45,7 +45,7 @@ protected void run() {
try {
runInterceptHook();

if (CurrencyUtil.isFiatCurrency(trade.getOffer().getCurrencyCode())) {
if (trade.getOffer() != null && CurrencyUtil.isFiatCurrency(trade.getOffer().getCurrencyCode())) {
final AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
final TradingPeer tradingPeer = processModel.getTradingPeer();
final PaymentAccountPayload peersPaymentAccountPayload = checkNotNull(tradingPeer.getPaymentAccountPayload(),
Expand Down
@@ -0,0 +1,62 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/

package bisq.core.trade.protocol.tasks.seller;

import bisq.core.offer.Offer;
import bisq.core.payment.AccountAgeWitnessService;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;

import bisq.common.taskrunner.TaskRunner;

import java.util.Date;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SellerVerifiesPeersAccountAge extends TradeTask {

@SuppressWarnings({"WeakerAccess", "unused"})
public SellerVerifiesPeersAccountAge(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}

@Override
protected void run() {
try {
runInterceptHook();

Offer offer = trade.getOffer();
if (offer != null && PaymentMethod.hasChargebackRisk(offer.getPaymentMethod())) {
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
long accountCreationDate = new Date().getTime() - accountAgeWitnessService.getTradingPeersAccountAge(trade);
if (accountCreationDate <= AccountAgeWitnessService.SAFE_ACCOUNT_AGE_DATE) {
complete();
} else {
failed("Trade process failed because the buyer's payment account was created after March 15th 2019 and the payment method is considered " +
"risky regarding chargeback risk.");
}
} else {
complete();
}
} catch (Throwable t) {
failed(t);
}
}
}
10 changes: 10 additions & 0 deletions core/src/main/resources/i18n/displayStrings.properties
Expand Up @@ -344,6 +344,16 @@ offerbook.warning.noTradingAccountForCurrency.headline=No trading account for se
offerbook.warning.noTradingAccountForCurrency.msg=You don't have a trading account for the selected currency.\nDo you want to create an offer with one of your existing trading accounts?
offerbook.warning.noMatchingAccount.headline=No matching trading account.
offerbook.warning.noMatchingAccount.msg=You don't have a trading account with the payment method required for that offer.\nYou need to setup a trading account with that payment method if you want to take this offer.\nDo you want to do this now?
offerbook.warning.makerHasNoMatureAccountForBuyOffer=You cannot create an offer because you do not have a payment account which was created before March 15th 2019. \
Copy link
Contributor

Choose a reason for hiding this comment

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

Not show-stopping but I made some minor changes:

offerbook.warning.noMatchingAccount.msg=To take this offer, you will need to set up a payment account using this payment method.\n\nWould you like to do this now?
offerbook.warning.makerHasNoMatureAccountForBuyOffer=You cannot create this offer because you do not have a payment account which was created before March 15th 2019. \
 The selected payment method is considered risky for bank chargebacks. We needed to deploy this restriction as a short-term measure for enhanced security.\n\n\
 The next software release will provide more robust chargeback protection tools, and this restriction for new accounts will be removed.
offerbook.warning.riskyBuyOfferWithImmatureAccountAge=This offer cannot be taken because the maker's payment account \
 was created after March 15th 2019, and the payment method is considered risky for bank chargebacks. We needed to deploy this restriction as a \
 short-term measure for enhanced security.\n\n\
 The next software release will provide more robust protection tools so that offers with this risk profile can be traded again.
offerbook.warning.sellOfferAndAnyTakerPaymentAccountForOfferMature=This offer cannot be taken because your payment account \
 was created after March 15th 2019 and the payment method is considered risky for bank chargebacks. We needed to deploy this restriction as a \
 short-term measure for enhanced security.\n\n\
 The next software release will provide more robust protection tools so that offers with this risk profile can be traded again.

The selected payment method is considered risky regarding bank chargeback and we needed to deploy those restrictions for enhanced security.\n\n\
With the next software release we will provide more protection tools and this restriction for new accounts will be removed.
offerbook.warning.riskyBuyOfferWithImmatureAccountAge=This offer cannot be taken because the maker's payment account \
was created after March 15th 2019 and the payment method is considered risky regarding bank chargeback and we needed to deploy those restrictions for enhanced security.\n\n\
With the next software release we will provide more protection tools so that offers with that risk profile can be traded as well.
offerbook.warning.sellOfferAndAnyTakerPaymentAccountForOfferMature=This offer cannot be taken because your payment account \
was created after March 15th 2019 and the payment method is considered risky regarding bank chargeback and we needed to deploy those restrictions for enhanced security.\n\n\
With the next software release we will provide more protection tools so that offers with that risk profile can be traded as well.

offerbook.warning.wrongTradeProtocol=That offer requires a different protocol version as the one used in your version of the software.\n\nPlease check if you have the latest version installed, otherwise the user who created the offer has used an older version.\n\nUsers cannot trade with an incompatible trade protocol version.
offerbook.warning.userIgnored=You have added that user's onion address to your ignore list.
offerbook.warning.offerBlocked=That offer was blocked by the Bisq developers.\nProbably there is an unhandled bug causing issues when taking that offer.
Expand Down
Expand Up @@ -39,6 +39,7 @@
import bisq.core.payment.HalCashAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.handlers.TransactionResultHandler;
Expand Down Expand Up @@ -85,6 +86,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -254,25 +256,30 @@ public boolean initWithData(OfferPayload.Direction direction, TradeCurrency trad

fillPaymentAccounts();

PaymentAccount account;
PaymentAccount account = null;

PaymentAccount lastSelectedPaymentAccount = getPreselectedPaymentAccount();
if (lastSelectedPaymentAccount != null &&
lastSelectedPaymentAccount.getTradeCurrencies().contains(tradeCurrency) &&
user.getPaymentAccounts() != null &&
user.getPaymentAccounts().stream().anyMatch(paymentAccount -> paymentAccount.getId().equals(lastSelectedPaymentAccount.getId()))) {
user.getPaymentAccounts().stream().anyMatch(paymentAccount -> paymentAccount.getId().equals(lastSelectedPaymentAccount.getId())) &&
isPaymentAccountMatureForBuyOffer(lastSelectedPaymentAccount)) {
account = lastSelectedPaymentAccount;
} else {
account = user.findFirstPaymentAccountWithCurrency(tradeCurrency);
PaymentAccount firstPaymentAccountWithCurrency = user.findFirstPaymentAccountWithCurrency(tradeCurrency);
if (firstPaymentAccountWithCurrency != null && isPaymentAccountMatureForBuyOffer(firstPaymentAccountWithCurrency)) {
account = firstPaymentAccountWithCurrency;
}
}

if (account != null) {
this.paymentAccount = account;
} else {
Optional<PaymentAccount> paymentAccountOptional = paymentAccounts.stream().findAny();
if (paymentAccountOptional.isPresent()) {
this.paymentAccount = paymentAccountOptional.get();

PaymentAccount anyPaymentAccount = paymentAccountOptional.get();
if (isPaymentAccountMatureForBuyOffer(anyPaymentAccount))
this.paymentAccount = anyPaymentAccount;
} else {
log.warn("PaymentAccount not available. Should never get called as in offer view you should not be able to open a create offer view");
return false;
Expand Down Expand Up @@ -426,7 +433,7 @@ void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) {
}

void onPaymentAccountSelected(PaymentAccount paymentAccount) {
if (paymentAccount != null && !this.paymentAccount.equals(paymentAccount)) {
if (paymentAccount != null && !this.paymentAccount.equals(paymentAccount) && isPaymentAccountMatureForBuyOffer(paymentAccount)) {
volume.set(null);
minVolume.set(null);
price.set(null);
Expand All @@ -444,6 +451,12 @@ void onPaymentAccountSelected(PaymentAccount paymentAccount) {
}
}

private boolean isPaymentAccountMatureForBuyOffer(PaymentAccount paymentAccount) {
return direction == OfferPayload.Direction.SELL ||
!PaymentMethod.hasChargebackRisk(paymentAccount.getPaymentMethod()) ||
new Date().getTime() - accountAgeWitnessService.getMyAccountAge(paymentAccount.getPaymentAccountPayload()) <= AccountAgeWitnessService.SAFE_ACCOUNT_AGE_DATE;
}

private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) {
if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) {
if (paymentAccount.getSelectedTradeCurrency() != null)
Expand Down Expand Up @@ -694,8 +707,11 @@ public void swapTradeToSavings() {
}

private void fillPaymentAccounts() {
if (user.getPaymentAccounts() != null)
paymentAccounts.setAll(new HashSet<>(user.getPaymentAccounts()));
if (user.getPaymentAccounts() != null) {
paymentAccounts.setAll(new HashSet<>(user.getPaymentAccounts().stream()
.filter(this::isPaymentAccountMatureForBuyOffer)
.collect(Collectors.toSet())));
}
}

protected void setAmount(Coin amount) {
Expand Down