Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

Crosschain transfer #658

Draft
wants to merge 18 commits into
base: 0.2.5-beta.18
Choose a base branch
from
Draft
4 changes: 3 additions & 1 deletion modules/contracts/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const func: DeployFunction = async () => {
["ChannelFactory", ["ChannelMastercopy", Zero]],
["HashlockTransfer", []],
["Withdraw", []],
["CrosschainTransfer", []],
["TransferRegistry", []],
["TestToken", []],
];
Expand All @@ -93,14 +94,15 @@ const func: DeployFunction = async () => {

// Default: run standard migration
} else {
log.info(`Running testnet migration`);
log.info(`Running standard migration`);
for (const row of standardMigration) {
const name = row[0] as string;
const args = row[1] as Array<string | BigNumber>;
await migrate(name, args);
}
await registerTransfer("Withdraw", deployer);
await registerTransfer("HashlockTransfer", deployer);
await registerTransfer("CrosschainTransfer", deployer);
}

if ([1337, 5].includes(network.config.chainId ?? 0)) {
Expand Down
143 changes: 143 additions & 0 deletions modules/contracts/src.sol/transferDefinitions/CrosschainTransfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.1;
pragma experimental ABIEncoderV2;

import "./TransferDefinition.sol";
import "../lib/LibChannelCrypto.sol";

/// @title CrosschainTransfer
/// @author Connext <support@connext.network>
/// @notice This contract burns the initiator's funds if a mutually signed
/// transfer can be generated

contract CrosschainTransfer is TransferDefinition {
using LibChannelCrypto for bytes32;

struct TransferState {
bytes initiatorSignature;
address initiator;
address responder;
bytes32 data;
uint256 nonce; // Included so that each transfer commitment has a unique hash.
uint256 fee;
address callTo;
bytes callData;
bytes32 lockHash;
}

struct TransferResolver {
bytes responderSignature;
bytes32 preImage;
}

// Provide registry information.
string public constant override Name = "CrosschainTransfer";
string public constant override StateEncoding =
"tuple(bytes initiatorSignature, address initiator, address responder, bytes32 data, uint256 nonce, uint256 fee, address callTo, bytes callData, bytes32 lockHash)";
string public constant override ResolverEncoding =
"tuple(bytes responderSignature, bytes32 preImage)";

function EncodedCancel() external pure override returns (bytes memory) {
TransferResolver memory resolver;
resolver.responderSignature = new bytes(65);
return abi.encode(resolver);
}
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved

function create(bytes calldata encodedBalance, bytes calldata encodedState)
external
pure
override
returns (bool)
{
// Get unencoded information.
TransferState memory state = abi.decode(encodedState, (TransferState));
Balance memory balance = abi.decode(encodedBalance, (Balance));

// Ensure data and nonce provided.
require(state.data != bytes32(0), "CrosschainTransfer: EMPTY_DATA");
require(state.nonce != uint256(0), "CrosschainTransfer: EMPTY_NONCE");

// Initiator balance must be greater than 0 and include amount for fee.
require(
balance.amount[0] > 0,
"CrosschainTransfer: ZER0_SENDER_BALANCE"
);
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved
require(
state.fee <= balance.amount[0],
"CrosschainTransfer: INSUFFICIENT_BALANCE"
);

// Recipient balance must be 0.
require(
balance.amount[1] == 0,
"CrosschainTransfer: NONZERO_RECIPIENT_BALANCE"
);

// Valid lockHash to secure funds must be provided.
require(
state.lockHash != bytes32(0),
"CrosschainTransfer: EMPTY_LOCKHASH"
);

require(
state.data.checkSignature(
state.initiatorSignature,
state.initiator
),
"CrosschainTransfer: INVALID_INITIATOR_SIG"
);

// Valid initial transfer state
return true;
}
Comment on lines +93 to +94
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Things that are not yet validated in the state:

  1. responder
  2. callTo
  3. callData
  4. balance.to

These are probably all okay, but this means that anything could be put into these values and a transfer could be created that is potentially unresolvable (i.e. what if responder isn't actually a proper address?)


function resolve(
bytes calldata encodedBalance,
bytes calldata encodedState,
bytes calldata encodedResolver
) external pure override returns (Balance memory) {
TransferState memory state = abi.decode(encodedState, (TransferState));
TransferResolver memory resolver =
abi.decode(encodedResolver, (TransferResolver));
Balance memory balance = abi.decode(encodedBalance, (Balance));

// Ensure data and nonce provided.
require(state.data != bytes32(0), "CrosschainTransfer: EMPTY_DATA");
require(state.nonce != uint256(0), "CrosschainTransfer: EMPTY_NONCE");

rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved
// Amount recipient is able to withdraw > 0.
require(
balance.amount[1] == 0,
"CrosschainTransfer: NONZERO_RECIPIENT_BALANCE"
);
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved

// Transfer must have two valid parties.
require(
state.initiator != address(0) && state.responder != address(0),
"CrosschainTransfer: EMPTY_SIGNERS"
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be in the create


require(
state.data.checkSignature(
resolver.responderSignature,
state.responder
),
"CrosschainTransfer: INVALID_RESPONDER_SIG"
);

// Check hash for normal payment unlock.
bytes32 generatedHash = sha256(abi.encode(resolver.preImage));
require(
state.lockHash == generatedHash,
"CrosschainTransfer: INVALID_PREIMAGE"
);

// Reduce CrosschainTransfer amount to optional fee.
// It's up to the offchain validators to ensure that the
// CrosschainTransfer commitment takes this fee into account.
balance.amount[1] = state.fee;
balance.amount[0] = 0;

return balance;
}
}
79 changes: 73 additions & 6 deletions modules/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ import {
MinimalTransaction,
WITHDRAWAL_RESOLVED_EVENT,
VectorErrorJson,
FullTransferState,
} from "@connext/vector-types";
import {
generateMerkleTreeData,
recoverAddressFromChannelMessage,
validateChannelUpdateSignatures,
getSignerAddressFromPublicIdentifier,
getRandomBytes32,
Expand All @@ -41,6 +42,7 @@ import {
import pino from "pino";
import Ajv from "ajv";
import { Evt } from "evt";
import { BigNumber } from "@ethersproject/bignumber";

import { version } from "../package.json";

Expand All @@ -51,7 +53,7 @@ import {
convertSetupParams,
convertWithdrawParams,
} from "./paramConverter";
import { setupEngineListeners } from "./listeners";
import { isCrosschainTransfer, setupEngineListeners } from "./listeners";
import { getEngineEvtContainer, withdrawRetryForTransferId, addTransactionToCommitment } from "./utils";
import { sendIsAlive } from "./isAlive";
import { WithdrawCommitment } from "@connext/vector-contracts";
Expand Down Expand Up @@ -877,6 +879,7 @@ export class VectorEngine implements IVectorEngine {
const validate = ajv.compile(EngineParams.ResolveTransferSchema);
const valid = validate(params);
if (!valid) {
console.log("validate.errors: ", validate.errors);
return Result.fail(
new RpcError(RpcError.reasons.InvalidParams, params.channelAddress ?? "", this.publicIdentifier, {
invalidParamsError: validate.errors?.map((e) => e.message).join(","),
Expand All @@ -885,11 +888,17 @@ export class VectorEngine implements IVectorEngine {
);
}

const transferRes = await this.getTransferState({ transferId: params.transferId });
if (transferRes.isError) {
return Result.fail(transferRes.getError()!);
let transfer: FullTransferState | undefined;
try {
transfer = await this.store.getTransferState(params.transferId);
} catch (e) {
return Result.fail(
new RpcError(RpcError.reasons.TransferNotFound, params.channelAddress ?? "", this.publicIdentifier, {
transferId: params.transferId,
getTransferStateError: jsonifyError(e),
}),
);
}
const transfer = transferRes.getValue();
if (!transfer) {
return Result.fail(
new RpcError(RpcError.reasons.TransferNotFound, params.channelAddress ?? "", this.publicIdentifier, {
Expand All @@ -899,6 +908,64 @@ export class VectorEngine implements IVectorEngine {
}
this.logger.info({ transfer, method, methodId }, "Transfer pre-resolve");

// special case for crosschain transfer
// we need to generate a separate sig for withdrawal commitment since the transfer resolver may have gotten forwarded
// and needs to be regenerated for this leg of the transfer
const isCrossChain = await isCrosschainTransfer(transfer, this.chainAddresses, this.chainService);
if (isCrossChain) {
// first check if the provided sig is valid. in the case of the receiver directly resolving the withdrawal, it will
// be valid already
let channel: FullChannelState | undefined;
try {
channel = await this.store.getChannelState(transfer.channelAddress);
} catch (e) {
return Result.fail(
new RpcError(RpcError.reasons.ChannelNotFound, transfer.channelAddress, this.publicIdentifier, {
getChannelStateError: jsonifyError(e),
}),
);
}
if (!channel) {
return Result.fail(
new RpcError(RpcError.reasons.ChannelNotFound, transfer.channelAddress, this.publicIdentifier),
);
}
const {
transferState: { nonce, initiatorSignature, fee, callTo, callData, balance },
} = transfer;
const withdrawalAmount = balance.amount.reduce((prev, curr) => prev.add(curr), BigNumber.from(0)).sub(fee);
const commitment = new WithdrawCommitment(
channel.channelAddress,
channel.alice,
channel.bob,
transfer.balance.amount[0],
transfer.assetId,
withdrawalAmount.toString(),
nonce,
callTo,
callData,
);
let recovered: string;
try {
recovered = await recoverAddressFromChannelMessage(
commitment.hashToSign(),
params.transferResolver.responderSignature,
);
} catch (e) {
recovered = e.message;
}
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved

// if it is not valid, regenerate the sig, otherwise use the provided one
if (recovered !== channel.alice && recovered !== channel.bob) {
this.logger.info({ method, methodId }, "Crosschain transfer signature invalid, regenerating sig");
// Generate your signature on the withdrawal commitment
params.transferResolver.responderSignature = await this.signer.signMessage(commitment.hashToSign());
}
await commitment.addSignatures(initiatorSignature, params.transferResolver.responderSignature);
// Store the double signed commitment
await this.store.saveWithdrawalCommitment(transfer.transferId, commitment.toJson());
}

// First, get translated `create` params using the passed in conditional transfer ones
const resolveResult = convertResolveConditionParams(params, transfer);
if (resolveResult.isError) {
Expand Down
19 changes: 19 additions & 0 deletions modules/engine/src/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,7 @@ export const isWithdrawTransfer = async (
chainAddresses: ChainAddresses,
chainService: IVectorChainReader,
): Promise<Result<boolean, ChainError>> => {
// TODO: cache this!
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved
const withdrawInfo = await chainService.getRegisteredTransferByName(
TransferNames.Withdraw,
chainAddresses[transfer.chainId].transferRegistryAddress,
Expand All @@ -1036,6 +1037,24 @@ export const isWithdrawTransfer = async (
return Result.ok(transfer.transferDefinition === definition);
};

export const isCrosschainTransfer = async (
transfer: FullTransferState,
chainAddresses: ChainAddresses,
chainService: IVectorChainReader,
): Promise<Result<boolean, ChainError>> => {
// TODO: cache this!
const crosschainInfo = await chainService.getRegisteredTransferByName(
TransferNames.CrosschainTransfer,
chainAddresses[transfer.chainId].transferRegistryAddress,
transfer.chainId,
);
if (crosschainInfo.isError) {
return Result.fail(crosschainInfo.getError()!);
}
const { definition } = crosschainInfo.getValue();
return Result.ok(transfer.transferDefinition === definition);
};

export const resolveWithdrawal = async (
channelState: FullChannelState,
transfer: FullTransferState,
Expand Down
Loading