Skip to content

Commit

Permalink
feat(ics20): initial version of transfer app (#4)
Browse files Browse the repository at this point in the history
* minimal initial poc for ics20 onSendPacket

* lint

* lint more

* ICS20 version check

* erc20 balance protection

* onAcknowledgementPacket

* onTimeoutPacket

* emit events from ibc app callbakcs

* forge fmt

* ack and timeout integration tests + client test

* Some minor cleanups

* Added link back to repo where ICS20Lib came from

* Update src/apps/transfer/ICS20Transfer.sol

Co-authored-by: srdtrk <[email protected]>

* cr fixes and cleanup

* add sendTransfer

* rename test utility method

* imp: convert internal to private

---------

Co-authored-by: srdtrk <[email protected]>
Co-authored-by: srdtrk <[email protected]>
  • Loading branch information
3 people authored Jul 31, 2024
1 parent f4fcc89 commit ea66332
Show file tree
Hide file tree
Showing 29 changed files with 1,344 additions and 35 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ yarn.lock
!broadcast
broadcast/*
broadcast/*/31337/

# ide files
.idea/
*.iml
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,36 +27,36 @@ Note that OpenZeppelin Contracts is pre-installed, so you can follow that as an

This is a list of the most frequently needed commands.

### Build
### Install Dependencies

Build the contracts:
Install the dependencies:

```sh
$ forge build
$ bun install
```

### Clean
### Build

Delete the build artifacts and cache directories:
Build/compile the contracts:

```sh
$ forge clean
$ forge build
```

### Compile
### Test

Compile the contracts:
Run the tests:

```sh
$ forge build
$ forge test
```

### Coverage
### Clean

Get a test coverage report:
Delete the build artifacts and cache directories:

```sh
$ forge coverage
$ forge clean
```

### Deploy
Expand Down Expand Up @@ -97,12 +97,12 @@ Lint the contracts:
$ bun run lint
```

### Test
### Coverage

Run the tests:
Get a test coverage report:

```sh
$ forge test
$ forge coverage
```

Generate test coverage and output result to the terminal:
Expand Down
4 changes: 2 additions & 2 deletions src/ICS02Client.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IICS02Client } from "./interfaces/IICS02Client.sol";
Expand Down Expand Up @@ -35,7 +35,7 @@ contract ICS02Client is IICS02Client, IICS02ClientErrors, Ownable {
function getCounterparty(string calldata clientId) public view returns (CounterpartyInfo memory) {
CounterpartyInfo memory counterpartyInfo = counterpartyInfos[clientId];
if (bytes(counterpartyInfo.clientId).length == 0) {
revert IBCClientNotFound(clientId);
revert IBCCounterpartyClientNotFound(clientId);
}

return counterpartyInfo;
Expand Down
140 changes: 140 additions & 0 deletions src/ICS20Transfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IIBCApp } from "./interfaces/IIBCApp.sol";
import { IICS20Errors } from "./errors/IICS20Errors.sol";
import { ICS20Lib } from "./utils/ICS20Lib.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { IICS20Transfer } from "./interfaces/IICS20Transfer.sol";
import { IICS26Router } from "./interfaces/IICS26Router.sol";
import { IICS26RouterMsgs } from "./msgs/IICS26RouterMsgs.sol";

using SafeERC20 for IERC20;

/*
* Things not handled yet:
* - Prefixed denoms (source chain is not the source) and the burning of tokens related to that
* - Separate escrow balance tracking
* - Related to escrow ^: invariant checking (where to implement that?)
* - Receiving packets
*/
contract ICS20Transfer is IIBCApp, IICS20Transfer, IICS20Errors, Ownable, ReentrancyGuard {
/// @param owner_ The owner of the contract
constructor(address owner_) Ownable(owner_) { }

function sendTransfer(SendTransferMsg calldata msg_) external override returns (uint32) {
IICS26Router ibcRouter = IICS26Router(owner());

string memory sender = ICS20Lib.addressToHexString(msg.sender);
string memory sourcePort = ICS20Lib.addressToHexString(address(this));
bytes memory packetData;
if (bytes(msg_.memo).length == 0) {
packetData = ICS20Lib.marshalJSON(msg_.denom, msg_.amount, sender, msg_.receiver);
} else {
packetData = ICS20Lib.marshalJSON(msg_.denom, msg_.amount, sender, msg_.receiver, msg_.memo);
}

IICS26RouterMsgs.MsgSendPacket memory msgSendPacket = IICS26RouterMsgs.MsgSendPacket({
sourcePort: sourcePort,
sourceChannel: msg_.sourceChannel,
destPort: msg_.destPort,
data: packetData,
timeoutTimestamp: msg_.timeoutTimestamp, // TODO: Default timestamp?
version: ICS20Lib.ICS20_VERSION
});

return ibcRouter.sendPacket(msgSendPacket);
}

function onSendPacket(OnSendPacketCallback calldata msg_) external override onlyOwner nonReentrant {
if (keccak256(abi.encodePacked(msg_.packet.version)) != keccak256(abi.encodePacked(ICS20Lib.ICS20_VERSION))) {
revert ICS20UnexpectedVersion(msg_.packet.version);
}

ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data);

// TODO: Maybe have a "ValidateBasic" type of function that checks the packet data, could be done in unwrapping?

if (packetData.amount == 0) {
revert ICS20InvalidAmount(packetData.amount);
}

// TODO: Handle prefixed denoms (source chain is not the source) and burn

// The packet sender has to be either the packet data sender or the contract itself
// The scenarios are either the sender sent the packet directly to the router (msg_.sender == packetData.sender)
// or sender used the sendTransfer function, which makes this contract the sender (msg_.sender == address(this))
if (msg_.sender != packetData.sender && msg_.sender != address(this)) {
revert ICS20MsgSenderIsNotPacketSender(msg_.sender, packetData.sender);
}

_transferFrom(packetData.sender, address(this), packetData.erc20ContractAddress, packetData.amount);

emit ICS20Transfer(packetData);
}

function onRecvPacket(OnRecvPacketCallback calldata)
external
override
onlyOwner
nonReentrant
returns (bytes memory)
{
// TODO: Implement
return "";
}

function onAcknowledgementPacket(OnAcknowledgementPacketCallback calldata msg_)
external
override
onlyOwner
nonReentrant
{
ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data);
bool isSuccessAck = true;

if (keccak256(msg_.acknowledgement) != ICS20Lib.KECCAK256_SUCCESSFUL_ACKNOWLEDGEMENT_JSON) {
isSuccessAck = false;
_refundTokens(packetData);
}

// Nothing needed to be done if the acknowledgement was successful, tokens are already in escrow or burnt

emit ICS20Acknowledgement(packetData, msg_.acknowledgement, isSuccessAck);
}

function onTimeoutPacket(OnTimeoutPacketCallback calldata msg_) external override onlyOwner nonReentrant {
ICS20Lib.UnwrappedFungibleTokenPacketData memory packetData = ICS20Lib.unwrapPacketData(msg_.packet.data);
_refundTokens(packetData);

emit ICS20Timeout(packetData);
}

// TODO: Implement escrow balance tracking
function _refundTokens(ICS20Lib.UnwrappedFungibleTokenPacketData memory data) private {
address refundee = data.sender;
IERC20(data.erc20ContractAddress).safeTransfer(refundee, data.amount);
}

// TODO: Implement escrow balance tracking
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);
}
}
}
10 changes: 7 additions & 3 deletions src/ICS26Router.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IIBCApp } from "./interfaces/IIBCApp.sol";
Expand Down Expand Up @@ -60,7 +60,7 @@ contract ICS26Router is IICS26Router, IBCStore, Ownable, IICS26RouterErrors, Ree
/// @param msg_ The message for sending packets
/// @return The sequence number of the packet
function sendPacket(MsgSendPacket calldata msg_) external nonReentrant returns (uint32) {
string memory counterpartyId = ics02Client.getCounterparty(msg_.sourcePort).clientId;
string memory counterpartyId = ics02Client.getCounterparty(msg_.sourceChannel).clientId;

// TODO: validate all identifiers
if (msg_.timeoutTimestamp <= block.timestamp) {
Expand All @@ -83,7 +83,11 @@ contract ICS26Router is IICS26Router, IBCStore, Ownable, IICS26RouterErrors, Ree
IIBCAppCallbacks.OnSendPacketCallback memory sendPacketCallback =
IIBCAppCallbacks.OnSendPacketCallback({ packet: packet, sender: msg.sender });

apps[msg_.sourcePort].onSendPacket(sendPacketCallback);
IIBCApp app = apps[msg_.sourcePort];
if (app == IIBCApp(address(0))) {
revert IBCAppNotFound(msg_.sourcePort);
}
app.onSendPacket(sendPacketCallback);

IBCStore.commitPacket(packet);

Expand Down
5 changes: 4 additions & 1 deletion src/errors/IICS02ClientErrors.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

interface IICS02ClientErrors {
Expand All @@ -7,4 +7,7 @@ interface IICS02ClientErrors {

/// @param clientId client identifier
error IBCClientNotFound(string clientId);

/// @param counterpartyClientId counterparty client identifier
error IBCCounterpartyClientNotFound(string counterpartyClientId);
}
55 changes: 55 additions & 0 deletions src/errors/IICS20Errors.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity >=0.8.25;

interface IICS20Errors {
/// @param msgSender Address of the message sender
/// @param packetSender Address of the packet sender
error ICS20MsgSenderIsNotPacketSender(address msgSender, address packetSender);

/// @param sender Address whose tokens are being transferred
error ICS20InvalidSender(string sender);

/// @param amount Amount of tokens being transferred
error ICS20InvalidAmount(uint256 amount);

/// @param tokenContract Address of the token contract
error ICS20InvalidTokenContract(string tokenContract);

/// @param version Version string
error ICS20UnexpectedVersion(string version);

/// @param expected Expected balance of the ERC20 token for ICS20Transfer
/// @param actual Actual balance of the ERC20 token for ICS20Transfer
error ICS20UnexpectedERC20Balance(uint256 expected, uint256 actual);

// ICS20Lib Errors:

/// @param position position in packet data bytes
/// @param expected expected bytes
/// @param actual actual bytes
error ICS20JSONUnexpectedBytes(uint256 position, bytes32 expected, bytes32 actual);

/// @param position position in packet data bytes
/// @param actual actual value
error ICS20JSONClosingBraceNotFound(uint256 position, bytes1 actual);

/// @param position position in packet data bytes
/// @param actual actual value
error ICS20JSONStringClosingDoubleQuoteNotFound(uint256 position, bytes1 actual);

/// @param bz json string value
/// @param position position in packet data bytes
error ICS20JSONStringUnclosed(bytes bz, uint256 position);

/// @param position position in packet data bytes
/// @param actual actual value
error ICS20JSONInvalidEscape(uint256 position, bytes1 actual);

/// @param length length of the slice
error ICS20BytesSliceOverflow(uint256 length);

/// @param length length of the bytes
/// @param start start index
/// @param end end index
error ICS20BytesSliceOutOfBounds(uint256 length, uint256 start, uint256 end);
}
2 changes: 1 addition & 1 deletion src/errors/IICS24HostErrors.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

interface IICS24HostErrors {
Expand Down
4 changes: 3 additions & 1 deletion src/errors/IICS26RouterErrors.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

interface IICS26RouterErrors {
Expand All @@ -18,4 +18,6 @@ interface IICS26RouterErrors {
error IBCAsyncAcknowledgementNotSupported();

error IBCPacketCommitmentMismatch(bytes32 expected, bytes32 actual);

error IBCAppNotFound(string portId);
}
2 changes: 1 addition & 1 deletion src/interfaces/IIBCApp.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IIBCAppCallbacks } from "../msgs/IIBCAppCallbacks.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/IICS02Client.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IICS02ClientMsgs } from "../msgs/IICS02ClientMsgs.sol";
Expand Down
29 changes: 29 additions & 0 deletions src/interfaces/IICS20Transfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { ICS20Lib } from "../utils/ICS20Lib.sol";
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);

// TODO: If we want error and/or success result in the event (resp.Result), parsing the acknowledgement is needed
/// @notice Called after handling acknowledgement in onAcknowledgementPacket
/// @param packetData The transfer packet data
/// @param acknowledgement The acknowledgement data
/// @param success Whether the acknowledgement received was a success or error
event ICS20Acknowledgement(
ICS20Lib.UnwrappedFungibleTokenPacketData packetData, bytes acknowledgement, bool success
);

/// @notice Called after handling a timeout in onTimeoutPacket
/// @param packetData The transfer packet data
event ICS20Timeout(ICS20Lib.UnwrappedFungibleTokenPacketData packetData);

/// @notice Send a transfer
/// @param msg The message for sending a transfer
/// @return sequence The sequence number of the packet created
function sendTransfer(SendTransferMsg calldata msg) external returns (uint32 sequence);
}
2 changes: 1 addition & 1 deletion src/interfaces/IICS26Router.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { IICS26RouterMsgs } from "../msgs/IICS26RouterMsgs.sol";
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/ILightClient.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.25;

import { ILightClientMsgs } from "../msgs/ILightClientMsgs.sol";
Expand Down
Loading

0 comments on commit ea66332

Please sign in to comment.