Skip to content
This repository was archived by the owner on Dec 27, 2022. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from 13 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
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);
}

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"
);
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");

// Amount recipient is able to withdraw > 0.
require(
balance.amount[1] == 0,
"CrosschainTransfer: NONZERO_RECIPIENT_BALANCE"
);

// 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;
}
}
81 changes: 75 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 @@ -885,11 +887,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 +907,67 @@ 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;
console.log("transfer: ", 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,
this.signer.address,
transfer.assetId,
withdrawalAmount.toString(),
nonce,
callTo,
callData,
);
console.log("commitment: ", commitment.toJson());
let recovered: string;
try {
recovered = await recoverAddressFromChannelMessage(
commitment.hashToSign(),
params.transferResolver.responderSignature,
);
} catch (e) {
recovered = e.message;
}

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