diff --git a/docs/src/content/docs/protocol/deployments.mdx b/docs/src/content/docs/protocol/deployments.mdx index 3b5a3e835e..7396b631cf 100644 --- a/docs/src/content/docs/protocol/deployments.mdx +++ b/docs/src/content/docs/protocol/deployments.mdx @@ -18,6 +18,7 @@ Deployments of `ibc-union` on EVM chains. Solidity contract sources can be found | **apps** | UCS00 | [`0x92735254407859361265B51cDb76583ED7E3359b`](https://eth-holesky.blockscout.com/address/0x92735254407859361265B51cDb76583ED7E3359b) | | | UCS01 | [`0xdF48f737cc7eE649FC119B312932a9b99C40f417`](https://eth-holesky.blockscout.com/address/0xdF48f737cc7eE649FC119B312932a9b99C40f417) | | | UCS02 | [`0x8c5BB6EE0C679D605Fda89341148b9921C0d119c`](https://eth-holesky.blockscout.com/address/0x8c5BB6EE0C679D605Fda89341148b9921C0d119c) | +| | UCS03 | [`0x7B7872fEc715C787A1BE3f062AdeDc82b3B06144`](https://eth-holesky.blockscout.com/address/0x7B7872fEc715C787A1BE3f062AdeDc82b3B06144) | | **support** | Deployer | [`0xa3cd41bfF71AD19fDDfd901A9773C975A0404D97`](https://eth-holesky.blockscout.com/address/0xa3cd41bfF71AD19fDDfd901A9773C975A0404D97) | | | Sender | [`0x153919669Edc8A5D0c8D1E4507c9CE60435A1177`](https://eth-holesky.blockscout.com/address/0x153919669Edc8A5D0c8D1E4507c9CE60435A1177) | | | Multicall | [`0x64A764A734648fA636525C7e4b3cE38Ca256b647`](https://eth-holesky.blockscout.com/address/0x64A764A734648fA636525C7e4b3cE38Ca256b647) | @@ -32,6 +33,7 @@ Deployments of `ibc-union` on EVM chains. Solidity contract sources can be found | **apps** | UCS00 | [`0x271126f4F9B36CE16d9e2eF75691485ddCE11dB6`](https://eth-sepolia.blockscout.com/address/0x271126f4F9B36CE16d9e2eF75691485ddCE11dB6) | | | UCS01 | [`0xCFb741465F8e0AE9C62A548Fa85D312E6E5615Ba`](https://eth-sepolia.blockscout.com/address/0xCFb741465F8e0AE9C62A548Fa85D312E6E5615Ba) | | | UCS02 | [`0x12650fCccE6dB9E99CEE482490A5fAF248A62B22`](https://eth-sepolia.blockscout.com/address/0x12650fCccE6dB9E99CEE482490A5fAF248A62B22) | +| | UCS03 | [`0x84F074C15513F15baeA0fbEd3ec42F0Bd1fb3efa`](https://eth-sepolia.blockscout.com/address/0x84F074C15513F15baeA0fbEd3ec42F0Bd1fb3efa) | | **support** | Deployer | [`0xac6dBD360ABCfe0578e998D359d4F43a5A117219`](https://eth-sepolia.blockscout.com/address/0xac6dBD360ABCfe0578e998D359d4F43a5A117219) | | | Sender | [`0x153919669Edc8A5D0c8D1E4507c9CE60435A1177`](https://eth-sepolia.blockscout.com/address/0x153919669Edc8A5D0c8D1E4507c9CE60435A1177) | | | Multicall | [`0x6FD4bf9438fAC8C535218E79191594A879E47E96`](https://eth-sepolia.blockscout.com/address/0x6FD4bf9438fAC8C535218E79191594A879E47E96) | @@ -50,7 +52,7 @@ Deployments of `ibc-union` on CosmWasm (cosmos) chains. CosmWasm contract source | `berachain-light-client` | [`union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky) | | `ucs00` | [`union194e3rchcaqyynwcj6qr6647ge7lheymrgkhq9tdknw35050ufhuqzqz2he`](https://explorer.testnet-9.union.build/union/cosmwasm/0/transactions?contract=union194e3rchcaqyynwcj6qr6647ge7lheymrgkhq9tdknw35050ufhuqzqz2he) | | ? | `union1au6fkkfcgqc6vn8dz9tq2a6ma0vzwn2zfwwgpm7awpaeekw346uqjedtky` | - + ### Stargaze Testnet elgafar-1 diff --git a/evm/contracts/apps/ucs/03-zkgm/IEurekaModule.sol b/evm/contracts/apps/ucs/03-zkgm/IEurekaModule.sol new file mode 100644 index 0000000000..a42e04d41d --- /dev/null +++ b/evm/contracts/apps/ucs/03-zkgm/IEurekaModule.sol @@ -0,0 +1,5 @@ +pragma solidity ^0.8.27; + +interface IEurekaModule { + function onZkgm(bytes calldata sender, bytes calldata message) external; +} diff --git a/evm/contracts/apps/ucs/03-zkgm/IZkgmERC20.sol b/evm/contracts/apps/ucs/03-zkgm/IZkgmERC20.sol new file mode 100644 index 0000000000..4dcb9dc685 --- /dev/null +++ b/evm/contracts/apps/ucs/03-zkgm/IZkgmERC20.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.8.27; + +import "@openzeppelin/token/ERC20/IERC20.sol"; +import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; + +interface IZkgmERC20 is IERC20, IERC20Metadata { + function mint(address to, uint256 amount) external; + function burn(address from, uint256 amount) external; +} diff --git a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol new file mode 100644 index 0000000000..97a977856a --- /dev/null +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -0,0 +1,1100 @@ +pragma solidity ^0.8.27; + +import "@openzeppelin-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin-upgradeable/utils/PausableUpgradeable.sol"; + +import "@openzeppelin/token/ERC20/IERC20.sol"; +import "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; + +import "solady/utils/CREATE3.sol"; +import "solady/utils/LibBit.sol"; +import "solady/utils/LibString.sol"; + +import "../../Base.sol"; +import "../../../core/04-channel/IBCPacket.sol"; +import "../../../core/05-port/IIBCModule.sol"; + +import "./IEurekaModule.sol"; +import "./IZkgmERC20.sol"; +import "./ZkgmERC20.sol"; + +struct ZkgmPacket { + bytes32 salt; + uint256 path; + bytes syscall; +} + +struct SyscallPacket { + uint8 version; + uint8 index; + bytes packet; +} + +struct ForwardPacket { + uint32 channelId; + uint64 timeoutHeight; + uint64 timeoutTimestamp; + bytes syscallPacket; +} + +struct MultiplexPacket { + bytes sender; + bool eureka; + bytes contractAddress; + bytes contractCalldata; +} + +struct BatchPacket { + bytes[] syscallPackets; +} + +struct FungibleAssetTransferPacket { + bytes sender; + bytes receiver; + bytes sentToken; + uint256 sentTokenPrefix; + string sentSymbol; + string sentName; + uint256 sentAmount; + bytes askToken; + uint256 askAmount; + bool onlyMaker; +} + +struct Acknowledgement { + uint256 tag; + bytes innerAck; +} + +struct BatchAcknowledgement { + bytes[] acknowledgements; +} + +struct AssetTransferAcknowledgement { + uint256 fillType; + bytes marketMaker; +} + +library ZkgmLib { + bytes public constant ACK_EMPTY = hex""; + + uint256 public constant ACK_FAILURE = 0x00; + uint256 public constant ACK_SUCCESS = 0x01; + + bytes public constant ACK_ERR_ONLYMAKER = hex"DEADC0DE"; + + uint256 public constant FILL_TYPE_PROTOCOL = 0xB0CAD0; + uint256 public constant FILL_TYPE_MARKETMAKER = 0xD1CEC45E; + + uint8 public constant SYSCALL_FORWARD = 0x00; + uint8 public constant SYSCALL_MULTIPLEX = 0x01; + uint8 public constant SYSCALL_BATCH = 0x02; + uint8 public constant SYSCALL_FUNGIBLE_ASSET_TRANSFER = 0x03; + + uint8 public constant ZKGM_VERSION_0 = 0x00; + + bytes32 public constant IBC_VERSION = keccak256("ucs03-zkgm-0"); + + error ErrUnsupportedVersion(); + error ErrUnimplemented(); + error ErrBatchMustBeSync(); + error ErrUnknownSyscall(); + error ErrInfiniteGame(); + error ErrUnauthorized(); + error ErrInvalidAmount(); + error ErrOnlyMaker(); + error ErrInvalidFillType(); + error ErrInvalidIBCVersion(); + error ErrInvalidHops(); + error ErrInvalidAssetOrigin(); + error ErrInvalidAssetSymbol(); + error ErrInvalidAssetName(); + + function encodeAssetTransferAck( + AssetTransferAcknowledgement memory ack + ) internal pure returns (bytes memory) { + return abi.encode(ack.fillType, ack.marketMaker); + } + + function decodeAssetTransferAck( + bytes calldata stream + ) internal pure returns (AssetTransferAcknowledgement calldata) { + AssetTransferAcknowledgement calldata ack; + assembly { + ack := stream.offset + } + return ack; + } + + function encodeBatchAck( + BatchAcknowledgement memory ack + ) internal pure returns (bytes memory) { + return abi.encode(ack.acknowledgements); + } + + function decodeBatchAck( + bytes calldata stream + ) internal pure returns (BatchAcknowledgement calldata) { + BatchAcknowledgement calldata acks; + assembly { + acks := stream.offset + } + return acks; + } + + function encodeAck( + Acknowledgement memory ack + ) internal pure returns (bytes memory) { + return abi.encode(ack.tag, ack.innerAck); + } + + function decodeAck( + bytes calldata stream + ) internal pure returns (Acknowledgement calldata) { + Acknowledgement calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function encode( + ZkgmPacket memory packet + ) internal pure returns (bytes memory) { + return abi.encode(packet.salt, packet.path, packet.syscall); + } + + function decode( + bytes calldata stream + ) internal pure returns (ZkgmPacket calldata) { + ZkgmPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function decodeSyscall( + bytes calldata stream + ) internal pure returns (SyscallPacket calldata) { + SyscallPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function decodeBatch( + bytes calldata stream + ) internal pure returns (BatchPacket calldata) { + BatchPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function decodeForward( + bytes calldata stream + ) internal pure returns (ForwardPacket calldata) { + ForwardPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function decodeMultiplex( + bytes calldata stream + ) internal pure returns (MultiplexPacket calldata) { + MultiplexPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function decodeFungibleAssetTransfer( + bytes calldata stream + ) internal pure returns (FungibleAssetTransferPacket calldata) { + FungibleAssetTransferPacket calldata packet; + assembly { + packet := stream.offset + } + return packet; + } + + function isDeployed( + address addr + ) internal returns (bool) { + uint32 size = 0; + assembly { + size := extcodesize(addr) + } + return size > 0; + } + + function updateChannelPath( + uint256 path, + uint32 nextChannelId + ) internal pure returns (uint256) { + if (path == 0) { + return uint256(nextChannelId); + } + uint256 nextHopIndex = LibBit.fls(path) / 32 + 1; + if (nextHopIndex > 7) { + revert ErrInvalidHops(); + } + return (uint256(nextChannelId) << 32 * nextHopIndex) | path; + } + + function lastChannelFromPath( + uint256 path + ) internal pure returns (uint32) { + if (path == 0) { + return 0; + } + uint256 currentHopIndex = LibBit.fls(path) / 32; + return uint32(path >> currentHopIndex * 32); + } +} + +contract UCS03Zkgm is + IBCAppBase, + Initializable, + UUPSUpgradeable, + OwnableUpgradeable, + PausableUpgradeable +{ + using ZkgmLib for *; + using LibString for *; + + IIBCPacket public ibcHandler; + mapping(bytes32 => IBCPacket) public inFlightPacket; + mapping(uint32 => mapping(address => uint256)) public channelBalance; + mapping(address => uint256) public tokenOrigin; + + constructor() { + _disableInitializers(); + } + + function initialize( + IIBCPacket _ibcHandler, + address admin + ) public initializer { + __Ownable_init(admin); + ibcHandler = _ibcHandler; + } + + function ibcAddress() public view virtual override returns (address) { + return address(ibcHandler); + } + + function send( + uint32 channelId, + uint64 timeoutHeight, + uint64 timeoutTimestamp, + bytes32 salt, + bytes calldata rawSyscall + ) public { + verifyInternal(channelId, 0, rawSyscall); + ibcHandler.sendPacket( + channelId, + timeoutHeight, + timeoutTimestamp, + ZkgmLib.encode( + // TODO: change salt to string and then assert its prefixed with user address and keccak256 it + ZkgmPacket({salt: salt, path: 0, syscall: rawSyscall}) + ) + ); + } + + function verifyInternal( + uint32 channelId, + uint256 path, + bytes calldata rawSyscall + ) internal { + SyscallPacket calldata syscallPacket = ZkgmLib.decodeSyscall(rawSyscall); + if (syscallPacket.version != ZkgmLib.ZKGM_VERSION_0) { + revert ZkgmLib.ErrUnsupportedVersion(); + } + if (syscallPacket.index == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { + verifyFungibleAssetTransfer( + channelId, + path, + ZkgmLib.decodeFungibleAssetTransfer(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_BATCH) { + verifyBatch( + channelId, path, ZkgmLib.decodeBatch(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_FORWARD) { + verifyForward( + channelId, path, ZkgmLib.decodeForward(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_MULTIPLEX) { + verifyMultiplex( + channelId, path, ZkgmLib.decodeMultiplex(syscallPacket.packet) + ); + } else { + revert ZkgmLib.ErrUnknownSyscall(); + } + } + + function verifyFungibleAssetTransfer( + uint32 channelId, + uint256 path, + FungibleAssetTransferPacket calldata assetTransferPacket + ) internal { + IERC20Metadata sentToken = + IERC20Metadata(address(bytes20(assetTransferPacket.sentToken))); + if (!assetTransferPacket.sentName.eq(sentToken.name())) { + revert ZkgmLib.ErrInvalidAssetName(); + } + if (!assetTransferPacket.sentSymbol.eq(sentToken.symbol())) { + revert ZkgmLib.ErrInvalidAssetSymbol(); + } + uint256 origin = tokenOrigin[address(sentToken)]; + if (ZkgmLib.lastChannelFromPath(origin) == channelId) { + IZkgmERC20(address(sentToken)).burn( + msg.sender, assetTransferPacket.sentAmount + ); + } else { + // TODO: extract this as a step before verifying to allow for ERC777 + // send hook + SafeERC20.safeTransferFrom( + sentToken, + msg.sender, + address(this), + assetTransferPacket.sentAmount + ); + channelBalance[channelId][address(sentToken)] += + assetTransferPacket.sentAmount; + } + if (!assetTransferPacket.onlyMaker) { + if (assetTransferPacket.sentTokenPrefix != origin) { + revert ZkgmLib.ErrInvalidAssetOrigin(); + } + } + } + + function verifyBatch( + uint32 channelId, + uint256 path, + BatchPacket calldata batchPacket + ) internal { + uint256 l = batchPacket.syscallPackets.length; + for (uint256 i = 0; i < l; i++) { + verifyInternal(channelId, path, batchPacket.syscallPackets[i]); + } + } + + function verifyForward( + uint32 channelId, + uint256 path, + ForwardPacket calldata forwardPacket + ) internal { + verifyInternal( + channelId, + ZkgmLib.updateChannelPath(path, forwardPacket.channelId), + forwardPacket.syscallPacket + ); + } + + function verifyMultiplex( + uint32 channelId, + uint256 path, + MultiplexPacket calldata multiplexPacket + ) internal {} + + function onRecvPacket( + IBCPacket calldata packet, + address relayer, + bytes calldata relayerMsg + ) external virtual override onlyIBC returns (bytes memory) { + (bool success, bytes memory returnData) = address(this).call( + abi.encodeCall(this.execute, (packet, relayer, relayerMsg)) + ); + bytes memory acknowledgement = abi.decode(returnData, (bytes)); + if (success) { + // The acknowledgement may be asynchronous (forward/multiplex) + if (acknowledgement.length == 0) { + return ZkgmLib.ACK_EMPTY; + } + + // Special case where we should avoid the packet from being + // received entirely as it is only fillable by a market maker. + if ( + keccak256(acknowledgement) + == keccak256(ZkgmLib.ACK_ERR_ONLYMAKER) + ) { + revert ZkgmLib.ErrOnlyMaker(); + } + + return ZkgmLib.encodeAck( + Acknowledgement({ + tag: ZkgmLib.ACK_SUCCESS, + innerAck: acknowledgement + }) + ); + } else { + return ZkgmLib.encodeAck( + Acknowledgement({ + tag: ZkgmLib.ACK_FAILURE, + innerAck: ZkgmLib.ACK_EMPTY + }) + ); + } + } + + function execute( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg + ) public returns (bytes memory) { + // Only callable through the onRecvPacket endpoint. + if (msg.sender != address(this)) { + revert ZkgmLib.ErrUnauthorized(); + } + ZkgmPacket calldata zkgmPacket = ZkgmLib.decode(ibcPacket.data); + return executeInternal( + ibcPacket, + relayer, + relayerMsg, + zkgmPacket.salt, + zkgmPacket.path, + ZkgmLib.decodeSyscall(zkgmPacket.syscall) + ); + } + + function executeInternal( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg, + bytes32 salt, + uint256 path, + SyscallPacket calldata syscallPacket + ) internal returns (bytes memory) { + if (syscallPacket.version != ZkgmLib.ZKGM_VERSION_0) { + revert ZkgmLib.ErrUnsupportedVersion(); + } + if (syscallPacket.index == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { + return executeFungibleAssetTransfer( + ibcPacket, + relayer, + relayerMsg, + salt, + path, + ZkgmLib.decodeFungibleAssetTransfer(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_BATCH) { + return executeBatch( + ibcPacket, + relayer, + relayerMsg, + salt, + path, + ZkgmLib.decodeBatch(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_FORWARD) { + return executeForward( + ibcPacket, + relayer, + relayerMsg, + salt, + path, + ZkgmLib.decodeForward(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_MULTIPLEX) { + return executeMultiplex( + ibcPacket, + relayer, + relayerMsg, + salt, + ZkgmLib.decodeMultiplex(syscallPacket.packet) + ); + } else { + revert ZkgmLib.ErrUnknownSyscall(); + } + } + + function executeBatch( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg, + bytes32 salt, + uint256 path, + BatchPacket calldata batchPacket + ) internal returns (bytes memory) { + uint256 l = batchPacket.syscallPackets.length; + bytes[] memory acks = new bytes[](l); + for (uint256 i = 0; i < l; i++) { + SyscallPacket calldata syscallPacket = + ZkgmLib.decodeSyscall(batchPacket.syscallPackets[i]); + acks[i] = executeInternal( + ibcPacket, + relayer, + relayerMsg, + keccak256(abi.encode(salt)), + path, + syscallPacket + ); + if (acks[i].length == 0) { + revert ZkgmLib.ErrBatchMustBeSync(); + } + } + return ZkgmLib.encodeBatchAck( + BatchAcknowledgement({acknowledgements: acks}) + ); + } + + function executeForward( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg, + bytes32 salt, + uint256 path, + ForwardPacket calldata forwardPacket + ) internal returns (bytes memory) { + // TODO: consider using a magic value for few bytes of the salt in order + // to know that it's a forwarded packet in the acknowledgement, without + // having to index in `inFlightPacket`, saving gas in the process. + IBCPacket memory sentPacket = ibcHandler.sendPacket( + forwardPacket.channelId, + forwardPacket.timeoutHeight, + forwardPacket.timeoutTimestamp, + ZkgmLib.encode( + ZkgmPacket({ + salt: keccak256(abi.encode(salt)), + path: ZkgmLib.updateChannelPath( + path, ibcPacket.destinationChannel + ), + syscall: forwardPacket.syscallPacket + }) + ) + ); + // Guaranteed to be unique by the above sendPacket + bytes32 packetHash = IBCPacketLib.commitPacketMemory(sentPacket); + inFlightPacket[packetHash] = ibcPacket; + return ZkgmLib.ACK_EMPTY; + } + + function executeMultiplex( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg, + bytes32 salt, + MultiplexPacket calldata multiplexPacket + ) internal returns (bytes memory) { + address contractAddress = + address(bytes20(multiplexPacket.contractAddress)); + if (multiplexPacket.eureka) { + IEurekaModule(contractAddress).onZkgm( + multiplexPacket.sender, multiplexPacket.contractCalldata + ); + return abi.encode(ZkgmLib.ACK_SUCCESS); + } else { + IBCPacket memory multiplexIbcPacket = IBCPacket({ + sourceChannel: ibcPacket.sourceChannel, + destinationChannel: ibcPacket.destinationChannel, + data: abi.encode( + multiplexPacket.sender, multiplexPacket.contractCalldata + ), + timeoutHeight: ibcPacket.timeoutHeight, + timeoutTimestamp: ibcPacket.timeoutTimestamp + }); + bytes memory acknowledgement = IIBCModule(contractAddress) + .onRecvPacket(multiplexIbcPacket, relayer, relayerMsg); + if (acknowledgement.length == 0) { + /* TODO: store the packet for async ack To handle async acks on + multiplexing, we need to have a mapping from (receiver, + virtualPacket) => ibcPacket. Then the receiver will be the + only one able to acknowledge a virtual packet, resulting in + the origin ibc packet to be acknowledged itself. + */ + revert ZkgmLib.ErrUnimplemented(); + } + return acknowledgement; + } + } + + function internalPredictWrappedToken( + uint256 path, + uint32 channel, + bytes calldata token + ) internal returns (address, bytes32) { + bytes32 wrappedTokenSalt = keccak256(abi.encode(path, channel, token)); + address wrappedToken = + CREATE3.predictDeterministicAddress(wrappedTokenSalt); + return (wrappedToken, wrappedTokenSalt); + } + + function predictWrappedToken( + uint256 path, + uint32 channel, + bytes calldata token + ) public returns (address, bytes32) { + return internalPredictWrappedToken(path, channel, token); + } + + function executeFungibleAssetTransfer( + IBCPacket calldata ibcPacket, + address relayer, + bytes calldata relayerMsg, + bytes32 salt, + uint256 path, + FungibleAssetTransferPacket calldata assetTransferPacket + ) internal returns (bytes memory) { + if (assetTransferPacket.onlyMaker) { + return ZkgmLib.ACK_ERR_ONLYMAKER; + } + // The protocol can only wrap or unwrap an asset, hence 1:1 baked. + // The fee is the difference, which can only be positive. + if (assetTransferPacket.askAmount > assetTransferPacket.sentAmount) { + revert ZkgmLib.ErrInvalidAmount(); + } + (address wrappedToken, bytes32 wrappedTokenSalt) = + internalPredictWrappedToken( + path, ibcPacket.destinationChannel, assetTransferPacket.sentToken + ); + address askToken = address(bytes20(assetTransferPacket.askToken)); + address receiver = address(bytes20(assetTransferPacket.receiver)); + // Previously asserted to be <=. + uint256 fee = + assetTransferPacket.sentAmount - assetTransferPacket.askAmount; + if (askToken == wrappedToken) { + if (!ZkgmLib.isDeployed(wrappedToken)) { + CREATE3.deployDeterministic( + abi.encodePacked( + type(ZkgmERC20).creationCode, + abi.encode( + assetTransferPacket.sentName, + assetTransferPacket.sentSymbol, + address(this) + ) + ), + wrappedTokenSalt + ); + tokenOrigin[wrappedToken] = ZkgmLib.updateChannelPath( + path, ibcPacket.destinationChannel + ); + } + IZkgmERC20(wrappedToken).mint( + receiver, assetTransferPacket.askAmount + ); + if (fee > 0) { + IZkgmERC20(wrappedToken).mint(relayer, fee); + } + } else { + if (assetTransferPacket.sentTokenPrefix == ibcPacket.sourceChannel) + { + channelBalance[ibcPacket.destinationChannel][askToken] -= + assetTransferPacket.sentAmount; + SafeERC20.safeTransfer( + IERC20(askToken), receiver, assetTransferPacket.askAmount + ); + if (fee > 0) { + SafeERC20.safeTransfer(IERC20(askToken), relayer, fee); + } + } else { + return ZkgmLib.ACK_ERR_ONLYMAKER; + } + } + return ZkgmLib.encodeAssetTransferAck( + AssetTransferAcknowledgement({ + fillType: ZkgmLib.FILL_TYPE_PROTOCOL, + marketMaker: ZkgmLib.ACK_EMPTY + }) + ); + } + + function onAcknowledgementPacket( + IBCPacket calldata ibcPacket, + bytes calldata ack, + address relayer + ) external virtual override onlyIBC { + bytes32 packetHash = IBCPacketLib.commitPacketMemory(ibcPacket); + IBCPacket memory parent = inFlightPacket[packetHash]; + // Specific case of forwarding where the ack is threaded back directly. + if (parent.timeoutTimestamp != 0 || parent.timeoutHeight != 0) { + ibcHandler.writeAcknowledgement(parent, ack); + delete inFlightPacket[packetHash]; + } else { + ZkgmPacket calldata zkgmPacket = ZkgmLib.decode(ibcPacket.data); + Acknowledgement calldata zkgmAck = ZkgmLib.decodeAck(ack); + acknowledgeInternal( + ibcPacket, + relayer, + zkgmPacket.salt, + ZkgmLib.decodeSyscall(zkgmPacket.syscall), + zkgmAck.tag == ZkgmLib.ACK_SUCCESS, + zkgmAck.innerAck + ); + } + } + + function acknowledgeInternal( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + SyscallPacket calldata syscallPacket, + bool successful, + bytes calldata ack + ) internal { + if (syscallPacket.version != ZkgmLib.ZKGM_VERSION_0) { + revert ZkgmLib.ErrUnsupportedVersion(); + } + if (syscallPacket.index == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { + acknowledgeFungibleAssetTransfer( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeFungibleAssetTransfer(syscallPacket.packet), + successful, + ack + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_BATCH) { + acknowledgeBatch( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeBatch(syscallPacket.packet), + successful, + ack + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_FORWARD) { + acknowledgeForward( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeForward(syscallPacket.packet), + successful, + ack + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_MULTIPLEX) { + acknowledgeMultiplex( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeMultiplex(syscallPacket.packet), + successful, + ack + ); + } else { + revert ZkgmLib.ErrUnknownSyscall(); + } + } + + function acknowledgeBatch( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + BatchPacket calldata batchPacket, + bool successful, + bytes calldata ack + ) internal { + uint256 l = batchPacket.syscallPackets.length; + BatchAcknowledgement calldata batchAck = ZkgmLib.decodeBatchAck(ack); + for (uint256 i = 0; i < l; i++) { + // The syscallAck is set to the ack by default just to satisfy the + // compiler. The failure branch will never read the ack, hence the + // assignation has no effect in the recursive handling semantic. + bytes calldata syscallAck = ack; + if (successful) { + syscallAck = batchAck.acknowledgements[i]; + } + acknowledgeInternal( + ibcPacket, + relayer, + keccak256(abi.encode(salt)), + ZkgmLib.decodeSyscall(batchPacket.syscallPackets[i]), + successful, + syscallAck + ); + } + } + + function acknowledgeForward( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + ForwardPacket calldata forwardPacket, + bool successful, + bytes calldata ack + ) internal {} + + function acknowledgeMultiplex( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + MultiplexPacket calldata multiplexPacket, + bool successful, + bytes calldata ack + ) internal { + if (successful && !multiplexPacket.eureka) { + IBCPacket memory multiplexIbcPacket = IBCPacket({ + sourceChannel: ibcPacket.sourceChannel, + destinationChannel: ibcPacket.destinationChannel, + data: abi.encode( + multiplexPacket.contractAddress, + multiplexPacket.contractCalldata + ), + timeoutHeight: ibcPacket.timeoutHeight, + timeoutTimestamp: ibcPacket.timeoutTimestamp + }); + IIBCModule(address(bytes20(multiplexPacket.sender))) + .onAcknowledgementPacket(multiplexIbcPacket, ack, relayer); + } + } + + function acknowledgeFungibleAssetTransfer( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + FungibleAssetTransferPacket calldata assetTransferPacket, + bool successful, + bytes calldata ack + ) internal { + if (successful) { + AssetTransferAcknowledgement calldata assetTransferAck = + ZkgmLib.decodeAssetTransferAck(ack); + if (assetTransferAck.fillType == ZkgmLib.FILL_TYPE_PROTOCOL) { + // The protocol filled, fee was paid to relayer. + } else if ( + assetTransferAck.fillType == ZkgmLib.FILL_TYPE_MARKETMAKER + ) { + // A market maker filled, we pay with the sent asset. + address marketMaker = + address(bytes20(assetTransferAck.marketMaker)); + address sentToken = + address(bytes20(assetTransferPacket.sentToken)); + if ( + ZkgmLib.lastChannelFromPath( + assetTransferPacket.sentTokenPrefix + ) == ibcPacket.sourceChannel + ) { + IZkgmERC20(address(sentToken)).mint( + marketMaker, assetTransferPacket.sentAmount + ); + } else { + SafeERC20.safeTransfer( + IERC20(sentToken), + marketMaker, + assetTransferPacket.sentAmount + ); + } + } else { + revert ZkgmLib.ErrInvalidFillType(); + } + } else { + refund(ibcPacket.sourceChannel, assetTransferPacket); + } + } + + function onTimeoutPacket( + IBCPacket calldata ibcPacket, + address relayer + ) external virtual override onlyIBC { + bytes32 packetHash = IBCPacketLib.commitPacketMemory(ibcPacket); + IBCPacket memory parent = inFlightPacket[packetHash]; + // Specific case of forwarding where the failure is threaded back directly. + if (parent.timeoutTimestamp != 0 || parent.timeoutHeight != 0) { + ibcHandler.writeAcknowledgement( + parent, + ZkgmLib.encodeAck( + Acknowledgement({ + tag: ZkgmLib.ACK_FAILURE, + innerAck: ZkgmLib.ACK_EMPTY + }) + ) + ); + delete inFlightPacket[packetHash]; + } else { + ZkgmPacket calldata zkgmPacket = ZkgmLib.decode(ibcPacket.data); + timeoutInternal( + ibcPacket, + relayer, + zkgmPacket.salt, + ZkgmLib.decodeSyscall(zkgmPacket.syscall) + ); + } + } + + function timeoutInternal( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + SyscallPacket calldata syscallPacket + ) internal { + if (syscallPacket.version != ZkgmLib.ZKGM_VERSION_0) { + revert ZkgmLib.ErrUnsupportedVersion(); + } + if (syscallPacket.index == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { + timeoutFungibleAssetTransfer( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeFungibleAssetTransfer(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_BATCH) { + timeoutBatch( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeBatch(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_FORWARD) { + timeoutForward( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeForward(syscallPacket.packet) + ); + } else if (syscallPacket.index == ZkgmLib.SYSCALL_MULTIPLEX) { + timeoutMultiplex( + ibcPacket, + relayer, + salt, + ZkgmLib.decodeMultiplex(syscallPacket.packet) + ); + } else { + revert ZkgmLib.ErrUnknownSyscall(); + } + } + + function timeoutBatch( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + BatchPacket calldata batchPacket + ) internal { + uint256 l = batchPacket.syscallPackets.length; + for (uint256 i = 0; i < l; i++) { + timeoutInternal( + ibcPacket, + relayer, + keccak256(abi.encode(salt)), + ZkgmLib.decodeSyscall(batchPacket.syscallPackets[i]) + ); + } + } + + function timeoutForward( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + ForwardPacket calldata forwardPacket + ) internal {} + + function timeoutMultiplex( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + MultiplexPacket calldata multiplexPacket + ) internal { + if (!multiplexPacket.eureka) { + IBCPacket memory multiplexIbcPacket = IBCPacket({ + sourceChannel: ibcPacket.sourceChannel, + destinationChannel: ibcPacket.destinationChannel, + data: abi.encode( + multiplexPacket.contractAddress, + multiplexPacket.contractCalldata + ), + timeoutHeight: ibcPacket.timeoutHeight, + timeoutTimestamp: ibcPacket.timeoutTimestamp + }); + IIBCModule(address(bytes20(multiplexPacket.sender))).onTimeoutPacket( + multiplexIbcPacket, relayer + ); + } + } + + function timeoutFungibleAssetTransfer( + IBCPacket calldata ibcPacket, + address relayer, + bytes32 salt, + FungibleAssetTransferPacket calldata assetTransferPacket + ) internal { + refund(ibcPacket.sourceChannel, assetTransferPacket); + } + + function refund( + uint32 sourceChannel, + FungibleAssetTransferPacket calldata assetTransferPacket + ) internal { + address sender = address(bytes20(assetTransferPacket.sender)); + address sentToken = address(bytes20(assetTransferPacket.sentToken)); + if ( + ZkgmLib.lastChannelFromPath(assetTransferPacket.sentTokenPrefix) + == sourceChannel + ) { + IZkgmERC20(address(sentToken)).mint( + sender, assetTransferPacket.sentAmount + ); + } else { + SafeERC20.safeTransfer( + IERC20(sentToken), sender, assetTransferPacket.sentAmount + ); + } + } + + function onChanOpenInit( + uint32, + uint32, + string calldata version, + address + ) external virtual override onlyIBC { + if (keccak256(bytes(version)) != ZkgmLib.IBC_VERSION) { + revert ZkgmLib.ErrInvalidIBCVersion(); + } + } + + function onChanOpenTry( + uint32, + uint32, + uint32, + string calldata version, + string calldata counterpartyVersion, + address + ) external virtual override onlyIBC { + if (keccak256(bytes(version)) != ZkgmLib.IBC_VERSION) { + revert ZkgmLib.ErrInvalidIBCVersion(); + } + if (keccak256(bytes(counterpartyVersion)) != ZkgmLib.IBC_VERSION) { + revert ZkgmLib.ErrInvalidIBCVersion(); + } + } + + function onChanOpenAck( + uint32 channelId, + uint32, + string calldata, + address + ) external virtual override onlyIBC {} + + function onChanOpenConfirm( + uint32 channelId, + address + ) external virtual override onlyIBC {} + + function onChanCloseInit( + uint32, + address + ) external virtual override onlyIBC { + revert ZkgmLib.ErrInfiniteGame(); + } + + function onChanCloseConfirm( + uint32, + address + ) external virtual override onlyIBC { + revert ZkgmLib.ErrInfiniteGame(); + } + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} +} diff --git a/evm/contracts/apps/ucs/03-zkgm/ZkgmERC20.sol b/evm/contracts/apps/ucs/03-zkgm/ZkgmERC20.sol new file mode 100644 index 0000000000..41f78b00e3 --- /dev/null +++ b/evm/contracts/apps/ucs/03-zkgm/ZkgmERC20.sol @@ -0,0 +1,44 @@ +pragma solidity ^0.8.27; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import "./IZkgmERC20.sol"; + +contract ZkgmERC20 is ERC20, IZkgmERC20 { + error ERC20Unauthorized(); + + address public admin; + uint8 private _decimals; + + constructor(string memory n, string memory s, address a) ERC20(n, s) { + admin = a; + _decimals = 18; + } + + function decimals() + public + view + override(ERC20, IERC20Metadata) + returns (uint8) + { + return _decimals; + } + + function mint(address to, uint256 amount) external onlyAdmin { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyAdmin { + _burn(from, amount); + } + + modifier onlyAdmin() { + _checkAdmin(); + _; + } + + function _checkAdmin() internal view virtual { + if (msg.sender != admin) { + revert ERC20Unauthorized(); + } + } +} diff --git a/evm/evm.nix b/evm/evm.nix index 6ccdd03539..4240f37c34 100644 --- a/evm/evm.nix +++ b/evm/evm.nix @@ -213,7 +213,7 @@ _: { } { network = "holesky"; - rpc-url = "https://ethereum-holesky-rpc.publicnode.com"; + rpc-url = "https://1rpc.io/holesky"; private-key = ''"$1"''; extra-args = ''--verify --verifier etherscan --etherscan-api-key "$2"''; } @@ -693,6 +693,12 @@ _: { value = eth-deploy-single ({ kind = "EvmLens"; } // args); }) networks ) + // builtins.listToAttrs ( + builtins.map (args: { + name = "eth-deploy-${args.network}-ucs03"; + value = eth-deploy-single ({ kind = "UCS03"; } // args); + }) networks + ) // builtins.listToAttrs ( builtins.map (args: { name = "eth-deploy-${args.network}-multicall"; @@ -723,6 +729,18 @@ _: { ); }) networks ) + // builtins.listToAttrs ( + builtins.map (args: { + name = "eth-upgrade-${args.network}-ucs03"; + value = eth-upgrade ( + { + dry = false; + protocol = "UCS03"; + } + // args + ); + }) networks + ) // builtins.listToAttrs ( builtins.map (args: { name = "eth-upgrade-${args.network}-evm-lens-client"; diff --git a/evm/scripts/Deploy.s.sol b/evm/scripts/Deploy.s.sol index 7ed3c11bbc..e09042cbd0 100644 --- a/evm/scripts/Deploy.s.sol +++ b/evm/scripts/Deploy.s.sol @@ -20,6 +20,7 @@ import {EvmInCosmosClient} from "../contracts/clients/EvmInCosmosClient.sol"; import "../contracts/apps/ucs/00-pingpong/PingPong.sol"; import "../contracts/apps/ucs/01-relay/Relay.sol"; import "../contracts/apps/ucs/02-nft/NFT.sol"; +import "../contracts/apps/ucs/03-zkgm/Zkgm.sol"; import "../contracts/lib/Hex.sol"; import "./Deployer.sol"; @@ -56,6 +57,7 @@ library Protocols { string constant UCS00 = "ucs00"; string constant UCS01 = "ucs01"; string constant UCS02 = "ucs02"; + string constant UCS03 = "ucs03"; function make( string memory protocol @@ -188,6 +190,21 @@ abstract contract UnionScript is UnionBase { ); } + function deployUCS03( + IBCHandler handler, + address owner + ) internal returns (UCS03Zkgm) { + return UCS03Zkgm( + deploy( + Protocols.make(Protocols.UCS03), + abi.encode( + address(new UCS03Zkgm()), + abi.encodeCall(UCS03Zkgm.initialize, (handler, owner)) + ) + ) + ); + } + function deployIBC( address owner ) @@ -295,6 +312,47 @@ contract DeployEvmLens is UnionScript { } } +contract DeployUCS03 is UnionScript { + using LibString for *; + + address immutable deployer; + address immutable sender; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + } + + function getDeployer() internal view override returns (Deployer) { + return Deployer(deployer); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + uint256 privateKey = vm.envUint("PRIVATE_KEY"); + + address owner = vm.addr(privateKey); + + address handler = getDeployed(IBC.BASED); + + vm.startBroadcast(privateKey); + + UCS03Zkgm zkgm = deployUCS03(IBCHandler(handler), owner); + + vm.stopBroadcast(); + + console.log("UCS03: ", address(zkgm)); + } +} + contract DeployIBC is UnionScript { Deployer immutable deployer; @@ -414,6 +472,7 @@ contract GetDeployed is Script { address ucs00 = getDeployed(Protocols.make(Protocols.UCS00)); address ucs01 = getDeployed(Protocols.make(Protocols.UCS01)); address ucs02 = getDeployed(Protocols.make(Protocols.UCS02)); + address ucs03 = getDeployed(Protocols.make(Protocols.UCS03)); console.log( string(abi.encodePacked("Multicall: ", multicall.toHexString())) @@ -436,6 +495,7 @@ contract GetDeployed is Script { console.log(string(abi.encodePacked("UCS00: ", ucs00.toHexString()))); console.log(string(abi.encodePacked("UCS01: ", ucs01.toHexString()))); console.log(string(abi.encodePacked("UCS02: ", ucs02.toHexString()))); + console.log(string(abi.encodePacked("UCS03: ", ucs03.toHexString()))); string memory impls = "base"; @@ -541,6 +601,24 @@ contract GetDeployed is Script { ); impls.serialize(ucs02.toHexString(), proxyUCS02); + string memory proxyUCS03 = "proxyUCS03"; + proxyUCS03.serialize( + "contract", + string( + "libs/@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol:ERC1967Proxy" + ) + ); + proxyUCS03 = proxyUCS03.serialize( + "args", + abi.encode( + implOf(ucs03), + abi.encodeCall( + UCS03Zkgm.initialize, (IIBCPacket(handler), sender) + ) + ) + ); + impls.serialize(ucs03.toHexString(), proxyUCS03); + string memory implMulticall = "implMulticall"; implMulticall.serialize( "contract", string("contracts/Multicall.sol:Multicall") @@ -593,12 +671,53 @@ contract GetDeployed is Script { "contract", string("contracts/apps/ucs/02-nft/NFT.sol:UCS02NFT") ); implUCS02 = implUCS02.serialize("args", bytes(hex"")); - impls = impls.serialize(implOf(ucs01).toHexString(), implUCS02); + impls = impls.serialize(implOf(ucs02).toHexString(), implUCS02); + + string memory implUCS03 = "implUCS03"; + implUCS03.serialize( + "contract", string("contracts/apps/ucs/03-zkgm/Zkgm.sol:UCS03Zkgm") + ); + implUCS03 = implUCS03.serialize("args", bytes(hex"")); + impls = impls.serialize(implOf(ucs03).toHexString(), implUCS03); impls.write(vm.envString("OUTPUT")); } } +contract UpgradeUCS03 is Script { + using LibString for *; + + address immutable deployer; + address immutable sender; + uint256 immutable privateKey; + + constructor() { + deployer = vm.envAddress("DEPLOYER"); + sender = vm.envAddress("SENDER"); + privateKey = vm.envUint("PRIVATE_KEY"); + } + + function getDeployed( + string memory salt + ) internal view returns (address) { + return CREATE3.predictDeterministicAddress( + keccak256(abi.encodePacked(sender.toHexString(), "/", salt)), + deployer + ); + } + + function run() public { + address ucs03 = getDeployed(Protocols.make(Protocols.UCS03)); + + console.log(string(abi.encodePacked("UCS03: ", ucs03.toHexString()))); + + vm.startBroadcast(privateKey); + address newImplementation = address(new UCS03Zkgm()); + UCS03Zkgm(ucs03).upgradeToAndCall(newImplementation, new bytes(0)); + vm.stopBroadcast(); + } +} + contract UpgradeUCS00 is Script { using LibString for *; diff --git a/evm/tests/src/app/Zkgm.t.sol b/evm/tests/src/app/Zkgm.t.sol new file mode 100644 index 0000000000..1c7116c618 --- /dev/null +++ b/evm/tests/src/app/Zkgm.t.sol @@ -0,0 +1,191 @@ +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "@openzeppelin/proxy/ERC1967/ERC1967Proxy.sol"; + +import "../../../contracts/apps/ucs/03-zkgm/Zkgm.sol"; + +contract ZkgmTests is Test { + UCS03Zkgm zkgm; + + function setUp() public { + UCS03Zkgm implementation = new UCS03Zkgm(); + ERC1967Proxy proxy = new ERC1967Proxy( + address(implementation), + abi.encodeWithSelector( + UCS03Zkgm.initialize.selector, + IIBCPacket(address(this)), + address(this) + ) + ); + zkgm = UCS03Zkgm(address(proxy)); + } + + function decodeFungible( + bytes calldata b + ) public returns (FungibleAssetTransferPacket calldata) { + return ZkgmLib.decodeFungibleAssetTransfer(b); + } + + function decodeSyscall( + bytes calldata b + ) public returns (SyscallPacket calldata) { + return ZkgmLib.decodeSyscall(b); + } + + function decode( + bytes calldata b + ) public returns (ZkgmPacket calldata) { + return ZkgmLib.decode(b); + } + + function check(ZkgmPacket calldata a, bytes calldata b) public { + bytes memory x = ZkgmLib.encode(a); + console.logBytes(x); + console.logBytes(b); + assertEq(keccak256(x), keccak256(b)); + } + + function test_sendZkgmPacket() public { + bytes memory rawFungible = abi.encode( + hex"153919669Edc8A5D0c8D1E4507c9CE60435A1177", + hex"153919669Edc8A5D0c8D1E4507c9CE60435A1177", + hex"d1B482D1B947A96E96C9b76d15De34f7f70A20A1", + uint256(5), + "ChainLink Token", + "LINK", + uint256(9), + hex"779877a7b0d9e8603169ddbd7836e478b4624789", + uint256(8), + false + ); + FungibleAssetTransferPacket memory transfer = + this.decodeFungible(rawFungible); + console.log(transfer.onlyMaker); + console.log(transfer.askAmount); + console.logBytes(rawFungible); + console.logBytes( + abi.encode( + ZkgmLib.ZKGM_VERSION_0, + ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER, + rawFungible + ) + ); + + bytes memory rawZk = + hex"000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000034000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000002c00000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014153919669edc8a5d0c8d1e4507c9ce60435a11770000000000000000000000000000000000000000000000000000000000000000000000000000000000000014153919669edc8a5d0c8d1e4507c9ce60435a11770000000000000000000000000000000000000000000000000000000000000000000000000000000000000014779877a7b0d9e8603169ddbd7836e478b462478900000000000000000000000000000000000000000000000000000000000000000000000000000000000000044c494e4b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f436861696e4c696e6b20546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000142A2868c2fa4F1480A22FfE960aA4dac57f2D7a44000000000000000000000000"; + + ZkgmPacket memory p = this.decode(rawZk); + SyscallPacket memory s = this.decodeSyscall(p.syscall); + FungibleAssetTransferPacket memory f = this.decodeFungible(s.packet); + + assertEq(f.onlyMaker, false); + assertEq(f.sentTokenPrefix, 0); + + (address wrapped,) = zkgm.predictWrappedToken( + 0, 5, hex"779877a7b0d9e8603169ddbd7836e478b4624789" + ); + + console.log(wrapped); + + zkgm.onRecvPacket( + IBCPacket({ + sourceChannel: 3, + destinationChannel: 5, + data: rawZk, + timeoutHeight: 0, + timeoutTimestamp: 0 + }), + address(this), + hex"" + ); + } + + function test_lastChannelFromPathOk_1( + uint32 a + ) public { + vm.assume(a > 0); + assertEq( + ZkgmLib.lastChannelFromPath(ZkgmLib.updateChannelPath(0, a)), a + ); + } + + function test_lastChannelFromPathOk_2(uint32 a, uint32 b) public { + vm.assume(a > 0); + vm.assume(b > 0); + assertEq( + ZkgmLib.lastChannelFromPath( + ZkgmLib.updateChannelPath(ZkgmLib.updateChannelPath(0, a), b) + ), + b + ); + } + + function test_lastChannelFromPathOk_3( + uint32 a, + uint32 b, + uint32 c + ) public { + vm.assume(a > 0); + vm.assume(b > 0); + vm.assume(c > 0); + assertEq( + ZkgmLib.lastChannelFromPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath(0, a), b + ), + c + ) + ), + c + ); + } + + function test_channelPathOk( + uint32 a, + uint32 b, + uint32 c, + uint32 d, + uint32 e, + uint32 f, + uint32 g, + uint32 h + ) public { + vm.assume(a > 0); + vm.assume(b > 0); + vm.assume(c > 0); + vm.assume(d > 0); + vm.assume(e > 0); + vm.assume(f > 0); + vm.assume(g > 0); + vm.assume(h > 0); + assertEq( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath( + ZkgmLib.updateChannelPath(0, a), b + ), + c + ), + d + ), + e + ), + f + ), + g + ), + h + ), + uint256(a) | uint256(b) << 32 | uint256(c) << 64 | uint256(d) << 96 + | uint256(e) << 128 | uint256(f) << 160 | uint256(g) << 192 + | uint256(h) << 224 + ); + } +}