Open hats-bug-reporter[bot] opened 1 year ago
@9olidity please complete your submission with a comment here. The submission was not 100% submitted.
function _redeemTicketInternal(
address self,
RedeemableTicket calldata redeemable,
HoprCrypto.VRFParameters calldata params
)
internal
validateBalance(redeemable.data.amount)
HoprCrypto.isFieldElement(redeemable.porSecret)
{
Channel storage spendingChannel = channels[redeemable.data.channelId];
if (spendingChannel.status != ChannelStatus.OPEN && spendingChannel.status != ChannelStatus.PENDING_TO_CLOSE) {
revert WrongChannelState({ reason: "spending channel must be OPEN or PENDING_TO_CLOSE" });
}
if (ChannelEpoch.unwrap(spendingChannel.epoch) != ChannelEpoch.unwrap(redeemable.data.epoch)) {
revert WrongChannelState({ reason: "channel epoch must match" });
}
// Aggregatable Tickets - validity interval:
// A ticket has a base index and an offset. The offset must be > 0,
// while the base index must be >= the currently set ticket index in the
// channel.
uint48 baseIndex = TicketIndex.unwrap(redeemable.data.ticketIndex);
uint32 baseIndexOffset = TicketIndexOffset.unwrap(redeemable.data.indexOffset);
uint48 currentIndex = TicketIndex.unwrap(spendingChannel.ticketIndex);
if (baseIndexOffset < 1 || baseIndex < currentIndex) {
revert InvalidAggregatedTicketInterval();
}
if (Balance.unwrap(spendingChannel.balance) < Balance.unwrap(redeemable.data.amount)) {
revert InsufficientChannelBalance();
}
// Deviates from EIP712 due to computed property and non-standard struct property encoding
bytes32 ticketHash = _getTicketHash(redeemable);
if (!_isWinningTicket(ticketHash, redeemable, params)) {
revert TicketIsNotAWin();
}
HoprCrypto.VRFPayload memory payload =
HoprCrypto.VRFPayload(ticketHash, self, abi.encodePacked(domainSeparator));
if (!vrfVerify(params, payload)) {
revert InvalidVRFProof();
}
address source = ECDSA.recover(ticketHash, redeemable.signature.r, redeemable.signature.vs);
+ require(source != self);
if (_getChannelId(source, self) != redeemable.data.channelId) {
revert InvalidTicketSignature();
}
spendingChannel.ticketIndex = TicketIndex.wrap(baseIndex + baseIndexOffset);
spendingChannel.balance =
Balance.wrap(Balance.unwrap(spendingChannel.balance) - Balance.unwrap(redeemable.data.amount));
indexEvent(
abi.encodePacked(ChannelBalanceDecreased.selector, redeemable.data.channelId, spendingChannel.balance)
);
emit ChannelBalanceDecreased(redeemable.data.channelId, spendingChannel.balance);
bytes32 outgoingChannelId = _getChannelId(self, source);
Channel storage earningChannel = channels[outgoingChannelId];
// Informs about new ticketIndex
indexEvent(abi.encodePacked(TicketRedeemed.selector, redeemable.data.channelId, spendingChannel.ticketIndex));
emit TicketRedeemed(redeemable.data.channelId, spendingChannel.ticketIndex);
if (earningChannel.status == ChannelStatus.CLOSED) {
// The other channel does not exist, so we need to transfer funds directly
if (token.transfer(msg.sender, Balance.unwrap(redeemable.data.amount)) != true) {
revert TokenTransferFailed();
}
} else {
// this CAN produce channels with more stake than MAX_USED_AMOUNT - which does not lead
// to overflows since total supply < type(uin96).max
earningChannel.balance =
Balance.wrap(Balance.unwrap(earningChannel.balance) + Balance.unwrap(redeemable.data.amount));
indexEvent(abi.encodePacked(ChannelBalanceIncreased.selector, outgoingChannelId, earningChannel.balance));
emit ChannelBalanceIncreased(outgoingChannelId, earningChannel.balance);
}
}
It's forbidden to open a channel where src
is equal to dest
. Therefore it's expected that redeemTicket()
is not allowed in this case.
@QYuQianchen There is no such restriction in the code, so I think this vulnerability is valid
There is. See usage and definition
Github username: @9olidity Submission hash (on-chain): 0x7ec10f183a2c176578f059a0e81592ca36c1e6f1c377ec067808fcc5f6ee0438 Severity: medium
Description: Description\ When
src==dest
,redeemTicket()
will become unusable.Attack Scenario\
When executing
_redeemTicketInternal
, the relationship betweenself
andsource
will be checked, but there is no restriction in the code here thatself
is equal tosource
. Whenself == source
is equal, the first execution of_redeemTicketInternal
can be executed normally, but the second When calling_redeemTicketInternal
, anInvalidAggregatedTicketInterval
error occurs andredeemTicket()
cannot execute normally.Attachments
forge test --match-test test_sameredeemTicket -vvvv
Revised Code File (Optional)