-
Notifications
You must be signed in to change notification settings - Fork 207
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
IBC Rate Limiting Audit Fixes #818
Conversation
a07f1cb
to
1e5cbd8
Compare
switch { | ||
// withdrawal address | ||
case portID == withdrawalAddress: | ||
zoneInfo.WithdrawalAccount = &types.ICAAccount{Address: address, Target: types.ICAAccountType_WITHDRAWAL} |
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.
tiniest pico nit (definitely don't need to change) I find it slightly cleaner to repeat im.keeper.RatelimitKeeper.AddAddressToWhitelist(ctx, address)
on each line
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.
done
func (k Keeper) SetPendingSendPacket(ctx sdk.Context, channelId string, sequence uint64) { | ||
store := prefix.NewStore(ctx.KVStore(k.storeKey), types.PendingSendPacketPrefix) | ||
key := types.GetPendingSendPacketKey(channelId, sequence) | ||
store.Set(key, []byte{1}) |
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's the []byte{1}
needed 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.
All we really need to store is the key, the value doesn't matter as long as it's non-empty.
The implementation can be thought of more as a a set in python, rather than a map
x/ratelimit/keeper/rate_limit.go
Outdated
|
||
// Check if the sender or receiver are white listed | ||
// If so, return a success without modifying the quota | ||
if k.IsAddressWhitelisted(ctx, packetInfo.Sender) || k.IsAddressWhitelisted(ctx, packetInfo.Receiver) { |
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.
if k.IsAddressWhitelisted(ctx, packetInfo.Sender) || k.IsAddressWhitelisted(ctx, packetInfo.Receiver) { | |
if k.IsAddressWhitelisted(ctx, packetInfo.Sender) && direction == types.PACKET_SEND || k.IsAddressWhitelisted(ctx, packetInfo.Receiver) && directino == types.PACKET_RECEIVE { |
I think this condition is more specific, it's unnecessarily broad right now (e.g. sending to stride's module account address on a counterparty chain would be whitelisted)
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.
done
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.
@asalzmann thinking about this a bit more, I'm wondering if we should just remove the ICA accounts from the whitelist now that there's the extra clause that specifies the packet type?
The epochly transfer would be a SEND_PACKET with Sender == module account, and the prop 8 transfer would be a RECV_PACKET with Receiver == module account. I'm not sure there's any other cases to worry about
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 seems cleaner to me. Agree that those are the only two cases. Note: I believe prop 8 rev goes into a separate module account, that will need to be whitelisted.
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.
Actually an even slightly safer design imo would be
Whitelist
- Both must be true: (SENDER == DELEGATION_ICA) && (RECEIVER == MAIN STAKEIBC MODULE ACCOUNT)
- Both must be true: (SENDER == WITHDRAWAL_ICA) && (RECEIVER == PROP8 STAKEIBC MODULE ACCOUNT)
With the "either or" whitelist approach and whitelisting the main stakeibc mod acct and the delegation ICA, I think we've disabled rate limits along these paths:
- transfers from stakeibc module account to malicious cosmoshub address
- transfers from delegation acct to malicious stride address
} | ||
|
||
// If the ack was successful, remove the pending packet | ||
if ackResponse.Status == icacallbacktypes.AckResponseStatus_SUCCESS { |
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.
Should this check if the packet exists before removing it? e.g. what if it's just a normal ack (from a channel that has no rate limit)
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 stored pending packets is only used for rate limiting to determine if the packet was sent in the same rate limit window. This code isn't deleting the actual packet, just the record that it was sent this window. It has no affect on other modules
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 makes sense, but I think we only set the pending packet if we update a flow
if updatedFlow {
k.SetPendingSendPacket(ctx, packetInfo.ChannelID, packet.Sequence)
}
However, this will always try and delete a packet (unless I'm missing something). If you call Delete
on a key that doesn't exist, does it throw an error?
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.
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.
Gotcha! Makes sense, either way seems fine to me
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.
cool, I'll just leave it as is
Added the testing instructions to the PR description |
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.
Sorry @riley-stride, just noticed this! I don't think autopilot LS uses a module account - I think it sends it to the user's stride address |
x/ratelimit/keeper/rate_limit.go
Outdated
store.Delete(key) | ||
} | ||
|
||
// Check if an address is currently whitelisted | ||
func (k Keeper) IsAddressWhitelisted(ctx sdk.Context, address string) bool { | ||
// Check if an address pair is currently whitelisted |
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.
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.
Hmm that's a good point. But I think Address/Account should still be specified, otherwise Route
may easily be conflated with a channel, and Path
is already used to describe the channel
@@ -49,3 +49,8 @@ message RateLimit { | |||
Quota quota = 2; | |||
Flow flow = 3; | |||
} | |||
|
|||
message WhitelistedAddressPair { |
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 think we make an assumption here that only one of the two addresses will use the stride...
address space. Otherwise, the address pair will whitelist two paths. E.g. for (address-A, address-B), the following paths are valid
From Stride
address-A -> address-B
To Stride
address-A -> address-B
Solutions
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.
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.
Sgtm - should we just add a comment above the data structure linking to this discussion?
Context and purpose of the change
This PR addresses two issues from the informal audit (note these are in a private repo):
Diagram
Excalidraw
Issue #1 - Rolling back outflow
Overview
Currently when packets are sent, the outflow is incremented by the transfer amount; however, if the packet fails or times out, it is not decremented. This opens the door for someone to clog up the rate limit by sending packets with a short timeout.
Solution
The obvious solution here is to decrement the outflow during the timeout or ack failure. But the complexity lies in that we need to make sure that we're in the same rate limit time window as the original packet send, otherwise the outflow will have already been reset to 0 and any additional decrement would be incorrect. In other words, if the rate limit was reset before the packet response was relayed (but after it was sent), then the outflow should not be modified during the ack.
Implementation
This was implemented by saving down the sequence numbers of packets that have been sent in the given rate limit window, and then checking for the presence of that sequence number when handling the acknowledgment or timeout. More specifically, the high level changes were:
Changelog
UndoSendPacket
which checks if the packet's sequence number exists in the pending store (i.e. the packet was sent in the current time window), and if so, decrements the outflowSetPendingSendPacket
RemovePendingSendPacket
CheckPacketSendDuringCurrentQuota
RemoveAllChannelPendingSendPackets
SendRateLimitedPacket
to store a pending packetResetRateLimit
to remove all pending packetsTimeoutRateLimitedPacket
which callsUndoSendPacket
AcknowledgeRateLimitedPacket
which checks if the ack failed, and if so, callsUndoSendPacket
ParsePacketInfo
to group some redundant packet parsing logic that was required in each IBC callbackIssue #2 - Channel bottlenecks
Overview
In Stride's stakeibc flow, a single transfer is issued every epoch with all the aggregate liquid stake deposits from the previous epoch window. This transfer is large enough that it would likely exceed the rate limit quota depending on how strict it was set.
Solution
Transfers to or from Stride module account's or ICA's should be whitelisted such that they are not included in the
Inflow/Outflow
calculation and cannot be rate limited.Implementation
This was implemented by adding a list of whitelisted addresses to the store, and adding a check to Send/Receive packet for whether the sender or receiver is in the list of addresses.
Changelog
AddAddressToWhitelist
RemoveAddressFromWhitelist
IsAddressWhitelisted
GetAllWhitelistedAddresses
CheckRateLimitAndUpdateFlow
to check if the sender or receiver is in the whitelist, and if so, allows the packet to pass without updating the flowAdditional Misc Changelog
Testing
Ack Failure/Timeout
(GAIA JUNO OSMO)
in config.shdockernet/scripts/ratelimit/run_all_tests.sh
and comment out all the tests besides the last one "test_send_failures"ustrd
rate limitAddress Whitelist