Skip to content

Commit 19e3a54

Browse files
fix(thorchain): enhance validation for from_amount to prevent leading zeros and overflow
1 parent f66e3dc commit 19e3a54

2 files changed

Lines changed: 73 additions & 6 deletions

File tree

src/THORChain/Swap.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <algorithm>
2828
#include <cctype>
2929
#include <cstdlib>
30+
#include <limits>
3031

3132
/*
3233
* References:
@@ -35,13 +36,20 @@
3536

3637
namespace TW::THORChainSwap {
3738

38-
/// Returns true if the string is a non-empty sequence of decimal digits only.
39-
/// Used to reject non-numeric amount fields before they reach uint256_t()/std::stoull(),
40-
/// both of which throw on malformed input.
39+
/// Returns true if the string is a canonical decimal uint256_t (no leading zeros, e.g. "0500"
40+
/// would otherwise be misparsed as octal 320) that doesn't exceed uint256_t's max (which would
41+
/// otherwise silently wrap around).
4142
static bool isValidUInt(const std::string& value) {
42-
return !value.empty() && std::all_of(value.begin(), value.end(), [](unsigned char c) {
43-
return std::isdigit(c) != 0;
44-
});
43+
if (value.empty() || !std::all_of(value.begin(), value.end(), [](unsigned char c) {
44+
return std::isdigit(c) != 0;
45+
})) {
46+
return false;
47+
}
48+
if (value.size() > 1 && value.front() == '0') {
49+
return false;
50+
}
51+
using boost::multiprecision::cpp_int;
52+
return cpp_int(value) <= cpp_int(std::numeric_limits<uint256_t>::max());
4553
}
4654

4755
static Data ethAddressStringToData(const std::string& asString) {

tests/chains/Cosmos/THORChain/TWSwapTests.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,65 @@ TEST(TWTHORChainSwap, NegativeOverflowToAmountLimit) {
467467
EXPECT_EQ(outputProto.error().code(), Proto::ErrorCode::Error_general);
468468
EXPECT_FALSE(outputProto.has_bitcoin());
469469
}
470+
471+
// Regression: a from_amount with a leading zero used to pass the digit-only pre-check and then
472+
// be misparsed as octal by uint256_t's string constructor (e.g. "0500" -> 320), silently
473+
// building a swap for the wrong amount. It must now be rejected outright.
474+
TEST(TWTHORChainSwap, NegativeLeadingZeroFromAmount) {
475+
Proto::SwapInput input;
476+
Proto::Asset fromAsset;
477+
fromAsset.set_chain(Proto::BTC);
478+
*input.mutable_from_asset() = fromAsset;
479+
input.set_from_address(Address1Btc);
480+
Proto::Asset toAsset;
481+
toAsset.set_chain(Proto::ETH);
482+
toAsset.set_symbol("ETH");
483+
*input.mutable_to_asset() = toAsset;
484+
input.set_to_address(Address1Eth);
485+
input.set_vault_address(VaultBtc);
486+
input.set_from_amount("0500");
487+
input.set_to_amount_limit("140000000000000000");
488+
489+
const auto inputData_ = input.SerializeAsString();
490+
const auto inputTWData_ = WRAPD(TWDataCreateWithBytes((const uint8_t*)inputData_.data(), inputData_.size()));
491+
492+
const auto outputTWData_ = WRAPD(TWTHORChainSwapBuildSwap(inputTWData_.get()));
493+
const auto outputData = data(TWDataBytes(outputTWData_.get()), TWDataSize(outputTWData_.get()));
494+
Proto::SwapOutput outputProto;
495+
ASSERT_TRUE(outputProto.ParseFromArray(outputData.data(), static_cast<int>(outputData.size())));
496+
EXPECT_EQ(outputProto.error().code(), Proto::ErrorCode::Error_general);
497+
EXPECT_FALSE(outputProto.has_bitcoin());
498+
}
499+
500+
// Regression: an all-digit from_amount larger than 2^256-1 used to pass the digit-only
501+
// pre-check and then silently wrap around inside uint256_t's fixed-width arithmetic, building
502+
// a swap for an arbitrary, unrelated amount. It must now be rejected outright.
503+
TEST(TWTHORChainSwap, NegativeOverflowFromAmount) {
504+
Proto::SwapInput input;
505+
Proto::Asset fromAsset;
506+
fromAsset.set_chain(Proto::BTC);
507+
*input.mutable_from_asset() = fromAsset;
508+
input.set_from_address(Address1Btc);
509+
Proto::Asset toAsset;
510+
toAsset.set_chain(Proto::ETH);
511+
toAsset.set_symbol("ETH");
512+
*input.mutable_to_asset() = toAsset;
513+
input.set_to_address(Address1Eth);
514+
input.set_vault_address(VaultBtc);
515+
// 2^256 == 115792089237316195423570985008687907853269984665640564039457584007913129639936
516+
input.set_from_amount("115792089237316195423570985008687907853269984665640564039457584007913129639936");
517+
input.set_to_amount_limit("140000000000000000");
518+
519+
const auto inputData_ = input.SerializeAsString();
520+
const auto inputTWData_ = WRAPD(TWDataCreateWithBytes((const uint8_t*)inputData_.data(), inputData_.size()));
521+
522+
const auto outputTWData_ = WRAPD(TWTHORChainSwapBuildSwap(inputTWData_.get()));
523+
const auto outputData = data(TWDataBytes(outputTWData_.get()), TWDataSize(outputTWData_.get()));
524+
Proto::SwapOutput outputProto;
525+
ASSERT_TRUE(outputProto.ParseFromArray(outputData.data(), static_cast<int>(outputData.size())));
526+
EXPECT_EQ(outputProto.error().code(), Proto::ErrorCode::Error_general);
527+
EXPECT_FALSE(outputProto.has_bitcoin());
528+
}
470529
// clang-format on
471530

472531
} // namespace TW::ThorChainSwap::tests

0 commit comments

Comments
 (0)