polytone-evm 0.3.0

Core interfaces for Polytone Interchain accounts and queries.
Documentation
pragma solidity ^0.8.24;

import {IRelay} from "../IRelay.sol";
import {LocalToken} from "../Requests.sol";
import {IbcCoreClientV1Height} from "../IBCTypes.sol";

import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";

/// @title Ibc Token Actions
/// @dev encapsulates common actions for IbcTokens
/// @custom:todo this should be an ERC7579 module once we have AAs
contract IbcTokenActions is Ownable, Pausable {
    // --------------------- Data Structure --------------------- //

    struct IbcSendMessage {
        LocalToken[] tokens;
        // TODO: We could store a mapping of chain id -> channelId somewhere
        string channelId;
        bytes receiver;
        string extension;
    }

    /// @notice Struct to store a token and its percentage (scaled to 1e6)
    struct PercentToken {
        address denom;
        uint32 percentage;
    }

    struct IbcSendPercentageMessage {
        PercentToken[] tokens;
        string channelId;
        bytes receiver;
        string extension;
    }

    /// @notice constant to scale uints into percentages (1e6 == 100%)
    uint32 public constant PERCENTAGE_SCALE = 1e6;

    IRelay public tokenRelay;
    uint64 public timeout;

    // --------------------- Errors --------------------- //

    error FailedApproval(address token, uint128 amount);
    error InvalidPercentage(uint32 percentage, uint32 max);

    // --------------------- Events --------------------- //

    event IbcSend(bytes indexed receiver, LocalToken[] tokens, string extension);

    // --------------------- Functions --------------------- //

    constructor(IRelay tokenRelay_, uint64 timeout_, address admin) Ownable(admin) {
        tokenRelay = tokenRelay_;
        timeout = timeout_;
    }

    /**
     * @dev Update the token relay contract.
     */
    function setTokenRelay(IRelay newTokenRelay) public onlyOwner {
        tokenRelay = newTokenRelay;
    }

    /**
     * @dev Update the default token timeout.
     */
    function setTimeout(uint64 newTimeout) public onlyOwner {
        timeout = newTimeout;
    }

    function _ibcSend(IbcTokenActions executor, IbcSendMessage memory sendMessage)
        internal
        returns (bool, bytes memory)
    {
        emit IbcSend(sendMessage.receiver, sendMessage.tokens, sendMessage.extension);

        address tokenRelayAddress = address(executor.tokenRelay());
        uint64 packetTimeout = executor.timeout();

        // Approve the IBC handler to transfer the funds
        for (uint256 i = 0; i < sendMessage.tokens.length; i++) {
            LocalToken memory localToken = sendMessage.tokens[i];
            IERC20 token = IERC20(localToken.denom);
            bool success = token.approve(tokenRelayAddress, localToken.amount);

            if (!success) {
                revert FailedApproval(address(token), localToken.amount);
            }
        }

        // Initiate the IBC transfer
        return tokenRelayAddress.call(
            abi.encodeWithSelector(
                IRelay.send.selector,
                sendMessage.channelId,
                sendMessage.receiver,
                sendMessage.tokens,
                sendMessage.extension,
                // No height timeout
                IbcCoreClientV1Height.IbcCoreClientV1HeightData({revision_number: 0, revision_height: 0}),
                uint64(block.timestamp * 1e9) + packetTimeout
            )
        );
    }

    /**
     * @dev Approve and transfer erc20 tokens over IBC
     * 1. Approve the IBC handler to transfer the funds
     * 2. Send the funds to the IBC handler via the sendCall method
     * @param executor - this contract's address because it's executed via delegatecall, meaning it doesn't have access to its own storage. We hack this by passing its own address in.
     * @param sendMessage - the send tokens message to be executed.
     */
    function ibcSend(IbcTokenActions executor, IbcSendMessage memory sendMessage)
        external
        whenNotPaused
        returns (bool, bytes memory)
    {
        return _ibcSend(executor, sendMessage);
    }

    /**
     * @dev Approve and transfer ERC20 tokens over IBC by percentages.
     * @param executor - This contract's address, used due to delegatecall constraints.
     * @param percentageMessage - the percentage send tokens message to be executed.
     */
    function ibcSendPercentage(IbcTokenActions executor, IbcSendPercentageMessage memory percentageMessage)
        external
        whenNotPaused
        returns (bool, bytes memory)
    {
        PercentToken[] memory tokens = percentageMessage.tokens;
        uint256 length = tokens.length;

        LocalToken[] memory sendTokens = new LocalToken[](length);

        // Calculate token amounts based on scaled percentages
        for (uint256 i = 0; i < length; i++) {
            PercentToken memory percentToken = tokens[i];
            uint32 percentage = percentToken.percentage;

            if (percentage > PERCENTAGE_SCALE) {
                revert InvalidPercentage(percentage, PERCENTAGE_SCALE);
            }

            // Fetch the balance and calculate the token amount based on the scaled percentage
            address denom = percentToken.denom;
            uint256 balance = IERC20(denom).balanceOf(address(this));

            // Calculate amount with the scaled percentage
            uint256 calculatedAmount = (balance * percentage) / PERCENTAGE_SCALE;
            // We use uint128 for IBC tokens
            require(calculatedAmount <= type(uint128).max, "Amount exceeds uint128 limit");

            sendTokens[i] = LocalToken({denom: denom, amount: uint128(calculatedAmount)});
        }

        IbcSendMessage memory sendMessage = IbcSendMessage({
            tokens: sendTokens,
            channelId: percentageMessage.channelId,
            receiver: percentageMessage.receiver,
            extension: percentageMessage.extension
        });

        return _ibcSend(executor, sendMessage);
    }
}