Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deposit recovery upon lost state #5

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
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
95 changes: 77 additions & 18 deletions contracts/Adjudicator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ contract Adjudicator {
uint64 version;
bool hasApp;
uint8 phase;
bool depositRecovery; // Indicates whether this is a deposit recovery.
bytes32 stateHash;
}

Expand Down Expand Up @@ -87,13 +88,53 @@ contract Adjudicator {
// If registered, require newer version and refutation timeout not passed.
(Dispute memory dispute, bool registered) = getDispute(state.channelID);
if (registered) {
require(dispute.version < state.version, "invalid version");
// We skip the version check if the previous registration was a deposit recovery.
require(dispute.version < state.version || dispute.depositRecovery == true, "invalid version");
require(dispute.phase == uint8(DisputePhase.DISPUTE), "incorrect phase");
// solhint-disable-next-line not-rely-on-time
require(block.timestamp < dispute.timeout, "refutation timeout passed");
}

storeChallenge(params, state, DisputePhase.DISPUTE);
storeChallenge(params, state, DisputePhase.DISPUTE, false);
}

/**
* @notice registerDepositRecovery initiates the recovery of the deposited
* funds. It can be used if the channel state is lost.
*
* @param params The channel parameters.
* @param assets The assets to be recovered.
* @param sig A signature on the state.
* @param partIdx The participant index of the signer.
*/
function registerDepositRecovery(
Channel.Params memory params,
address[] memory assets,
bytes memory sig,
uint64 partIdx)
external
{
bytes32 channelID = calcChannelID(params);
// We authenticate the caller to protect against griefing.
require(Sig.verify(abi.encode(channelID, assets), sig, params.participants[partIdx]), "invalid signature");

(Dispute memory dispute, bool registered) = getDispute(channelID);
if (registered) {
require(dispute.depositRecovery == true, "already registered");
require(dispute.phase == uint8(DisputePhase.DISPUTE), "wrong phase");
}

storeChallenge(params, Channel.State({
channelID: channelID,
version: 0,
outcome: Channel.Allocation({
assets: assets,
balances: zeroBalances(assets.length, params.participants.length),
locked: new Channel.SubAlloc[](0)
}),
appData: "",
isFinal: false
}), DisputePhase.DISPUTE, true);
}

