-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat(ics20): receive foreign tokens #25
Conversation
src/ICS20Transfer.sol
Outdated
returns (ICS20Lib.SendPacketData memory) | ||
{ | ||
ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(packet.data); | ||
ICS20Lib.SendPacketData memory receivePacketData = ICS20Lib.SendPacketData({ |
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 just wanted to mention why this is being used instead of local variables further down. I implemented this first with a lot of local variables to make things as readable and nice as I could, but encountered some "stack too deep" issue because there were too many local variables. So I ended up using the return value as a way to avoid that issue.
There are a couple of alternatives to this that can be considered:
- Splitting up the function more
- Using brackets to scope temporary variables along the way so they don't affect the stack too much (can be a bit ugly, but might be more readable than the current way).
Might be some other way to optimise this, but those were the options I could see, at least.
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'm ok with introducing a new function for these purposes. But not ok with introducing new structs ICS20Lib.SendPacketData
and ICS20Lib.ReceivePacketData
unless we absolutely have to. This is a large addition to complexity that we should remove imo.
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.
Yeah, I understand what you mean. The main reason I ended up with doing that is because of the difference in addresses/string between sender and receiver and figuring out source/not source being kind of opposite. The other option is to only use the ICS20Lib.PacketDataJSON, or add a single slightly expanded version that keeps all fields the same (but would have to extract sender/receiver addresses when they are needed.
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.
Why can't these addresses be just returned as additional return arguments instead of whole structs?
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.
Consider guys that I if we have the stack too deep problem, if we really need the variables the only real solution we have is to split the logic over multiple contract/libraries.
- Overall I agree we should always try to keep things as minimal as we can IF we can.
- We should use nat spec (just give the function to gpt-4 or whatever AI you use and ask for it, don't spend time doing it manually)
- We should create a specific library to handle this unwrapping things
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 is also an option. Will look into it and see what makes most sense
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.
Thanks a lot, this is a good start.
|
||
emit ICS20Transfer(packetData); | ||
} | ||
|
||
function onRecvPacket(OnRecvPacketCallback calldata msg_) external onlyOwner nonReentrant returns (bytes memory) { | ||
// TODO Emit error event | ||
// Since this function mostly returns acks, also when it fails, the ics26router (the caller) will log the ack | ||
if (keccak256(abi.encodePacked(msg_.packet.version)) != keccak256(abi.encodePacked(ICS20Lib.ICS20_VERSION))) { | ||
return ICS20Lib.errorAck(abi.encodePacked("unexpected version: ", msg_.packet.version)); |
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 actually think maybe we should just revert here, since processing an unknown version could be dangerous. Perhaps ask Aditya
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.
Since this is the first check and it returns I was thinking it was OK to just return the ack. I have tried to follow the logic of the ibc-go implementation for acks, where they return errors and in the top level handler an error is just turned into an error ack:
if ackErr = im.keeper.OnRecvPacket(ctx, packet, data); ackErr != nil {
ack = channeltypes.NewErrorAcknowledgement(ackErr)
im.keeper.Logger(ctx).Error(fmt.Sprintf("%s sequence %d", ackErr.Error(), packet.Sequence))
return ack
}
But happy to get some more input from Aditya as well :)
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.
So a revert would revert the entire tx correct?
In this case, that would leave a second relayer open to submitting the exact same packet again and reaching the same revert.
This is why we return error acks on application error. That way we have a record of processing the packet and the error ack can be processed by the other side. It prevents infinite replays of the same error and also we want to give the sender chain an error ack saying your version is invalid rather than just having it timeout correct?
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.
Are there any scenarios where we would want to revert the entire tx? If not, would it be better revert in ICS20Transfer (instead of returning the ack) and catch any errors in the router? It is a bit ugly, but some reverts might be hard to avoid without adding custom error returns and handling everywhere else.
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.
Inside the ICS20 recv callback? No i don't think so.
Because at that point the packet was a valid receive from the IBC protocol standpoint. So then its just up to the application to tell us whether this packet was processed correctly or not, which it should communicate in the form of an error ack.
Basically:
Error in IBC core recv packet handler -> revert Tx
Error in ICS20 app onRecvPacket callback -> error ACK and commit tx
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; | ||
|
||
contract IBCERC20 is ERC20, Ownable { | ||
// TODO: Figure out naming and symbol for IBC denoms |
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 denom should be IBC denom that we use in ibc-go. In IBC-go, we also have a name for the tokens (in metadata). So we should just copy that logic from there.
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.
You mean the ibc/{hash(trace+base_denom)} think we use for the denom? We can do that here too, for sure. I just wonder if it is necessary or provide a lot of value, compared to actually having the trace available directly in the name (less hashing to do, less lookups for both us and others).
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.
We must do it here since spec says MUST.
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 spec only says It must encode the trace and base denomination to ensure that tokens coming over different paths are not treated as fungible.
, which the full denom does, but it takes more space than a hash.
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 see, but still, these minted tokens only have the name and symbol "IBC". We need to encode the info in the name and symbol right?
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'd actually propose we use ibc-go's implementation regardless as it has been battle tested for so long. I don't want us to do our own implementation and mess something up with some type of collision or a denom that contains slashes or something like that.
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.
Yes, we do need to add the info there. We could add the full denom as the name, as it might be harder to look up the full trace (given that the erc20 itself doesn't contain any link back to the ICS20Transfer contract where that information would most likely be). We could also use a hash as the name and have a separate denom or even trace with hops field like in the new Denom struct.
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 symbol should be fine, will do what the ibc-go implementation does
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.
"mess something up with some type of collision or a denom that contains slashes or something like that."
Note that in the EVM world we have to consider that we may event different hash collision problems to consider. Another note for future thoughts and audits.
src/ICS20Transfer.sol
Outdated
returns (ICS20Lib.SendPacketData memory) | ||
{ | ||
ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(packet.data); | ||
ICS20Lib.SendPacketData memory receivePacketData = ICS20Lib.SendPacketData({ |
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'm ok with introducing a new function for these purposes. But not ok with introducing new structs ICS20Lib.SendPacketData
and ICS20Lib.ReceivePacketData
unless we absolutely have to. This is a large addition to complexity that we should remove imo.
src/ICS20Transfer.sol
Outdated
@@ -168,4 +153,124 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr | |||
revert ICS20UnexpectedERC20Balance(expectedEndingBalance, actualEndingBalance); | |||
} | |||
} | |||
|
|||
function _unwrapSendPacketData(IICS26RouterMsgs.Packet calldata packet) |
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.
Function should probably be moved.
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.
Currently it cannot (at least not to ICS20Lib), as it depends on looking up _foreignDenomContracts. I suppose we could try to move most of the logic over and do contract dependent stuff here.
src/ICS20Transfer.sol
Outdated
@@ -168,4 +153,124 @@ contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, Reentr | |||
revert ICS20UnexpectedERC20Balance(expectedEndingBalance, actualEndingBalance); | |||
} | |||
} | |||
|
|||
function _unwrapSendPacketData(IICS26RouterMsgs.Packet calldata packet) |
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.
Please add natspec and link to the function it's based on from ibc-go or yui-ibc-solidity
src/ICS20Transfer.sol
Outdated
return receivePacketData; | ||
} | ||
|
||
function _unwrapReceivePacketData(IICS26RouterMsgs.Packet calldata packet) |
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.
Same as above:
- Should be moved
- Should have natspec
- Shouldn't introduce new structs
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.
Same comment as above.
Natspec part I will do on both :) pending decision on the other
src/ICS20Transfer.sol
Outdated
|
||
function _unwrapReceivePacketData(IICS26RouterMsgs.Packet calldata packet) | ||
private | ||
returns (ICS20Lib.ReceivePacketData memory, bytes memory) |
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 feel a bit uneasy using bytes as ack/error handling. What if we allow this function to revert if it fails, and it can be try catch'ed above.
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.
Yeah, I am not happy about it either.
Try/catch would be nice, but it is not possible to try/catch an internal function in Solidity. We could change the router to try/catch instead, but it might be a little weird also.
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 try-catch shouldn't be in the router imo. We could keep this for now unless there isn't a better error handling pattern
// we are not the source of this token, so we add a denom trace and find or create a token contract | ||
receivePacketData.senderChainIsSource = true; | ||
bytes memory newDenomPrefix = ICS20Lib.getDenomPrefix(packet.destPort, packet.destChannel); | ||
receivePacketData.denom = string(abi.encodePacked(newDenomPrefix, packetData.denom)); |
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, shouldn't we be following ICS20 specs to determine the IBCDenom?
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, what do you mean? Are we not?
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.
Do you mean the ibc/{hash(trace+base_denom)} stuff? It is not really spec, but an implementation detail of ibc-go. But we could certainly do the same here if we want to.
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.
It is a MUST part of the spec
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.
Not from how I read it. From what I can tell the spec only dictates that the denom is 1. unique and 2. possible to reconstruct into the full denom. Having the full denom does both.
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 should try to keep consistency between implementation unless we have a reason for not doing that e.g. gas consumption convenience .
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 you're right. I'd still propose we use ibc-go's implementation regardless as it has been battle tested for so long. I don't want us to do our own implementation and mess something up with some type of collision or a denom that contains slashes or something like that.
src/utils/ICS20Lib.sol
Outdated
/// @dev SendPacketData is a type used to represent the data needed to send a packet. | ||
struct SendPacketData { | ||
string denom; | ||
bool receiverChainIsSource; | ||
address erc20Contract; | ||
address sender; | ||
string receiver; | ||
uint256 amount; | ||
string memo; | ||
} | ||
|
||
/// @dev SendPacketData is a type used to represent the data needed to send a packet. | ||
struct ReceivePacketData { | ||
string denom; | ||
bool senderChainIsSource; | ||
address erc20Contract; | ||
string sender; | ||
address receiver; | ||
uint256 amount; | ||
string memo; | ||
} |
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.
We shouldn't introduce new structs. I am not convinced this is needed. We can add the extra bool as a return arguments to the functions that produce these structs.
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.
Yeah, the only real difference right now (except the bool, which you mentioned) is the address difference for sender and receiver. I can look into what it would look like to have a single struct and do the respective sender receiver unpacking in each method.
Thanks for the detailed review @srdtrk, really good stuff! I left some comments and we can hash (no put intended) out the details 💡 |
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.
Thanks @gjermundgaraba for all this work! Really great even the use of testFuzz! I left some comments, but if we need this to implement to complete workflow, we can evaluate not to address them immediately.
if (packetData.receiverChainIsSource) { | ||
// receiver chain is source: burn the vouchers | ||
// TODO: Implement escrow balance tracking (#6) | ||
IBCERC20 ibcERC20Contract = IBCERC20(packetData.erc20Contract); |
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.
Wondering if we should check the balance before and after the transfer before the burn. Let's say if we have a to deal with a malicious implementation of _transferFrom this may protect us. But it's more a thought future audits.
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 _transferFrom
that happens before the burn has that check already, actually, and the burn itself is safe as it only happens in our own ERC20 contract.
function _transferFrom(address sender, address receiver, address tokenContract, uint256 amount) private {
// we snapshot our current balance of this token
uint256 ourStartingBalance = IERC20(tokenContract).balanceOf(receiver);
IERC20(tokenContract).safeTransferFrom(sender, receiver, amount);
// check what this particular ERC20 implementation actually gave us, since it doesn't
// have to be at all related to the _amount
uint256 actualEndingBalance = IERC20(tokenContract).balanceOf(receiver);
uint256 expectedEndingBalance = ourStartingBalance + amount;
// a very strange ERC20 may trigger this condition, if we didn't have this we would
// underflow, so it's mostly just an error message printer
if (actualEndingBalance <= ourStartingBalance || actualEndingBalance != expectedEndingBalance) {
revert ICS20UnexpectedERC20Balance(expectedEndingBalance, actualEndingBalance);
}
}
src/ICS20Transfer.sol
Outdated
returns (ICS20Lib.SendPacketData memory) | ||
{ | ||
ICS20Lib.PacketDataJSON memory packetData = ICS20Lib.unmarshalJSON(packet.data); | ||
ICS20Lib.SendPacketData memory receivePacketData = ICS20Lib.SendPacketData({ |
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.
Consider guys that I if we have the stack too deep problem, if we really need the variables the only real solution we have is to split the logic over multiple contract/libraries.
- Overall I agree we should always try to keep things as minimal as we can IF we can.
- We should use nat spec (just give the function to gpt-4 or whatever AI you use and ask for it, don't spend time doing it manually)
- We should create a specific library to handle this unwrapping things
// we are not the source of this token, so we add a denom trace and find or create a token contract | ||
receivePacketData.senderChainIsSource = true; | ||
bytes memory newDenomPrefix = ICS20Lib.getDenomPrefix(packet.destPort, packet.destChannel); | ||
receivePacketData.denom = string(abi.encodePacked(newDenomPrefix, packetData.denom)); |
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 should try to keep consistency between implementation unless we have a reason for not doing that e.g. gas consumption convenience .
src/interfaces/IICS20Transfer.sol
Outdated
@@ -7,22 +7,21 @@ import { IICS20TransferMsgs } from "../msgs/IICS20TransferMsgs.sol"; | |||
interface IICS20Transfer is IICS20TransferMsgs { | |||
/// @notice Called when a packet is handled in onSendPacket and a transfer has been initiated | |||
/// @param packetData The transfer packet data | |||
event ICS20Transfer(ICS20Lib.UnwrappedFungibleTokenPacketData packetData); | |||
event ICS20Transfer(ICS20Lib.SendPacketData packetData); |
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's a general comment. As we have specific files for errors, I have seen many projects having the specific files for events too. We should consider it.
@@ -602,6 +603,305 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { | |||
})) | |||
} | |||
|
|||
func (s *IbcEurekaTestSuite) TestICS20TransferWithForeignCoin() { |
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.
Add this test to CI please
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.
Ah, forgot that needed to be done manually, will do!
@@ -60,6 +60,7 @@ type IbcEurekaTestSuite struct { | |||
|
|||
contractAddresses e2esuite.DeployedContracts | |||
|
|||
ethClient *ethclient.Client |
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 there is no good reason to move this up, I'd rather not do it. Keeping the test suite simple is a good idea in general. We can define a new client with client, err := ethclient.Dial(eth.GetHostRPCAddress())
whenever need be.
This change also doesn't have anything to do with the reason for this PR. You should only do this if this very much helps you write your new e2e test, which doesn't seem to be the case
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.
OK. It was used in the new test, but I agree that it is better to keep the suite simpler 👍
@@ -602,6 +603,305 @@ func (s *IbcEurekaTestSuite) TestICS20Transfer() { | |||
})) | |||
} | |||
|
|||
func (s *IbcEurekaTestSuite) TestICS20TransferWithForeignCoin() { |
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.
It is native to cosmos in a way. So maybe we can come up with another name?
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.
It doesn't strictly have to be, but it is in this case - for sure!
What about TestICS20TransferWithCosmosNativeCoin
?
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.
Maybe TestICS20TransferNativeSdkCoin
?
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 would be consistent and nice :) Will rename to that!
TimeoutTimestamp: returnPacket.TimeoutTimestamp * 1_000_000_000, | ||
}, | ||
ProofCommitment: []byte("doesn't matter"), | ||
ProofHeight: clientState.LatestHeight, |
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 mock client checks latest height?
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.
Nope :) fixing this
src/ICS20Transfer.sol
Outdated
|
||
if (packetData.amount == 0) { | ||
revert ICS20InvalidAmount(packetData.amount); | ||
} |
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.
Can ypu create a issue to create validation functions for the structs that we define? So that we don't need to write these by hand each time
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 #30
// transfer the tokens to the receiver | ||
IERC20(packetData.erc20Contract).safeTransfer(receiver, packetData.amount); |
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 we return an error ack if this call fails?
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.
Yes, that would probably make the most sense. The reason it is not done right now is because with safeTransfer, which is a Library, we can't do try/catch (because it is technically not an external call right here). There is a little "hack" we can do instead, by wrapping the transfer in an external function and calling it with this.wrappedSafeTransfer. If you prefer that I can go ahead and implement that :)
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.
How is this not an external call? It seems we are calling packetData.erc20Contract
?
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.
Note that the mint
function is being invoked on a contract at packetData.erc20Contract
, which is external to the current contract executing the onRecvPacket
function.
A typical way to handle external calls is to check if they succeed or not.
Kind like: bool success: IERC20(packetData.erc20Contract).safeTransfer(receiver, packetData.amount);
and then check the return value of 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.
I know, but since the safeTransfer is an actually a library wrapper, we are not the doing the external call here. The safeTransfer library is doing it :P So we don't get an external call to try/catch, nor a return value.
Error (2536): Try can only be used with external function calls and contract creation calls.
--> src/ICS20Transfer.sol:121:13:
|
121 | try IERC20(packetData.erc20Contract).safeTransfer(receiver, packetData.amount) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||
receivePacketData.originatorChainIsSource = !ICS20Lib.hasPrefix(denomBz, denomPrefix); | ||
|
||
if (receivePacketData.originatorChainIsSource) { |
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 wonder if we can somehow utilize an enum, since it seems that there is a lot of nested if logic. But I'm happy with what we have at the moment if you don't see a natural way of using enums.
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.
Yeah, I don't love it either. I will give it a shot and see if there is a smoother way to do this.
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.
ci passing
No description provided.