Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deliver messages directly to Polymer Dispatcher #31

Merged
merged 9 commits into from
Apr 20, 2024
6 changes: 3 additions & 3 deletions script/Deploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
79 changes: 79 additions & 0 deletions src/apps/polymer/APolymerEscrow.sol
Original file line number Diff line number Diff line change
@@ -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.
alfredo-stonk marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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)
Expand Down Expand Up @@ -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.
*/
Expand Down
181 changes: 181 additions & 0 deletions src/apps/polymer/vIBCEscrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// 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 } 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 ChannelNotFound();
error UnsupportedVersion();

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 constant VERSION = 'vIBC Escrow 1.0';
reednaa marked this conversation as resolved.
Show resolved Hide resolved

// 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 ---//

function onOpenIbcChannel(
string calldata version,
ChannelOrder /* */,
reednaa marked this conversation as resolved.
Show resolved Hide resolved
bool,
string[] calldata,
CounterParty calldata counterparty
) external view onlyIbcDispatcher returns (string memory selectedVersion) {
if (counterparty.channelId == bytes32(0)) {
// ChanOpenInit
if (
keccak256(abi.encodePacked(version)) != keccak256(abi.encodePacked(VERSION))
) revert UnsupportedVersion();
} else {
// ChanOpenTry
if (
keccak256(abi.encodePacked(counterparty.version)) != keccak256(abi.encodePacked(VERSION))
) revert UnsupportedVersion();
}
return VERSION;
}
reednaa marked this conversation as resolved.
Show resolved Hide resolved

function onConnectIbcChannel(
bytes32 channelId,
bytes32,
string calldata counterpartyVersion
) external onlyIbcDispatcher {
if (
keccak256(abi.encodePacked(counterpartyVersion)) != keccak256(abi.encodePacked(VERSION))
) revert UnsupportedVersion();
connectedChannels.push(channelId);
}

function onCloseIbcChannel(bytes32 channelId, string calldata, bytes32) external onlyIbcDispatcher {
unchecked {
// 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;
return;
// We could also break but early return saves gas.
}
}
if (!channelFound) revert ChannelNotFound();

}
}
reednaa marked this conversation as resolved.
Show resolved Hide resolved

//--- 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);
Comment on lines +164 to +169

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If _handleAck reverts, isVerifiedMessaageHash state wouldn't be mutated. So how to recover the ack? this is related to the question above too

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good question. What is the execution strictness when delivered from Polymer?
Is it required for applications that the on....Packet functions never revert?
So we need to call _handleAck within a try statement?
What happens if onAcknowledgementPacket reverts from `Polymer's perspective?

Generally, _handleAck should never revert if the relayer provides enough gas for the application. It is only if the relayer cheaps out the function can revert.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Polymer side, all packet callback functions on...Packet are effectively try-catched, eg. onAcknowledgementPacket(bool success, bytes memory data) = _callIfContract(. So it's ok if IBC-dApps revert in any callback functions.

In your case, since retry is handled by recoverAck, I'd recommend you wrap _handleAck in try-catch, so to record acks even when _handleAck reverts due to lack of gas.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The try-catch implementation in the dispatcher is vulnerable to gas denial of service for large gas spenders. This is because max 63/64 of the gas is forwarded. There should be an gas check, otherwise there is a soft-max gas limit for Polymer applications.

Something like this:

unchecked {
if (!success) if(gasleft() < maxGasAck * 1 / 63) revert NotEnoughGasExecution();
}

This is not a catch-all solution since reverting by spending all gas will block from the applications side. But at least that will be the applications fault.

Looking further through your implementation, this is not an immediate using it for our testnet purposes.

Our contracts should not revert when packages are properly relayed and as a result, the try-catch in itself is not what we worry about. Furthermore, there is still a way to process the acks since acks are not written (on Polymer) until they execute without failure.

}

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