Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@connext/vector-contracts
Advanced tools
Smart contracts powering Connext's minimalist channel platform
The contracts module contains the core solidity files that back Vector's security onchain.
Do not modify these contracts unless you know exactly what you are doing.
Contents:
In ~/vector
(root), run:
make contracts
to build just the contracts & it's dependenciesmake test-contracts
to run the testsmake watch-contracts
to test in watch-modeThe contracts are structured as follows:
To simplify the implementation and support the required featureset, the contracts adopt the following principles/assumptions:
[initiator, responder]
and signed into the initial channel state when setting up the channel.CoreChannelState
that is signed by both channel participants.depositA
function. The responder simply sends funds to the contract. (This allows for very powerful end-user experiences).create
ing and resolve
ing conditional transfers. Creating a transfer generates a CoreTransferState
which gets hashed and added to the merkleRoot
that is a part of the signed CoreChannelState
for that update. Resolving a transfer removes the hash from the merkleRoot
.
resolve
update. This happens through a transferDefinition
, a pure
or view
contract of the following interface (TODO) which outputs a final balance post-transfer.create
->resolve
flow. However, because they are generalized, it is possible to construct transfers with many intermediary states so long as those states are independently resolveable (i.e. so long as at any point the receiver of the transfer can resolve
to get a final balance).create
and resolve
.disputeChannel()
(or disputeTransfer()
) which finalizes the latest state onchain, (2) Calling defundChannel()
(or defundTransfer()
) which withdraws disputed funds.consensus
phase and a defund
phase, which are started when a dispute begins. After these phases end, the onchain channel contract resumes a "happy" state. This means both parties can continue signing commitments at a higher nonce and resume normal channel operations. They also retain the ability to dispute again in case further offchain coordination cannot be reached.The core purpose of any state channel protocol is to produce one or more commitments that represent a user's ability to get funds onchain in the event that coordination breaks down. This means that commitments are the primary interface between the onchain contracts (which manage rare channel failure cases i.e. disputes) and the offchain protocol (used 99.99% of the time).
In the interest of simplicity, Vector only has one type of commitment that is actually signed - the ChannelCommitment
, which is a signature on the CoreChannelState
:
struct CoreChannelState {
Balance[] balances, // index matches assetId index
address[] assetIds,
address channelAddress, // Globally unique
address[2] participants, // [initiator, responder]
uint256[] processedDepositsA, // index matches assetId index
uint256[] processedDepositsB, // index matches assetId index
uint256 timeout,
uint256 nonce,
bytes32 merkleRoot
}
Despite not being a "real" commitment, the CoreTransferState
is a part of the merkle root in the channel state. Thus it's security is enforced using both peers' signatures on the above.
struct CoreTransferState {
Balance initialBalance;
address assetId;
address channelAddress;
bytes32 transferId; // Globally unique
address transferDefinition;
uint256 transferTimeout; // Note: we have a transfer-specific timeout to enable decrementing timelocks for hopped transfers
bytes32 initialStateHash;
}
Vector uses a proxy pattern and the CREATE2 opcode to optimize onboarding UX for new channels. This means that participants can derive a channelAddress
deterministically and independently as part of setting up a channel (and, in Bob's case, depositing to it). At some point later (decoupled from onboarding flow), either participant can then call ChannelFactory.createChannel
to deploy their channel proxy.
To properly protect against replay attacks across chains or discrete networks, the channelAddress
MUST be globally unique. We additionally include channelAddress
as part of the channel state, and as a prt of the derivation for transferId
to properly domain-separate signed calldata as well.
Deriving channelAddress
uses the following CREATE2 salt:
keccak256(
abi.encodePacked(
alice, // initiator signer address
bob, // responder signer address
chainId,
keccak256("vector")
)
);
The dispute flow works as follows:
disputeChannel()
passing in their latest state. This begins the consensus
phase of the dispute game. The counterparty has the ability to respond with a higher-nonced state within the phase. Note that for now we just wait out the entire phase, but it would be possible to implement a shortcut where if both parties submit updates then the phase can be skipped.
defund
phase of the dispute game begins.defund
phase, either party may call defundChannel()
to withdraw all assets from the channel (for both parties).disputeTransfer()
which starts a timeout window within which the transfer state must be finalized. disputeTransfer()
checks that the hash of the passed in transfer state is a part of the merkle root checkpointed onchain during the channel consensus
phase.
create
channel op (where balances are locked into the transfer), and then is updated again to remove the transfer state during the resolve
channel op (where balances are reintroduced to the core channel state). This means that a disputed transfer can only ever be in it's initial state, which keeps things really simple.defundTransfer
anytime before the transfer dispute window expires. This will call the TransferDefinition
to get an updated set of balances, and then send those balances to both parties onchain. If no transfer resolver is available, the dispute window will expire and then defundTransfer
can be called (once again by anyone) to pay out the initial balances of the transfer via adjudicatorTransfer
on the VectorChannel
contract.As mentioned above, funding a channel is asymmetric. The initiator of a channel (as determined by participants[]
), must deposit using the depositA
function in the channel contract. The responder of a channel can deposit simply by sending funds to the channel address.
Calling depositAlice
increments the totalDepositsAlice
by the amount that Alice deposits for a given assetId. We can get this value offchain or in the adjudicator by calling the totalDepositsAlice
getter. We can also get totalDepositsBob
the same way -- the contract calculates using the following identity:
getBalance(assetId) + _totalWithdrawn[assetId] - _totalDepositedAlice[assetId]
Note that because this is an identity, we do not use SafeMath. We explicitly want these values to wrap around in the event of an over/undeflow.
Offchain, we track the processedDepositsA
and processedDepositsB
. Thus, we can calculate any pending deposits (that need to be reconciled with the offchain balance) as totalDepositsAlice.sub(processedDepositsA)
. We do the same onchain in the event of a dispute when calling defundChannel()
.
The above pattern has a few highly desireable UX consequences:
Withdrawing works a bit differently:
A withdraw from the channel is done by locking up some funds in a transfer and "burning" them, conditionally upon a withdraw commitment being generated from the channel. Once a commitment is generated, one or both parties always have the ability to put it onchain to get their funds. Because of this, we consider offchain that the withdraw was completed even if it wasn't actually submitted to chain. Note that, in the event of a dispute, both parties MUST submit any pending withdraw commitments to chain to properly receive their remaining funds.
disputeChannel
to only refresh in the case that the channel is not in the Consensus
phase. (Basically, each phase Running
, Consensus
, Dispute
should be handled separately)transferResolver
to resolve
a transfer onchain in defundTransfer
. Either party should be able to defund it with the existing state, however.onlyParticipants
anymore if we're allowing anybody to dispute.getChannelAddress
needs to be implemented using participants, chainId (from onchain data), hardcoded vector domain separator, and hardcoded ChannelFactory
address.adjudicatorTransfer
fngetTransactionHash
to use nonce-based replay protectioncreateChannelAndDepositA
is very ugly + we need two onchain txs no matter what because of approve/transferFromBalance
offchain to be fixed size arraysFAQs
Smart contracts powering Connext's minimalist channel platform
The npm package @connext/vector-contracts receives a total of 8 weekly downloads. As such, @connext/vector-contracts popularity was classified as not popular.
We found that @connext/vector-contracts demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 6 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.