From d05aa2d7358c10f3e879bde49d0597d3d239138c Mon Sep 17 00:00:00 2001 From: Hussein Ait Lahcen Date: Wed, 27 Nov 2024 23:57:55 +0100 Subject: [PATCH] feat(zkgm): mint/unescrow mechanism for fungible assets --- evm/contracts/apps/ucs/03-zkgm/IZkgmERC20.sol | 9 + evm/contracts/apps/ucs/03-zkgm/Zkgm.sol | 444 ++++++++++++++++-- evm/contracts/apps/ucs/03-zkgm/ZkgmERC20.sol | 44 ++ 3 files changed, 450 insertions(+), 47 deletions(-) create mode 100644 evm/contracts/apps/ucs/03-zkgm/IZkgmERC20.sol create mode 100644 evm/contracts/apps/ucs/03-zkgm/ZkgmERC20.sol 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 index 0a6720d274..63e854014b 100644 --- a/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol +++ b/evm/contracts/apps/ucs/03-zkgm/Zkgm.sol @@ -1,21 +1,47 @@ pragma solidity ^0.8.27; +import "@openzeppelin/token/ERC20/IERC20.sol"; +import "solady/utils/CREATE3.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 Acknowledgement { + uint256 tag; + bytes innerAck; +} + +struct BatchAcknowledgement { + bytes[] acknowledgements; +} + +struct AssetTransferAcknowledgement { + uint256 fillType; + bytes marketMaker; +} struct ZkgmPacket { - uint8 version; bytes32 salt; - uint8 syscallIndex; + bytes syscall; +} + +struct SyscallPacket { + uint8 version; + uint8 index; bytes packet; } struct ForwardPacket { uint32 channelId; - bytes zkgmPacket; + uint64 timeoutHeight; + uint64 timeoutTimestamp; + bytes syscallPacket; } struct MultiplexPacket { @@ -26,7 +52,7 @@ struct MultiplexPacket { } struct BatchPacket { - bytes[] zkgmPackets; + bytes[] syscallPackets; } struct FungibleAssetTransferPacket { @@ -35,13 +61,20 @@ struct FungibleAssetTransferPacket { bytes sentToken; uint256 sentAmount; bytes askToken; - bytes askAmount; + uint256 askAmount; + bool onlyMaker; } library ZkgmLib { bytes public constant ACK_EMPTY = hex""; - bytes public constant ACK_FAILURE = abi.encode(0x00); - bytes public constant ACK_SUCCESS = abi.encode(0x01); + + 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; @@ -56,6 +89,57 @@ library ZkgmLib { error ErrUnknownSyscall(); error ErrInfiniteGame(); error ErrUnauthorized(); + error ErrInvalidAmount(); + error ErrOnlyMaker(); + error ErrInvalidFillType(); + + 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 @@ -73,6 +157,16 @@ library ZkgmLib { 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) { @@ -112,12 +206,24 @@ library ZkgmLib { } return packet; } + + function isDeployed( + address addr + ) internal returns (bool) { + uint32 size = 0; + assembly { + size := extcodesize(addr) + } + return (size > 0); + } } contract Zkgm is IBCAppBase { using ZkgmLib for *; IBCHandler private ibcHandler; + mapping(bytes32 => IBCPacket) private inFlightPacket; + mapping(uint32 => mapping(address => uint256)) private channelBalance; constructor( IBCHandler _ibcHandler @@ -143,11 +249,27 @@ contract Zkgm is IBCAppBase { // 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 abi.encode(ZkgmLib.ACK_SUCCESS, acknowledgement); + return ZkgmLib.encodeAck( + Acknowledgement({ + tag: ZkgmLib.ACK_SUCCESS, + innerAck: acknowledgement + }) + ); } } else { - return ZkgmLib.ACK_FAILURE; + return ZkgmLib.encodeAck( + Acknowledgement({ + tag: ZkgmLib.ACK_FAILURE, + innerAck: ZkgmLib.ACK_EMPTY + }) + ); } } @@ -166,10 +288,8 @@ contract Zkgm is IBCAppBase { ibcPacket, relayer, relayerMsg, - zkgmPacket.version, zkgmPacket.salt, - zkgmPacket.syscallIndex, - zkgmPacket.packet + ZkgmLib.decodeSyscall(zkgmPacket.syscall) ); } @@ -177,45 +297,43 @@ contract Zkgm is IBCAppBase { IBCPacket calldata ibcPacket, address relayer, bytes calldata relayerMsg, - uint8 version, bytes32 salt, - uint8 syscallIndex, - bytes calldata packet - ) public returns (bytes memory) { - if (version != ZkgmLib.ZKGM_VERSION_0) { + SyscallPacket calldata syscallPacket + ) internal returns (bytes memory) { + if (syscallPacket.version != ZkgmLib.ZKGM_VERSION_0) { revert ZkgmLib.ErrUnsupportedVersion(); } - if (syscallIndex == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { + if (syscallPacket.index == ZkgmLib.SYSCALL_FUNGIBLE_ASSET_TRANSFER) { return executeFungibleAssetTransfer( ibcPacket, relayer, relayerMsg, salt, - ZkgmLib.decodeFungibleAssetTransfer(packet) + ZkgmLib.decodeFungibleAssetTransfer(syscallPacket.packet) ); - } else if (syscallIndex == ZkgmLib.SYSCALL_BATCH) { + } else if (syscallPacket.index == ZkgmLib.SYSCALL_BATCH) { return executeBatch( ibcPacket, relayer, relayerMsg, salt, - ZkgmLib.decodeBatch(packet) + ZkgmLib.decodeBatch(syscallPacket.packet) ); - } else if (syscallIndex == ZkgmLib.SYSCALL_FORWARD) { + } else if (syscallPacket.index == ZkgmLib.SYSCALL_FORWARD) { return executeForward( ibcPacket, relayer, relayerMsg, salt, - ZkgmLib.decodeForward(packet) + ZkgmLib.decodeForward(syscallPacket.packet) ); - } else if (syscallIndex == ZkgmLib.SYSCALL_MULTIPLEX) { + } else if (syscallPacket.index == ZkgmLib.SYSCALL_MULTIPLEX) { return executeMultiplex( ibcPacket, relayer, relayerMsg, salt, - ZkgmLib.decodeMultiplex(packet) + ZkgmLib.decodeMultiplex(syscallPacket.packet) ); } else { revert ZkgmLib.ErrUnknownSyscall(); @@ -229,25 +347,25 @@ contract Zkgm is IBCAppBase { bytes32 salt, BatchPacket calldata batchPacket ) internal returns (bytes memory) { - uint256 l = batchPacket.zkgmPackets.length; - bytes[] memory acknowledgements = new bytes[](l); + uint256 l = batchPacket.syscallPackets.length; + bytes[] memory acks = new bytes[](l); for (uint256 i = 0; i < l; i++) { - ZkgmPacket calldata zkgmPacket = - ZkgmLib.decode(batchPacket.zkgmPackets[i]); - acknowledgements[i] = executeInternal( + SyscallPacket calldata syscallPacket = + ZkgmLib.decodeSyscall(batchPacket.syscallPackets[i]); + acks[i] = executeInternal( ibcPacket, relayer, relayerMsg, - zkgmPacket.version, - keccak256(abi.encode(salt, zkgmPacket.salt)), - zkgmPacket.syscallIndex, - zkgmPacket.packet + keccak256(abi.encode(salt)), + syscallPacket ); - if (acknowledgements[i].length == 0) { + if (acks[i].length == 0) { revert ZkgmLib.ErrBatchMustBeSync(); } } - return abi.encode(acknowledgements); + return ZkgmLib.encodeBatchAck( + BatchAcknowledgement({acknowledgements: acks}) + ); } function executeForward( @@ -257,7 +375,21 @@ contract Zkgm is IBCAppBase { bytes32 salt, ForwardPacket calldata forwardPacket ) internal returns (bytes memory) { - revert ZkgmLib.ErrUnimplemented(); + IBCPacket memory sentPacket = ibcHandler.sendPacket( + forwardPacket.channelId, + forwardPacket.timeoutHeight, + forwardPacket.timeoutTimestamp, + ZkgmLib.encode( + ZkgmPacket({ + salt: keccak256(abi.encode(salt)), + 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( @@ -273,12 +405,14 @@ contract Zkgm is IBCAppBase { IEurekaModule(contractAddress).onZkgm( multiplexPacket.sender, multiplexPacket.contractCalldata ); - return ZkgmLib.ACK_SUCCESS; + return abi.encode(ZkgmLib.ACK_SUCCESS); } else { IBCPacket memory multiplexIbcPacket = IBCPacket({ sourceChannel: ibcPacket.sourceChannel, destinationChannel: ibcPacket.destinationChannel, - data: multiplexPacket.contractCalldata, + data: abi.encode( + multiplexPacket.sender, multiplexPacket.contractCalldata + ), timeoutHeight: ibcPacket.timeoutHeight, timeoutTimestamp: ibcPacket.timeoutTimestamp }); @@ -297,6 +431,16 @@ contract Zkgm is IBCAppBase { } } + function predictWrappedToken( + uint32 channel, + bytes calldata token + ) internal returns (address, bytes32) { + bytes32 wrappedTokenSalt = keccak256(abi.encode(channel, token)); + address wrappedToken = + CREATE3.predictDeterministicAddress(wrappedTokenSalt); + return (wrappedToken, wrappedTokenSalt); + } + function executeFungibleAssetTransfer( IBCPacket calldata ibcPacket, address relayer, @@ -304,19 +448,225 @@ contract Zkgm is IBCAppBase { bytes32 salt, FungibleAssetTransferPacket calldata assetTransferPacket ) internal returns (bytes memory) { - revert ZkgmLib.ErrUnimplemented(); + if (assetTransferPacket.onlyMaker) { + return ZkgmLib.ACK_ERR_ONLYMAKER; + } + if (assetTransferPacket.askAmount > assetTransferPacket.sentAmount) { + revert ZkgmLib.ErrInvalidAmount(); + } + (address wrappedToken, bytes32 wrappedTokenSalt) = predictWrappedToken( + ibcPacket.destinationChannel, assetTransferPacket.sentToken + ); + address askToken = address(bytes20(assetTransferPacket.askToken)); + address receiver = address(bytes20(assetTransferPacket.receiver)); + uint256 fee = + assetTransferPacket.sentAmount - assetTransferPacket.askAmount; + if (askToken == wrappedToken) { + if (!ZkgmLib.isDeployed(wrappedToken)) { + CREATE3.deployDeterministic( + abi.encodePacked( + type(ZkgmERC20).creationCode, "test", "test" + ), + wrappedTokenSalt + ); + } + IZkgmERC20(wrappedToken).mint( + receiver, assetTransferPacket.askAmount + ); + if (fee > 0) { + IZkgmERC20(wrappedToken).mint(relayer, fee); + } + } else { + channelBalance[ibcPacket.destinationChannel][askToken] -= + assetTransferPacket.askAmount; + IERC20(askToken).transfer(receiver, assetTransferPacket.askAmount); + if (fee > 0) { + IERC20(askToken).transfer(relayer, fee); + } + } + return ZkgmLib.encodeAssetTransferAck( + AssetTransferAcknowledgement({ + fillType: ZkgmLib.FILL_TYPE_PROTOCOL, + marketMaker: ZkgmLib.ACK_EMPTY + }) + ); } function onAcknowledgementPacket( - IBCPacket calldata, - bytes calldata acknowledgement, - address - ) external virtual override onlyIBC {} + 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 { + 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) { + IERC20(address(bytes20(assetTransferPacket.sentToken))).transfer( + address(bytes20(assetTransferAck.marketMaker)), + assetTransferPacket.sentAmount + ); + } else { + revert ZkgmLib.ErrInvalidFillType(); + } + } function onTimeoutPacket( - IBCPacket calldata, - address - ) external virtual override onlyIBC {} + IBCPacket calldata ibcPacket, + address relayer + ) external virtual override onlyIBC { + ZkgmPacket calldata zkgmPacket = ZkgmLib.decode(ibcPacket.data); + acknowledgeInternal( + ibcPacket, + relayer, + zkgmPacket.salt, + ZkgmLib.decodeSyscall(zkgmPacket.syscall), + false, + ibcPacket.data + ); + } function onChanOpenInit( uint32, 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(); + } + } +}