diff --git a/lib/vibc-core-smart-contracts b/lib/vibc-core-smart-contracts index 07362dd..8938907 160000 --- a/lib/vibc-core-smart-contracts +++ b/lib/vibc-core-smart-contracts @@ -1 +1 @@ -Subproject commit 07362dd18a7caed421a9ba5c4fcbca56c75373b8 +Subproject commit 893890793a5cb2d5d534604b2d9aec67b4fb0c28 diff --git a/script/Deploy.sol b/script/Deploy.sol index 01a31d7..b8baeb4 100644 --- a/script/Deploy.sol +++ b/script/Deploy.sol @@ -9,7 +9,7 @@ import { BaseMultiChainDeployer} from "./BaseMultiChainDeployer.s.sol"; // Import all the Apps for deployment here. import { IncentivizedMockEscrow } from "../src/apps/mock/IncentivizedMockEscrow.sol"; import { IncentivizedWormholeEscrow } from "../src/apps/wormhole/IncentivizedWormholeEscrow.sol"; -import { IncentivizedPolymerEscrow } from "../src/apps/polymer/IncentivizedPolymerEscrow.sol"; +import { UniversalPolymerEscrow } from "../src/apps/polymer/UniversalPolymerEscrow.sol"; contract DeployGeneralisedIncentives is BaseMultiChainDeployer { using stdJson for string; @@ -81,7 +81,7 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { address expectedAddress = _getAddress( abi.encodePacked( - type(IncentivizedPolymerEscrow).creationCode, + type(UniversalPolymerEscrow).creationCode, abi.encode(vm.envAddress("SEND_LOST_GAS_TO"), polymerBridgeContract) ), salt @@ -90,7 +90,7 @@ contract DeployGeneralisedIncentives is BaseMultiChainDeployer { // Check if it is already deployed. If it is, we skip. if (expectedAddress.codehash != bytes32(0)) return expectedAddress; - IncentivizedPolymerEscrow polymerEscrow = new IncentivizedPolymerEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), polymerBridgeContract); + UniversalPolymerEscrow polymerEscrow = new UniversalPolymerEscrow{salt: salt}(vm.envAddress("SEND_LOST_GAS_TO"), polymerBridgeContract); incentive = address(polymerEscrow); } else { diff --git a/script/bridge_contracts.json b/script/bridge_contracts.json index d10d54b..96b1199 100644 --- a/script/bridge_contracts.json +++ b/script/bridge_contracts.json @@ -23,12 +23,12 @@ }, "Polymer": { "basesepolia": { - "bridge": "0x50E32e236bfE4d514f786C9bC80061637dd5AF98", - "escrow": "0x147d414F1964c94c4Ac7422aC148aBddb4BEdC82" + "bridge": "0x9fcd52449261F732d017F8BD1CaCDc3dFbcD0361", + "escrow": "0x0000000000000000000010000000000000000000" }, "optimismsepolia": { - "bridge": "0x34a0e37cCCEdaC70EC1807e5a1f6A4a91D4AE0Ce", - "escrow": "0x62e27C0bD89D7AC599Df8765cDEe0183A34A9929" + "bridge": "0xE2029629f51ab994210d671Dc08b7Ec94899b278", + "escrow": "0x0000000000000000000010000000000000000000" } } } \ No newline at end of file diff --git a/src/apps/polymer/APolymerEscrow.sol b/src/apps/polymer/APolymerEscrow.sol new file mode 100644 index 0000000..a2ff078 --- /dev/null +++ b/src/apps/polymer/APolymerEscrow.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {IncentivizedMessageEscrow} from "../../IncentivizedMessageEscrow.sol"; +import "../../MessagePayload.sol"; + +/// @notice Scaffolding for Polymer Escrows +abstract contract APolymerEscrow is IncentivizedMessageEscrow { + error NonVerifiableMessage(); + error NotImplemented(); + + struct VerifiedMessageHashContext { + bytes32 chainIdentifier; + bytes implementationIdentifier; + } + + mapping(bytes32 => VerifiedMessageHashContext) public isVerifiedMessageHash; + + constructor(address sendLostGasTo) + IncentivizedMessageEscrow(sendLostGasTo) + {} + + function estimateAdditionalCost() external pure returns (address asset, uint256 amount) { + asset = address(0); + amount = 0; + } + + function _uniqueSourceIdentifier() internal view override returns (bytes32 sourceIdentifier) { + return sourceIdentifier = bytes32(block.chainid); + } + + function _proofValidPeriod(bytes32 /* destinationIdentifier */ ) internal pure override returns (uint64) { + return 0; + } + + /** @dev Disable processPacket */ + function processPacket( + bytes calldata, /* messagingProtocolContext */ + bytes calldata, /* rawMessage */ + bytes32 /* feeRecipitent */ + ) external payable override { + revert NotImplemented(); + } + + /** @dev Disable reemitAckMessage. Polymer manages the entire flow, so we don't need to worry about expired proofs. */ + function reemitAckMessage( + bytes32 /* sourceIdentifier */, + bytes calldata /* implementationIdentifier */, + bytes calldata /* receiveAckWithContext */ + ) external payable override { + revert NotImplemented(); + } + + /** @dev Disable timeoutMessage */ + function timeoutMessage( + bytes32 /* sourceIdentifier */, + bytes calldata /* implementationIdentifier */, + uint256 /* originBlockNumber */, + bytes calldata /* message */ + ) external payable override { + revert NotImplemented(); + } + + /// @notice This function is used to allow acks to be executed twice (if the first one ran out of gas) + /// This is not intended to allow processPacket to work. + function _verifyPacket(bytes calldata, /* messagingProtocolContext */ bytes calldata _message) + internal + view + override + returns (bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) + { + sourceIdentifier = isVerifiedMessageHash[keccak256(_message)].chainIdentifier; + implementationIdentifier = isVerifiedMessageHash[keccak256(_message)].implementationIdentifier; + + if (sourceIdentifier == bytes32(0)) revert NonVerifiableMessage(); + + message_ = _message; + } +} diff --git a/src/apps/polymer/IncentivizedPolymerEscrow.sol b/src/apps/polymer/UniversalPolymerEscrow.sol similarity index 58% rename from src/apps/polymer/IncentivizedPolymerEscrow.sol rename to src/apps/polymer/UniversalPolymerEscrow.sol index 0a9ab3a..fffc3e7 100644 --- a/src/apps/polymer/IncentivizedPolymerEscrow.sol +++ b/src/apps/polymer/UniversalPolymerEscrow.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; -import {IncentivizedMessageEscrow} from "../../IncentivizedMessageEscrow.sol"; +import {APolymerEscrow} from "./APolymerEscrow.sol"; import "../../MessagePayload.sol"; import {AckPacket} from "vibc-core-smart-contracts/libs/Ibc.sol"; @@ -13,83 +13,14 @@ import { } from "vibc-core-smart-contracts/interfaces/IbcMiddleware.sol"; /// @notice Polymer implementation of the Generalised Incentives based on vIBC. -contract IncentivizedPolymerEscrow is IncentivizedMessageEscrow, IbcMwUser, IbcUniversalPacketReceiver { - error NotEnoughGasProvidedForVerification(); - error NonVerifiableMessage(); - error NotImplemented(); - - struct VerifiedMessageHashContext { - bytes32 chainIdentifier; - bytes implementationIdentifier; - } - - mapping(bytes32 => VerifiedMessageHashContext) public isVerifiedMessageHash; - // packet will timeout if it's delivered on the destination chain after (this block time + _TIMEOUT_AFTER_BLOCK). - uint64 constant _TIMEOUT_AFTER_BLOCK = 1 days; +contract UniversalPolymerEscrow is APolymerEscrow, IbcMwUser, IbcUniversalPacketReceiver { constructor(address sendLostGasTo, address messagingProtocol) - IncentivizedMessageEscrow(sendLostGasTo) + APolymerEscrow(sendLostGasTo) IbcMwUser(messagingProtocol) {} - function estimateAdditionalCost() external pure returns (address asset, uint256 amount) { - asset = address(0); - amount = 0; - } - - function _uniqueSourceIdentifier() internal view override returns (bytes32 sourceIdentifier) { - return sourceIdentifier = bytes32(block.chainid); - } - - function _proofValidPeriod(bytes32 /* destinationIdentifier */ ) internal pure override returns (uint64) { - return 0; - } - - /** @dev Disable processPacket */ - function processPacket( - bytes calldata, /* messagingProtocolContext */ - bytes calldata, /* rawMessage */ - bytes32 /* feeRecipitent */ - ) external payable override { - revert NotImplemented(); - } - - /** @dev Disable reemitAckMessage. Polymer manages the entire flow, so we don't need to worry about expired proofs. */ - function reemitAckMessage( - bytes32 /* sourceIdentifier */, - bytes calldata /* implementationIdentifier */, - bytes calldata /* receiveAckWithContext */ - ) external payable override { - revert NotImplemented(); - } - - /** @dev Disable timeoutMessage */ - function timeoutMessage( - bytes32 /* sourceIdentifier */, - bytes calldata /* implementationIdentifier */, - uint256 /* originBlockNumber */, - bytes calldata /* message */ - ) external payable override { - revert NotImplemented(); - } - - /// @notice This function is used to allow acks to be executed twice (if the first one ran out of gas) - /// This is not intended to allow processPacket to work. - function _verifyPacket(bytes calldata, /* messagingProtocolContext */ bytes calldata _message) - internal - view - override - returns (bytes32 sourceIdentifier, bytes memory implementationIdentifier, bytes calldata message_) - { - sourceIdentifier = isVerifiedMessageHash[keccak256(_message)].chainIdentifier; - implementationIdentifier = isVerifiedMessageHash[keccak256(_message)].implementationIdentifier; - - if (sourceIdentifier == bytes32(0)) revert NonVerifiableMessage(); - - message_ = _message; - } - - // packet.srcPortAddr is the IncentivizedPolymerEscrow address on the source chain. + // packet.srcPortAddr is the UniversalPolymerEscrow address on the source chain. // packet.destPortAddr is the address of this contract. // channelId: the universal channel id from the running chain's perspective, which can be used to identify the counterparty chain. function onRecvUniversalPacket(bytes32 channelId, UniversalPacket calldata packet) @@ -145,7 +76,7 @@ contract IncentivizedPolymerEscrow is IncentivizedMessageEscrow, IbcMwUser, IbcU * Each universal channel/channelId represents a directional path from the running chain to a destination chain. * Universal ChannelIds should _destChainIdToChannelIdd from the Polymer registry. * Although everyone is free to establish their own channels, they're not "officially" vetted until they're in the Polymer registry. - * @param destinationImplementation IncentivizedPolymerEscrow address on the counterparty chain. + * @param destinationImplementation UniversalPolymerEscrow address on the counterparty chain. * @param message packet payload * @param deadline Packet will timeout after the dest chain's block time in nanoseconds since the epoch passes timeoutTimestamp. */ diff --git a/src/apps/polymer/vIBCEscrow.sol b/src/apps/polymer/vIBCEscrow.sol new file mode 100644 index 0000000..60b4133 --- /dev/null +++ b/src/apps/polymer/vIBCEscrow.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import { APolymerEscrow } from "./APolymerEscrow.sol"; +import "../../MessagePayload.sol"; + +import "vibc-core-smart-contracts/interfaces/IbcDispatcher.sol"; +import { AckPacket } from "vibc-core-smart-contracts/libs/Ibc.sol"; +import { IbcReceiverBase, IbcReceiver, IbcPacket } from "vibc-core-smart-contracts/interfaces/IbcReceiver.sol"; + +/** + * @notice Polymer implementation of the Generalised Incentives based on vIBC. + * @dev An implementation quirk of Polymer is that channels map 1:1 to BOTH chains + * and contracts. As a result, if we trust a channel, we also imply that we trust + * the contract on the other end of that channel. This is unlike "traditional" chain + * mappings where there may be may addresses on the other end. + * As a result, we are allowed to just append our address to the package and then trust that. + * Because if someone trust the channel (which is a requirement) then they must also + * trust the account AND the set value. + */ +contract IncentivizedPolymerEscrow is APolymerEscrow, IbcReceiverBase, IbcReceiver { + error UnsupportedChannelOrder(); + + uint constant POLYMER_SENDER_IDENTIFIER_START = 0; + uint constant POLYMER_SENDER_IDENTIFIER_END = 32; + uint constant POLYMER_PACKAGE_PAYLOAD_START = 32; + + bytes32[] public connectedChannels; + string[] public supportedVersions = ["1.0", "2.0"]; + + // Make a shortcut to save a bit of gas. + bytes32 immutable ADDRESS_THIS = bytes32(uint256(uint160(address(this)))); + + constructor(address sendLostGasTo, address dispatcher) + APolymerEscrow(sendLostGasTo) + IbcReceiverBase(IbcDispatcher(dispatcher)) + {} + + //--- IBC Channel Callbacks ---// + // Inspired by Mars.sol: https://github.com/open-ibc/vibc-core-smart-contracts/blob/v2.0.0-rc4/contracts/examples/Mars.sol + + function triggerChannelInit( + string calldata version, + ChannelOrder ordering, + bool feeEnabled, + string[] calldata connectionHops, + string calldata counterpartyPortId + ) external onlyOwner { + if (ordering != ChannelOrder.NONE && ordering != ChannelOrder.UNORDERED) revert UnsupportedChannelOrder(); + dispatcher.channelOpenInit(version, ordering, feeEnabled, connectionHops, counterpartyPortId); + } + + function onChanOpenInit(ChannelOrder ordering, string[] calldata, string calldata, string calldata version) + external + view + virtual + onlyIbcDispatcher + returns (string memory selectedVersion) + { + if (ordering != ChannelOrder.NONE && ordering != ChannelOrder.UNORDERED) revert UnsupportedChannelOrder(); + return _openChannel(version); + } + + function onChanOpenTry( + ChannelOrder ordering, + string[] memory, + bytes32 channelId, + string memory, + bytes32, + string calldata counterpartyVersion + ) external virtual onlyIbcDispatcher returns (string memory selectedVersion) { + if (ordering != ChannelOrder.NONE && ordering != ChannelOrder.UNORDERED) revert UnsupportedChannelOrder(); + return _connectChannel(channelId, counterpartyVersion); + } + + function onChanOpenAck(bytes32 channelId, bytes32, string calldata counterpartyVersion) external virtual onlyIbcDispatcher { + _connectChannel(channelId, counterpartyVersion); + } + + function onChanOpenConfirm(bytes32 channelId) external onlyIbcDispatcher {} + + function _connectChannel(bytes32 channelId, string calldata counterpartyVersion) private returns (string memory version) { + unchecked { + // ensure negotiated version is supported + for (uint256 i = 0; i < supportedVersions.length; ++i) { + if (keccak256(abi.encodePacked(counterpartyVersion)) == keccak256(abi.encodePacked(supportedVersions[i]))) { + connectedChannels.push(channelId); + return counterpartyVersion; + } + } + revert UnsupportedVersion(); + } + } + + function _openChannel(string calldata version) private view returns (string memory selectedVersion) { + unchecked { + for (uint256 i = 0; i < supportedVersions.length; ++i) { + if (keccak256(abi.encodePacked(version)) == keccak256(abi.encodePacked(supportedVersions[i]))) { + return version; + } + } + revert UnsupportedVersion(); + } + } + + function onCloseIbcChannel(bytes32 channelId, string calldata, bytes32) external virtual onlyIbcDispatcher { + // logic to determin if the channel should be closed + bool channelFound = false; + for (uint256 i = 0; i < connectedChannels.length; i++) { + if (connectedChannels[i] == channelId) { + delete connectedChannels[i]; + channelFound = true; + break; + } + } + if (!channelFound) revert ChannelNotFound(); + } + + //--- IBC Packet Callbacks ---// + + // packet.srcPortAddr is the IncentivizedPolymerEscrow address on the source chain. + // packet.destPortAddr is the address of this contract. + // channelId: the channel id from the running chain's perspective, which can be used to identify the counterparty chain. + function onRecvPacket(IbcPacket calldata packet) + external override + onlyIbcDispatcher + returns (AckPacket memory) + { + uint256 gasLimit = gasleft(); + bytes32 feeRecipitent = bytes32(uint256(uint160(tx.origin))); + + // Collect the implementation identifier we added. Remember, this is trusted IFF packet.src.channelId is trusted. + // sourceImplementationIdentifier has already been defined by the channel on channel creation. + bytes memory sourceImplementationIdentifier = packet.data[POLYMER_SENDER_IDENTIFIER_START:POLYMER_SENDER_IDENTIFIER_END]; + + bytes memory receiveAck = _handleMessage( + packet.src.channelId, + sourceImplementationIdentifier, + packet.data[POLYMER_PACKAGE_PAYLOAD_START: ], + feeRecipitent, + gasLimit + ); + + // Send ack: + return AckPacket({success: true, data: bytes.concat(ADDRESS_THIS, receiveAck)}); + } + + function onAcknowledgementPacket( + IbcPacket calldata packet, + AckPacket calldata ack + ) + external override + onlyIbcDispatcher + { + uint256 gasLimit = gasleft(); + bytes32 feeRecipitent = bytes32(uint256(uint160(tx.origin))); + + // Collect the implementation identifier we added. Remember, this is trusted IFF packet.src.channelId is trusted. + bytes memory destinationImplementationIdentifier = ack.data[POLYMER_SENDER_IDENTIFIER_START:POLYMER_SENDER_IDENTIFIER_END]; + + // Get the payload by removing the implementation identifier. + bytes calldata rawMessage = ack.data[POLYMER_PACKAGE_PAYLOAD_START:]; + + // Set a verificaiton context so we can recover the ack. + isVerifiedMessageHash[keccak256(rawMessage)] = VerifiedMessageHashContext({ + chainIdentifier: packet.src.channelId, + implementationIdentifier: destinationImplementationIdentifier + }); + _handleAck(packet.src.channelId, destinationImplementationIdentifier, rawMessage, feeRecipitent, gasLimit); + } + + function onTimeoutPacket(IbcPacket calldata packet) external override onlyIbcDispatcher{ + uint256 gasLimit = gasleft(); + bytes32 feeRecipitent = bytes32(uint256(uint160(tx.origin))); + + // We added a bytes32 implementation identifier. Remove it. + bytes calldata rawMessage = packet.data[POLYMER_PACKAGE_PAYLOAD_START:]; + bytes32 messageIdentifier = bytes32(rawMessage[MESSAGE_IDENTIFIER_START:MESSAGE_IDENTIFIER_END]); + address fromApplication = address(uint160(bytes20(rawMessage[FROM_APPLICATION_START_EVM:FROM_APPLICATION_END]))); + _handleTimeout( + packet.src.channelId, messageIdentifier, fromApplication, rawMessage[CTX0_MESSAGE_START:], feeRecipitent, gasLimit + ); + } + + /** + * @param destinationChainIdentifier Channel ID. It's always from the running chain's perspective. + * Each channel/channelId represents a directional path from the running chain to a destination chain + * AND destination implementation. If we trust a channelId, then it is also implied that we trust the + * implementation deployed there. + * @param message Packet payload. We will add our address to it. This is to standardize with other implementations where we return the destination. + * @param deadline Packet will timeout after the dest chain's block time in nanoseconds since the epoch passes timeoutTimestamp. If set to 0 we set it to type(uint64).max. + */ + function _sendPacket( + bytes32 destinationChainIdentifier, + bytes memory /* destinationImplementation */, + bytes memory message, + uint64 deadline + ) internal override returns (uint128 costOfsendPacketInNativeToken) { + // If timeoutTimestamp is set to 0, set it to maximum. + uint64 timeoutTimestamp = deadline > 0 ? deadline : type(uint64).max; + + dispatcher.sendPacket( + destinationChainIdentifier, + bytes.concat(ADDRESS_THIS, message), + timeoutTimestamp + ); + return 0; + } +}