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);
}
}