/**
Expand All @@ -119,6 +160,7 @@ contract Adjudicator {
external
{
Dispute memory dispute = requireGetDispute(state.channelID);
require(dispute.depositRecovery == false, "deposit recovery"); // We do not allow state progression for deposit recoveries.
if(dispute.phase == uint8(DisputePhase.DISPUTE)) {
// solhint-disable-next-line not-rely-on-time
require(block.timestamp >= dispute.timeout, "timeout not passed");
Expand All @@ -136,7 +178,7 @@ contract Adjudicator {
require(Sig.verify(Channel.encodeState(state), sig, params.participants[actorIdx]), "invalid signature");
requireValidTransition(params, stateOld, state, actorIdx);

storeChallenge(params, state, DisputePhase.FORCEEXEC);
storeChallenge(params, state, DisputePhase.FORCEEXEC, false);
}

/**
Expand Down Expand Up @@ -166,7 +208,7 @@ contract Adjudicator {
requireValidParams(params, state);

ensureTreeConcluded(state, subStates);
pushOutcome(state, subStates, params.participants);
pushOutcome(state, subStates, params.participants, dispute.depositRecovery);
}

/**
Expand Down Expand Up @@ -198,18 +240,18 @@ contract Adjudicator {
require(dispute.phase != uint8(DisputePhase.CONCLUDED), "channel already concluded");
}

storeChallenge(params, state, DisputePhase.CONCLUDED);
storeChallenge(params, state, DisputePhase.CONCLUDED, false);

Channel.State[] memory subStates = new Channel.State[](0);
pushOutcome(state, subStates, params.participants);
pushOutcome(state, subStates, params.participants, false);
}

/**
* @notice Calculates the channel's ID from the given parameters.
* @param params The parameters of the channel.
* @return The ID of the channel.
*/
function channelID(Channel.Params memory params) public pure returns (bytes32) {
function calcChannelID(Channel.Params memory params) public pure returns (bytes32) {
return keccak256(Channel.encodeParams(params));
}

Expand All @@ -231,7 +273,7 @@ contract Adjudicator {
Channel.Params memory params,
Channel.State memory state)
internal pure {
require(state.channelID == channelID(params), "invalid params");
require(state.channelID == calcChannelID(params), "invalid params");
}

/**
Expand All @@ -244,7 +286,8 @@ contract Adjudicator {
function storeChallenge(
Channel.Params memory params,
Channel.State memory state,
DisputePhase disputePhase)
DisputePhase disputePhase,
bool depositRecovery)
internal
{
(Dispute memory dispute, bool registered) = getDispute(state.channelID);
Expand All @@ -253,6 +296,7 @@ contract Adjudicator {
dispute.version = state.version;
dispute.hasApp = params.app != address(0);
dispute.phase = uint8(disputePhase);
dispute.depositRecovery = depositRecovery;
dispute.stateHash = hashState(state);

// Compute timeout.
Expand Down Expand Up @@ -423,7 +467,8 @@ contract Adjudicator {
function pushOutcome(
Channel.State memory state,
Channel.State[] memory subStates,
address[] memory participants)
address[] memory participants,
bool depositRecovery)
internal
{
address[] memory assets = state.outcome.assets;
Expand All @@ -445,25 +490,25 @@ contract Adjudicator {
}

// push accumulated outcome
AssetHolder(assets[a]).setOutcome(state.channelID, participants, outcome);
AssetHolder(assets[a]).setOutcome(state.channelID, participants, outcome, depositRecovery);
}
}

/**
* @dev Returns the dispute state for the given channelID. The second return
* value indicates whether the given channel has been registered yet.
*/
function getDispute(bytes32 _channelID) internal view returns (Dispute memory, bool) {
Dispute memory dispute = disputes[_channelID];
function getDispute(bytes32 channelID) internal view returns (Dispute memory, bool) {
Dispute memory dispute = disputes[channelID];
return (dispute, dispute.stateHash != bytes32(0));
}

/**
* @dev Returns the dispute state for the given channelID. Reverts if the
* channel has not been registered yet.
*/
function requireGetDispute(bytes32 _channelID) internal view returns (Dispute memory) {
(Dispute memory dispute, bool registered) = getDispute(_channelID);
function requireGetDispute(bytes32 channelID) internal view returns (Dispute memory) {
(Dispute memory dispute, bool registered) = getDispute(channelID);
require(registered, "not registered");
return dispute;
}
Expand All @@ -472,8 +517,22 @@ contract Adjudicator {
* @dev Sets the dispute state for the given channelID. Emits event
* ChannelUpdate.
*/
function setDispute(bytes32 _channelID, Dispute memory dispute) internal {
disputes[_channelID] = dispute;
emit ChannelUpdate(_channelID, dispute.version, dispute.phase, dispute.timeout);
function setDispute(bytes32 channelID, Dispute memory dispute) internal {
disputes[channelID] = dispute;
emit ChannelUpdate(channelID, dispute.version, dispute.phase, dispute.timeout);
}

/**
* @notice zeroBalances creates a zero-balance array with the specified
* dimensions.
*
* @param m The length of the first dimension.
* @param m The length of the second dimension.
*/
function zeroBalances(uint m, uint n) internal pure returns (uint256[][] memory balances) {
balances = new uint256[][](m);
for (uint i = 0; i < m; i++) {
balances[i] = new uint256[](n);
}
}
}
60 changes: 39 additions & 21 deletions contracts/AssetHolder.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,33 +86,38 @@ abstract contract AssetHolder {
function setOutcome(
bytes32 channelID,
address[] calldata parts,
uint256[] calldata newBals)
uint256[] calldata newBals,
bool depositRecovery)
external onlyAdjudicator {
require(parts.length == newBals.length, "participants length should equal balances"); // solhint-disable-line reason-string
require(settled[channelID] == false, "trying to set already settled channel"); // solhint-disable-line reason-string

// The channelID itself might already be funded
uint256 sumHeld = holdings[channelID];
holdings[channelID] = 0;
uint256 sumOutcome = 0;

bytes32[] memory fundingIDs = new bytes32[](parts.length);
for (uint256 i = 0; i < parts.length; i++) {
bytes32 id = calcFundingID(channelID, parts[i]);
// Save calculated ids to save gas.
fundingIDs[i] = id;
// Compute old balances.
sumHeld = sumHeld.add(holdings[id]);
// Compute new balances.
sumOutcome = sumOutcome.add(newBals[i]);
}
// We only redistribute assets if this is not a deposit recovery.
if (depositRecovery == false) {
// The channelID itself might already be funded
uint256 sumHeld = holdings[channelID];
holdings[channelID] = 0;
uint256 sumOutcome = 0;

// We allow overfunding channels, who overfunds looses their funds.
if (sumHeld >= sumOutcome) {
bytes32[] memory fundingIDs = new bytes32[](parts.length);
for (uint256 i = 0; i < parts.length; i++) {
holdings[fundingIDs[i]] = newBals[i];
bytes32 id = calcFundingID(channelID, parts[i]);
// Save calculated ids to save gas.
fundingIDs[i] = id;
// Compute old balances.
sumHeld = sumHeld.add(holdings[id]);
// Compute new balances.
sumOutcome = sumOutcome.add(newBals[i]);
}

// We allow overfunding channels, who overfunds looses their funds.
if (sumHeld >= sumOutcome) {
for (uint256 i = 0; i < parts.length; i++) {
holdings[fundingIDs[i]] = newBals[i];
}
}
}

settled[channelID] = true;
emit OutcomeSet(channelID);
}
Expand All @@ -130,13 +135,26 @@ abstract contract AssetHolder {
* Calculated as the hash of the channel id and the participant address.
* @param amount Amount of money that should be deposited.
*/
function deposit(bytes32 fundingID, uint256 amount) external payable {
function deposit(bytes32 fundingID, uint256 amount) public payable {
depositCheck(fundingID, amount);
holdings[fundingID] = holdings[fundingID].add(amount);
depositEnact(fundingID, amount);
emit Deposited(fundingID, amount);
}

/**
* @notice depositChannelParticipant deposits the given amount of assets
* at the specified channel for the specified participant.
*
* @param channelID Channel identifier.
* @param participant Channel participant.
* @param amount Deposit amount.
*/
function depositChannelParticipant(bytes32 channelID, address participant, uint256 amount) external payable {
bytes32 fundingID = calcFundingID(channelID, participant);
deposit(fundingID, amount);
}

/**
* @notice Sends money from authorization.participant to authorization.receiver.
* @dev Generic function which uses the virtual functions `withdrawCheck` and
Expand All @@ -157,7 +175,7 @@ abstract contract AssetHolder {
require(settled[authorization.channelID], "channel not settled");
require(Sig.verify(abi.encode(authorization), signature, authorization.participant), "signature verification failed");
bytes32 id = calcFundingID(authorization.channelID, authorization.participant);
require(holdings[id] >= authorization.amount, "insufficient ETH for withdrawal");
require(holdings[id] >= authorization.amount, "insufficient funds");
withdrawCheck(authorization, signature);
holdings[id] = holdings[id].sub(authorization.amount);
withdrawEnact(authorization, signature);
Expand Down
2 changes: 2 additions & 0 deletions src/test/Adjudicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ contract("Adjudicator", async (accounts) => {
initialDeposit(B);
});

//TODO test deposit recovery

describeWithBlockRevert("register and refute", () => {
const testsRegister = [
{
Expand Down
12 changes: 6 additions & 6 deletions src/test/AssetHolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,26 +101,26 @@ export function genericAssetHolderTest(setup: AssetHolderSetup) {
it("wrong parts length", async () => {
const wrongParts = [setup.parts[setup.A]]
await truffleAssert.reverts(
setup.ah.setOutcome(setup.channelID, wrongParts, finalBalance, { from: setup.adj }),
setup.ah.setOutcome(setup.channelID, wrongParts, finalBalance, false, { from: setup.adj }),
);
});

it("wrong balances length", async () => {
const wrongBals = [ether(1)]
await truffleAssert.reverts(
setup.ah.setOutcome(setup.channelID, setup.parts, wrongBals, { from: setup.adj }),
setup.ah.setOutcome(setup.channelID, setup.parts, wrongBals, false, { from: setup.adj }),
);
});

it("wrong sender", async () => {
await truffleAssert.reverts(
setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, { from: setup.txSender }),
setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, false, { from: setup.txSender }),
);
});

it("correct sender", async () => {
truffleAssert.eventEmitted(
await setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, { from: setup.adj }),
await setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, false, { from: setup.adj }),
'OutcomeSet',
(ev: any) => { return ev.channelID == setup.channelID }
);
Expand All @@ -133,7 +133,7 @@ export function genericAssetHolderTest(setup: AssetHolderSetup) {

it("correct sender (twice)", async () => {
await truffleAssert.reverts(
setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, { from: setup.adj })
setup.ah.setOutcome(setup.channelID, setup.parts, finalBalance, false, { from: setup.adj })
);
});
})
Expand Down Expand Up @@ -187,7 +187,7 @@ export function genericAssetHolderTest(setup: AssetHolderSetup) {
it("set outcome of the asset holder with deposit refusal", async () => {
assert(await setup.ah.settled.call(channelID) == false);
truffleAssert.eventEmitted(
await setup.ah.setOutcome(channelID, setup.parts, finalBalance, { from: setup.adj }),
await setup.ah.setOutcome(channelID, setup.parts, finalBalance, false, { from: setup.adj }),
'OutcomeSet',
(ev: any) => { return ev.channelID == channelID; }
);
Expand Down