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

ics-29 and incentivized ack #15

Merged
merged 17 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 118 additions & 27 deletions contracts/Dispatcher.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ pragma solidity ^0.8.9;

import '@openzeppelin/contracts/utils/Strings.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

import './Ibc.sol';
import './IbcDispatcher.sol';
import './IbcReceiver.sol';
import './IbcVerifier.sol';
import 'hardhat/console.sol';
import {Escrow} from './Escrow.sol';

// InitClientMsg is used to create a new Polymer client on an EVM chain
// TODO: replace bytes with explictly typed fields for gas cost saving
Expand Down Expand Up @@ -37,11 +36,11 @@ contract Dispatcher is IbcDispatcher, Ownable {
//
// channel events
//

event OpenIbcChannel(
address indexed portAddress,
string version,
ChannelOrder ordering,
bool feeEnabled,
string[] connectionHops,
string counterpartyPortId,
bytes32 counterpartyChannelId
Expand All @@ -54,7 +53,6 @@ contract Dispatcher is IbcDispatcher, Ownable {
//
// packet events
//

event SendPacket(
address indexed sourcePortAddress,
bytes32 indexed sourceChannelId,
Expand Down Expand Up @@ -91,9 +89,8 @@ contract Dispatcher is IbcDispatcher, Ownable {
//
// fields
//

ZKMintVerifier verifier;
address payable escrow;
Escrow escrow;
bool isClientCreated = false;
ConsensusState public latestConsensusState;
OptimisticConsensusState public trustedOptimisticConsensusState;
Expand Down Expand Up @@ -124,13 +121,15 @@ contract Dispatcher is IbcDispatcher, Ownable {
// methods
//

constructor(ZKMintVerifier _verifier,
address payable _escrow,
string memory initPortPrefix,
uint32 fraudProofWindowSeconds_) {
constructor(
ZKMintVerifier _verifier,
Escrow _escrow,
string memory initPortPrefix,
uint32 fraudProofWindowSeconds_
) {
verifier = _verifier;
// require(_escrow != address(0), 'Escrow cannot be zero address');
escrow = _escrow;
require(escrow != address(0), 'Escrow cannot be zero address');

// initialize portPrefix
portPrefix = initPortPrefix;
Expand Down Expand Up @@ -245,8 +244,10 @@ contract Dispatcher is IbcDispatcher, Ownable {
// check if the current untrusted op consensus state is outside the fraud proof window and
// set it to be the trusted op consensus state if so.
function tryPromotePendingOpConsensusState() internal {
if (block.timestamp > pendingOptimisticConsensusState.time + fraudProofWindowSeconds &&
pendingOptimisticConsensusState.height != 0) {
if (
block.timestamp > pendingOptimisticConsensusState.time + fraudProofWindowSeconds &&
pendingOptimisticConsensusState.height != 0
) {
trustedOptimisticConsensusState = pendingOptimisticConsensusState;
pendingOptimisticConsensusState = OptimisticConsensusState(0, 0, 0, 0);
}
Expand Down Expand Up @@ -308,8 +309,10 @@ contract Dispatcher is IbcDispatcher, Ownable {
// window.
function updateClientWithOptimisticConsensusState(OptimisticConsensusState calldata opConsensusState) external {
require(isClientCreated, 'Client not created');
require(opConsensusState.height >= pendingOptimisticConsensusState.height,
'UpdateClientMsg proof verification failed: must update to a newer op consensus state');
require(
opConsensusState.height >= pendingOptimisticConsensusState.height,
'UpdateClientMsg proof verification failed: must update to a newer op consensus state'
);
pendingOptimisticConsensusState = opConsensusState;

// Use the timestamp on the EVM chain since the timestamp
Expand Down Expand Up @@ -372,6 +375,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
IbcReceiver portAddress,
string calldata version,
ChannelOrder ordering,
bool feeEnabled,
string[] calldata connectionHops,
CounterParty calldata counterparty,
Proof calldata proof
Expand Down Expand Up @@ -402,6 +406,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
string memory selectedVersion = portAddress.onOpenIbcChannel(
version,
ordering,
feeEnabled,
connectionHops,
counterparty.portId,
counterparty.channelId,
Expand All @@ -412,6 +417,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
address(portAddress),
selectedVersion,
ordering,
feeEnabled,
connectionHops,
counterparty.portId,
counterparty.channelId
Expand All @@ -428,6 +434,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
bytes32 channelId,
string[] calldata connectionHops,
ChannelOrder ordering,
bool feeEnabled,
string calldata counterpartyPortId,
bytes32 counterpartyChannelId,
string calldata counterpartyVersion,
Expand All @@ -449,6 +456,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
portChannelMap[address(portAddress)][channelId] = Channel(
counterpartyVersion, // TODO: this should be self version instead of counterparty version
ordering,
feeEnabled,
connectionHops,
counterpartyPortId,
counterpartyChannelId
Expand All @@ -467,7 +475,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
* @param portAddress EVM address of the IBC port
* @param channelId IBC channel ID from the port perspective
* @return A channel struct is always returned. If it doesn't exists, the channel struct is populated with default
values per EVM.
* values per EVM.
*/
function getChannel(address portAddress, bytes32 channelId) external view returns (Channel memory) {
return portChannelMap[portAddress][channelId];
Expand Down Expand Up @@ -523,9 +531,9 @@ contract Dispatcher is IbcDispatcher, Ownable {
* @param packet The packet data to send.
* @param timeoutTimestamp The timestamp in nanoseconds after which the packet times out if it has not been received.
* @param fee The fee serves as the packet incentive for forward relay. It's escrowed on the running chain and will be
claimed by relayer later once the packet is delivered and ack'ed or timed out.
recvFee is always paid, but only ackFee or timeoutFee is paid, depending on packet path.
Total fee for packet roundtrip is determined by recvFee + max(ackFee, timeoutFee).
* claimed by relayer later once the packet is delivered and ack'ed or timed out.
* recvFee is always paid, but only ackFee or timeoutFee is paid, depending on packet path.
* Total fee for packet roundtrip is determined by recvFee + max(ackFee, timeoutFee).
*/
function sendPacket(
bytes32 channelId,
Expand All @@ -542,10 +550,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
require(sequence > 0, 'Invalid packet sequence');

// escescrow packet fee
uint256 escrowedFeeee = fee.recvFee + max(fee.ackFee, fee.timeoutFee); // only need to pay either ack or timeout fee, but not both
// ignore returned data from `call`
// (bool sent, bytes memory _data) = escrow.call{value: escrowedFeeee}('');
(bool sent, ) = escrow.call{value: escrowedFeeee}('');
(bool sent, ) = address(escrow).call{value: Ibc.calcEscrowFee(fee)}('');
require(sent, 'Failed to escrow packet fee');
// record packet fees
packetFees[msg.sender][channelId][sequence] = fee;
Expand All @@ -561,6 +566,8 @@ contract Dispatcher is IbcDispatcher, Ownable {
/**
* Pay extra fees for a packet that has already been sent.
* Dapps should call this function to incentivize packet relay if the packet is not ack'ed or timed out yet.
* @notice This function can be called multiple times for the same packet. But it shouldn't be called if the
* channel is not incentivized.
*/
function payPacketFeeAsync(
address portAddress,
Expand All @@ -573,8 +580,7 @@ contract Dispatcher is IbcDispatcher, Ownable {
require(hasCommitment, 'Packet commitment not found');

// escrow packet fee
uint256 maxFee = fee.recvFee + max(fee.ackFee, fee.timeoutFee); // only need to pay either ack or timeout fee, but not both
(bool sent, ) = escrow.call{value: maxFee}('');
(bool sent, ) = address(escrow).call{value: Ibc.calcEscrowFee(fee)}('');
require(sent, 'Failed to escrow packet fee');

// Record accumulated packet fees
Expand All @@ -589,8 +595,8 @@ contract Dispatcher is IbcDispatcher, Ownable {
/**
* @notice Handle the acknowledgement of an IBC packet by the counterparty
* @dev Verifies the given proof and calls the `onAcknowledgementPacket` function on the given `receiver` contract,
ie. the IBC dApp.
Prerequisite: the original packet is committed and not ack'ed or timed out yet.
* ie. the IBC dApp.
* Prerequisite: the original packet is committed and not ack'ed or timed out yet.
* @param receiver The IbcReceiver contract that should handle the packet acknowledgement event
* If the address doesn't satisfy the interface, the transaction will be reverted.
* @param packet The IbcPacket data for the acknowledged packet
Expand Down Expand Up @@ -618,6 +624,52 @@ contract Dispatcher is IbcDispatcher, Ownable {

// enforce ack'ed packet sequences always increment by 1 for ordered channels
Channel memory channel = portChannelMap[address(receiver)][packet.src.channelId];
require(!channel.feeEnabled, 'invalid channel type: incentivized');

if (channel.ordering == ChannelOrder.ORDERED) {
require(
packet.sequence == nextSequenceAck[address(receiver)][packet.src.channelId],
'Unexpected packet sequence'
);
nextSequenceAck[address(receiver)][packet.src.channelId] = packet.sequence + 1;
}

receiver.onAcknowledgementPacket(packet, ackPacket);

// delete packet commitment to avoid double ack
delete sendPacketCommitment[address(receiver)][packet.src.channelId][packet.sequence];

emit Acknowledgement(address(receiver), packet.src.channelId, packet.sequence);
}

/**
* @notice Handle the incentivized acknowledgement of an IBC packet by the counterparty
* @dev Verifies the given proof and calls the `onAcknowledgementPacket` function on the given `receiver` contract,
* ie. the IBC dApp.
* Prerequisite: the original packet is committed and not ack'ed or timed out yet.
* @param receiver The dApp contract that should handle the app-level packet acknowledgement
* @param packet The original IbcPacket data sent by the dApp
* @param incentivizedAck The incentivized acknowledgement from counterparty chain, where the relayer is the payee address on behalf of the forward relayer that delivered the packet
* @param proof The membership proof to verify the packet acknowledgement committed on Polymer chain
*/
function incentivizedAcknowledgement(
nicopernas marked this conversation as resolved.
Show resolved Hide resolved
IbcReceiver receiver,
IbcPacket calldata packet,
IncentivizedAckPacket calldata incentivizedAck,
Proof calldata proof
) external {
// verify `receiver` is the original packet sender
require(portIdAddressMatch(address(receiver), packet.src.portId), 'Receiver is not the original packet sender');
// prove ack packet is on Polymer chain
verifyMembership(proof, 'ack/packet/path', 'expected ack receipt hash on Polymer chain', 'Fail to prove ack');

// verify packet has been committed and not yet ack'ed or timed out
bool hasCommitment = sendPacketCommitment[address(receiver)][packet.src.channelId][packet.sequence];
require(hasCommitment, 'Packet commitment not found');

// enforce ack'ed packet sequences always increment by 1 for ordered channels
Channel memory channel = portChannelMap[address(receiver)][packet.src.channelId];
require(channel.feeEnabled, 'invalid channel type: non-incentivized');
if (channel.ordering == ChannelOrder.ORDERED) {
require(
packet.sequence == nextSequenceAck[address(receiver)][packet.src.channelId],
Expand All @@ -626,6 +678,33 @@ contract Dispatcher is IbcDispatcher, Ownable {
nextSequenceAck[address(receiver)][packet.src.channelId] = packet.sequence + 1;
}

// distribute fee to relayer
require(incentivizedAck.relayer.length == 20, 'Invalid relayer address');
nicopernas marked this conversation as resolved.
Show resolved Hide resolved
address payable recvFeePayee;
if (keccak256(abi.encodePacked(incentivizedAck.relayer)) == keccak256(abi.encodePacked(address(0)))) {
// no forward relayer registered on Polymer, then refund to receiver
recvFeePayee = payable(address(receiver));
} else {
// pay forward relayer's payee
recvFeePayee = payable(address(bytes20(incentivizedAck.relayer)));
}

// TODO: allow reverse relayer registration too
address payable ackFeePayee = payable(tx.origin);

// transfer recv and ack fees
PacketFee storage packetFee = packetFees[address(receiver)][packet.src.channelId][packet.sequence];
escrow.distributeAckFees([recvFeePayee, ackFeePayee], [packetFee.recvFee, packetFee.ackFee]);
// refund extra packet fee to packet sender, ie. receiver dApp
// TODO: allow refund payee registration too
uint refundFee = Ibc.ackRefundAmount(packetFee);
if (refundFee > 0) {
escrow.refund(payable(address(receiver)), refundFee);
}

// pass a regular ack packet to callback
AckPacket memory ackPacket = AckPacket(incentivizedAck.success, incentivizedAck.data);

receiver.onAcknowledgementPacket(packet, ackPacket);

// delete packet commitment to avoid double ack
Expand Down Expand Up @@ -653,6 +732,18 @@ contract Dispatcher is IbcDispatcher, Ownable {
bool hasCommitment = sendPacketCommitment[address(receiver)][packet.src.channelId][packet.sequence];
require(hasCommitment, 'Packet commitment not found');

PacketFee storage packetFee = packetFees[address(receiver)][packet.src.channelId][packet.sequence];
if (packetFee.timeoutFee != 0) {
// transfer timeout fee
address payable timeoutFeePayee = payable(tx.origin);
escrow.distributeTimeoutFee(timeoutFeePayee, packetFee.timeoutFee);
}
uint timeoutRefund = Ibc.timeoutRefundAmount(packetFee);
if (timeoutRefund > 0) {
// refund extra packet fee to packet sender, ie. receiver dApp
escrow.refund(payable(address(receiver)), timeoutRefund);
}

receiver.onTimeoutPacket(packet);

// delete packet commitment to avoid double timeout
Expand Down
74 changes: 74 additions & 0 deletions contracts/Escrow.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//SPDX-License-Identifier: UNLICENSED

pragma solidity ^0.8.9;

import '@openzeppelin/contracts/utils/Strings.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
import './Ibc.sol';
import './IbcDispatcher.sol';
import './IbcReceiver.sol';
import './IbcVerifier.sol';

contract Escrow is Ownable {
/// Polymer dispatcher contract address.
/// Only dispatcher can call `distributeFee` to distribute packet fee to relayers.
address public dispatcher;

/// if locked, no fee can be transferred out of Escrow
bool public locked = false;

/// This function is called for plain Ether transfers, i.e. for every call with empty calldata.
receive() external payable {}

/// setDispatcher sets the dispatcher contract address
function setDispatcher(address _dispatcher) external onlyOwner {
dispatcher = _dispatcher;
}

/// lockEscrow disables `distributedFee` function
function lockEscrow() external onlyOwner {
locked = true;
}

/// unlockEscrow enables `distributedFee` function
function unlockEscrow() external onlyOwner {
locked = false;
}

/// Distribute the packet recv and ack fees to forward and reverse relayers' payees accounts.
/// It can only be called by the dispatcher contract.
function distributeAckFees(address payable[2] memory relayers, uint256[2] memory fees) external {
// preconditions check
require(!locked, 'Escrow is locked');
require(msg.sender == dispatcher, 'Only dispatcher can call distributeFee');

for (uint256 i = 0; i < relayers.length; i++) {
_transfer(relayers[i], fees[i]);
}
}

/// Distribute packet timeout fee to forward relayer's payee account.
/// It can only be called by the dispatcher contract.
function distributeTimeoutFee(address payable relayer, uint256 fee) external {
// preconditions check
require(!locked, 'Escrow is locked');
require(msg.sender == dispatcher, 'Only dispatcher can call distributeFee');

_transfer(relayer, fee);
}

/// refund returns the extra packet fee to packet sender
function refund(address payable sender, uint256 amount) external {
// preconditions check
require(!locked, 'Escrow is locked');
require(msg.sender == dispatcher, 'Only dispatcher can call refund');

_transfer(sender, amount);
}

/// _transfer transfers the given amount of Ether to the given address.
function _transfer(address payable payee, uint256 amount) internal {
(bool sent, ) = payee.call{value: amount}('');
require(sent, 'Failed to transfer Ether');
}
}
Loading