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..cd8d446a4b --- /dev/null +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -0,0 +1,1050 @@ +pragma solidity ^0.8.27; + +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/25-handler/IBCHandler.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 = abi.encode(0xDEADC0DE); + + 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); + } + + 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); + } + + function decodeBatchAck( + bytes calldata stream + ) internal pure returns (BatchAcknowledgement calldata) { + BatchAcknowledgement calldata acks; + assembly { + acks := stream.offset + } + return acks; + } + + function encodeAck( + Acknowledgement memory packet + ) internal pure returns (bytes memory) { + return abi.encode(packet); + } + + 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); + } + + 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 Zkgm is IBCAppBase { + using ZkgmLib for *; + using LibString for *; + + IBCHandler private ibcHandler; + mapping(bytes32 => IBCPacket) private inFlightPacket; + mapping(uint32 => mapping(address => uint256)) private channelBalance; + mapping(address => uint256) tokenOrigin; + + constructor( + IBCHandler _ibcHandler + ) { + 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( + 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 + ); + } + 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 acknowledgement) = address(this).call( + abi.encodeWithSelector( + this.execute.selector, packet, packet.data, relayer, relayerMsg + ) + ); + if (success) { + // The acknowledgement may be asynchronous (forward/multiplex) + if (acknowledgement.length == 0) { + return ZkgmLib.ACK_EMPTY; + } else if ( + keccak256(acknowledgement) + == keccak256(ZkgmLib.ACK_ERR_ONLYMAKER) + ) { + // Special case where we should avoid the packet from being + // received entirely as it is only fillable by a market maker. + revert ZkgmLib.ErrOnlyMaker(); + } else { + 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, + bytes calldata rawZkgmPacket + ) public returns (bytes memory) { + // Only callable through the onRecvPacket endpoint. + if (msg.sender != address(this)) { + revert ZkgmLib.ErrUnauthorized(); + } + ZkgmPacket calldata zkgmPacket = ZkgmLib.decode(rawZkgmPacket); + 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 predictWrappedToken( + uint256 path, + uint32 destinationChannel, + bytes calldata token + ) internal returns (address, bytes32) { + bytes32 wrappedTokenSalt = + keccak256(abi.encode(path, destinationChannel, token)); + address wrappedToken = + CREATE3.predictDeterministicAddress(wrappedTokenSalt); + return (wrappedToken, wrappedTokenSalt); + } + + 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) = predictWrappedToken( + 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, + assetTransferPacket.sentSymbol, + assetTransferPacket.sentName + ), + 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.askAmount; + 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 { + 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(); + } +} 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..95578bb842 --- /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) ERC20(n, s) { + admin = msg.sender; + _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/scripts/Deploy.s.sol b/evm/scripts/Deploy.s.sol index ba1b35eb24..d0ebc406b8 100644 --- a/evm/scripts/Deploy.s.sol +++ b/evm/scripts/Deploy.s.sol @@ -15,6 +15,7 @@ import {CosmosInCosmosClient} from 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 "solady/utils/CREATE3.sol"; diff --git a/evm/tests/src/app/Zkgm.t.sol b/evm/tests/src/app/Zkgm.t.sol new file mode 100644 index 0000000000..605aab1ff4 --- /dev/null +++ b/evm/tests/src/app/Zkgm.t.sol @@ -0,0 +1,94 @@ +pragma solidity ^0.8.27; + +import "forge-std/Test.sol"; + +import "../../../contracts/apps/ucs/03-zkgm/Zkgm.sol"; + +contract ZkgmTests is Test { + 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 + ); + } +}