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