Skip to content

Conversation

@mariuszzak
Copy link
Collaborator

@mariuszzak mariuszzak commented Dec 12, 2025

Description

closes: STACK-1906

Implements the Interchain Fungible Token (IFT) standard in Solidity, enabling cross-chain token transfers via ICS27-GMP.

  • Add IFTBase abstract contract that extends ERC20 with cross-chain transfer capabilities
  • Implement burn-and-mint token bridge pattern with pending transfer tracking
  • Add EVMIFTSendCallConstructor for EVM-to-EVM bridges
  • Add CosmosIFTSendCallConstructor for EVM-to-Cosmos bridges (will be added together with Cosmos SDK IFT implementation)
  • Include unit tests and integration tests

Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Linked to GitHub issue with discussion and accepted design, OR link to spec that describes this work.
  • Wrote unit and integration tests.
  • Added relevant natspec and godoc comments.
  • Provide a conventional commit message to follow the repository standards.
  • Re-reviewed Files changed in the GitHub PR explorer.
  • Review SonarCloud Report in the comment section below once CI passes.

@mariuszzak mariuszzak self-assigned this Dec 12, 2025
/// @param amount The amount of tokens involved in the pending transfer
struct PendingTransfer {
address sender;
uint256 amount;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: clientId and sequence are mapping keys, hence they are not included in the struct.

/// @param denom_ The denom of the token on the Cosmos SDK chain (e.g., "uatom", "ibc/...")
/// @param icaAddress_ The interchain account address on the Cosmos SDK chain
constructor(string memory bridgeReceiveTypeUrl_, string memory denom_, string memory icaAddress_) {
bridgeReceiveTypeUrl = bridgeReceiveTypeUrl_;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I made it configurable instead of hardcoding.

@mariuszzak mariuszzak marked this pull request as ready for review December 12, 2025 15:59
@mariuszzak mariuszzak requested a review from srdtrk as a code owner December 12, 2025 15:59
@codecov
Copy link

codecov bot commented Dec 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.90%. Comparing base (fa1d3fb) to head (a22f678).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #864   +/-   ##
=======================================
  Coverage   99.89%   99.90%           
=======================================
  Files          22       26    +4     
  Lines         982     1079   +97     
=======================================
+ Hits          981     1078   +97     
  Misses          1        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 39 to 45
/// @notice Initializes the IFT base contract
/// @param ics27Gmp_ The ICS27-GMP contract address
/// @param authority_ The AccessManager contract address for access control
constructor(IICS27GMP ics27Gmp_, address authority_) AccessManaged(authority_) {
IFTStorage storage $ = _getIFTStorage();
$._ics27Gmp = ics27Gmp_;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The storage slot implies this is an upgradable contract, so we probably should use an initializer instead of constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to include new restricted functions on our deploy scripts

{
require(bytes(clientId).length > 0, IFTEmptyClientId());
require(bytes(counterpartyIFTAddress).length > 0, IFTEmptyCounterpartyAddress());
require(iftSendCallConstructor != address(0), IFTZeroAddressConstructor());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may consider checking interface with ERC 165

/// @title IFT Base Contract
/// @notice Abstract base contract for Interchain Fungible Tokens
/// @dev Extend this contract and implement the ERC20 constructor to create an IFT token
abstract contract IFTBase is IIFTErrors, IIFT, ERC20, AccessManaged, IBCCallbackReceiver {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add a warning somewhere that this is experimental. Similar to the warning on attestor LC in ibc-go

require(bytes(clientId).length > 0, IFTEmptyClientId());
require(bytes(receiver).length > 0, IFTEmptyReceiver());
require(amount > 0, IFTZeroAmount());
require(timeoutTimestamp > block.timestamp, IFTTimeoutInPast(timeoutTimestamp, uint64(block.timestamp)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is validated by the router, but I'll double check

@vaporif vaporif self-assigned this Dec 16, 2025
@srdtrk srdtrk assigned srdtrk and unassigned vaporif and mariuszzak Dec 17, 2025

IFTBaseStorage storage $ = _getIFTBaseStorage();
IIFTMsgs.IFTBridge memory bridge = $._iftBridges[clientId];
require(keccak256(bytes(bridge.clientId)) == keccak256(bytes(clientId)), IFTBridgeNotFound(clientId));
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The keccak256 comparison here is redundant since bridge is already looked up using clientId as the mapping key. If the mapping returns a populated struct, bridge.clientId will equal clientId by definition.

Suggested change
require(keccak256(bytes(bridge.clientId)) == keccak256(bytes(clientId)), IFTBridgeNotFound(clientId));
require(bytes(bridge.clientId).length > 0, IFTBridgeNotFound(clientId));

@linear
Copy link

linear bot commented Jan 6, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants