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
129 changes: 129 additions & 0 deletions modules/contracts/src.sol/transferDefinitions/CrosschainTransfer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// 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);
resolver.preImage = bytes32(0);
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 can be 0 for crosschain contract calls
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"
);

require(
state.initiator != address(0) && state.responder != address(0),
"CrosschainTransfer: EMPTY_SIGNERS"
);

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

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;
}
}
2 changes: 2 additions & 0 deletions modules/engine/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export class ParameterConversionError extends EngineError {
FeeGreaterThanAmount: "Fees charged are greater than amount",
NoOp: "Cannot create withdrawal with 0 amount and no call",
WithdrawToZero: "Cannot withdraw to AddressZero",
ChannelNotFound: "Channel not found",
TransferNotFound: "Transfer not found",
} as const;

constructor(
Expand Down
20 changes: 14 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 Down
17 changes: 17 additions & 0 deletions modules/engine/src/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,23 @@ export const isWithdrawTransfer = async (
return Result.ok(transfer.transferDefinition === definition);
};

export const isCrosschainTransfer = async (
transfer: FullTransferState,
chainAddresses: ChainAddresses,
chainService: IVectorChainReader,
): Promise<Result<boolean, ChainError>> => {
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
81 changes: 78 additions & 3 deletions modules/engine/src/paramConverter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { WithdrawCommitment } from "@connext/vector-contracts";
import { getRandomBytes32, getSignerAddressFromPublicIdentifier } from "@connext/vector-utils";
import {
getRandomBytes32,
getSignerAddressFromPublicIdentifier,
recoverAddressFromChannelMessage,
} from "@connext/vector-utils";
import {
CreateTransferParams,
ResolveTransferParams,
Expand All @@ -21,12 +25,15 @@ import {
IMessagingService,
DEFAULT_FEE_EXPIRY,
SetupParams,
IVectorChainService,
IEngineStore,
} from "@connext/vector-types";
import { BigNumber } from "@ethersproject/bignumber";
import { AddressZero } from "@ethersproject/constants";
import { getAddress } from "@ethersproject/address";

import { ParameterConversionError } from "./errors";
import { isCrosschainTransfer } from "./listeners";

export async function convertSetupParams(
params: EngineParams.Setup,
Expand Down Expand Up @@ -198,12 +205,80 @@ export async function convertConditionalTransferParams(
});
}

export function convertResolveConditionParams(
export async function convertResolveConditionParams(
params: EngineParams.ResolveTransfer,
transfer: FullTransferState,
): Result<ResolveTransferParams, EngineError> {
signer: IChannelSigner,
chainAddresses: ChainAddresses,
chainService: IVectorChainReader,
store: IEngineStore,
): Promise<Result<ResolveTransferParams, EngineError>> {
const { channelAddress, transferResolver, meta } = params;

// 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, chainAddresses, chainService);
if (isCrossChain.getValue()) {
// 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 store.getChannelState(transfer.channelAddress);
} catch (e) {
return Result.fail(
new ParameterConversionError(
ParameterConversionError.reasons.ChannelNotFound,
transfer.channelAddress,
signer.publicIdentifier,
{
getChannelStateError: jsonifyError(e),
},
),
);
}
if (!channel) {
return Result.fail(
new ParameterConversionError(
ParameterConversionError.reasons.ChannelNotFound,
transfer.channelAddress,
signer.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,
signer.address,
transfer.assetId,
withdrawalAmount.toString(),
nonce,
callTo,
callData,
);
let recovered: string;
try {
recovered = await recoverAddressFromChannelMessage(commitment.hashToSign(), 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) {
// Generate your signature on the withdrawal commitment
transferResolver.responderSignature = await signer.signMessage(commitment.hashToSign());
}
await commitment.addSignatures(initiatorSignature, transferResolver.responderSignature);
// Store the double signed commitment
await store.saveWithdrawalCommitment(transfer.transferId, commitment.toJson());
}

return Result.ok({
channelAddress,
transferId: transfer.transferId,
Expand Down
12 changes: 8 additions & 4 deletions modules/engine/src/testing/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import {
getRandomBytes32,
mkPublicIdentifier,
mkAddress,
createTestFullHashlockTransferState,
createTestFullCrosschainTransferState,
createTestChannelState,
ChannelSigner,
} from "@connext/vector-utils";
import Sinon from "sinon";

Expand All @@ -35,12 +39,12 @@ describe("VectorEngine", () => {
const validAddress = mkAddress("0xc");
const invalidAddress = "abc";

let storeService: IEngineStore;
let storeService: Sinon.SinonStubbedInstance<IEngineStore>;
let chainService: Sinon.SinonStubbedInstance<VectorChainService>;
beforeEach(() => {
storeService = Sinon.createStubInstance(MemoryStoreService, {
getChannelStates: Promise.resolve([]),
});
storeService = Sinon.createStubInstance(MemoryStoreService);
storeService.getChannelStates.resolves([]);
storeService.getTransferState.resolves(createTestFullHashlockTransferState());
chainService = Sinon.createStubInstance(VectorChainService);

chainService.getChainProviders.returns(Result.ok(env.chainProviders));
Expand Down
Loading