Skip to content

Commit

Permalink
Track withdraw index in deposit contract (#42)
Browse files Browse the repository at this point in the history
* Track withdraw index in deposit contract

* Add failedWithdrawalByIndex

* Add isWithdrawalProcessed

* Fix logic

* Add tests
  • Loading branch information
dapplion authored May 15, 2023
1 parent 23f83d4 commit 13e1555
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 2 deletions.
28 changes: 27 additions & 1 deletion contracts/SBCDepositContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -271,9 +271,12 @@ contract SBCDepositContract is
struct FailedWithdrawalRecord {
uint256 amount;
address receiver;
uint64 withdrawalIndex;
}
mapping(uint256 => FailedWithdrawalRecord) public failedWithdrawals;
mapping(uint64 => uint256) public failedWithdrawalIndexByWithdrawalIndex;
uint256 public numberOfFailedWithdrawals;
uint64 public nextWithdrawalIndex;

/**
* @dev Function to be used to process a failed withdrawal (possibly partially).
Expand Down Expand Up @@ -374,12 +377,35 @@ contract SBCDepositContract is
} else {
failedWithdrawals[numberOfFailedWithdrawals] = FailedWithdrawalRecord({
amount: amount,
receiver: _addresses[i]
receiver: _addresses[i],
withdrawalIndex: nextWithdrawalIndex
});
// Shift `failedWithdrawalIndex` by one to allow `isWithdrawalProcessed()`
// to detect successful withdrawals
failedWithdrawalIndexByWithdrawalIndex[nextWithdrawalIndex] = numberOfFailedWithdrawals + 1;
emit WithdrawalFailed(numberOfFailedWithdrawals, amount, _addresses[i]);
++numberOfFailedWithdrawals;
}

// First withdrawal is index 0
nextWithdrawalIndex++;
}
}

/**
* @dev Check if a block's withdrawal has been fully processed or not
* @param _withdrawalIndex EIP-4895 withdrawal.index property
*/
function isWithdrawalProcessed(uint64 _withdrawalIndex) external view returns (bool) {
require(_withdrawalIndex < nextWithdrawalIndex, "withdrawal_index out-of-bounds");
// Only failed withdrawals are registered into failedWithdrawalByIndex, so successful withdrawals
// `_withdrawalIndex` return `failedWithdrawalIndex` 0.
uint256 failedWithdrawalIndex = failedWithdrawalIndexByWithdrawalIndex[_withdrawalIndex];
if (failedWithdrawalIndex == 0) {
return true;
}
// `failedWithdrawalIndex` are shifted by one for the above case
return failedWithdrawals[failedWithdrawalIndex - 1].amount == 0;
}

/**
Expand Down
72 changes: 71 additions & 1 deletion test/deposit.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,46 @@ contract('SBCDepositContractProxy', (accounts) => {
expect(mGNOBalanceAfterFirstWithdrawal).to.be.equal(zeroEther)
})

it("isWithdrawalProcessed first withdrawal successful", async () => {
// No withdrawals yet, none should be processed
// Withdrawals state = []
await assertWithdrawalsState([]);

await executeSuccessfulWithdrawal();
// Withdrawal state = [success]
await assertWithdrawalsState([true]);

await executeFailedWithdrawal();
// Withdrawal state = [success, failed]
await assertWithdrawalsState([true, false]);

await reexecuteFailedWithdrawal();
// Withdrawal state = [success, success]
await assertWithdrawalsState([true, true]);
});

it("isWithdrawalProcessed first withdrawal failed", async () => {
// No withdrawals yet, none should be processed
// Withdrawals state = []
await assertWithdrawalsState([]);

await executeFailedWithdrawal();
// Withdrawal state = [failed]
await assertWithdrawalsState([false]);

await executeFailedWithdrawal();
// Withdrawal state = [failed, failed]
await assertWithdrawalsState([false, false]);

await reexecuteFailedWithdrawal();
// Withdrawal state = [failed, success]
await assertWithdrawalsState([true, false]);

await reexecuteFailedWithdrawal();
// Withdrawal state = [success, success]
await assertWithdrawalsState([true, true]);
});

it('should claim tokens', async () => {
const otherToken = await IERC677.new()
await stake.transfer(contract.address, 1)
Expand Down Expand Up @@ -348,10 +388,40 @@ contract('SBCDepositContractProxy', (accounts) => {
await contract.unwrapTokens(wrapper.address, token.address)
expect((await stake.balanceOf(contract.address)).toString()).to.be.equal(web3.utils.toWei('42'))
})

async function executeSuccessfulWithdrawal() {
const amounts = ["0x0000000773594000"] // 32 * 10^9
const addresses = [accounts[1]]
await stake.transfer(contract.address, depositAmount)
await contract.executeSystemWithdrawals(0, amounts, addresses)
}

async function executeFailedWithdrawal() {
const amounts = ["0x0000000773594000"] // 32 * 10^9
const addresses = [accounts[1]]
await contract.executeSystemWithdrawals(0, amounts, addresses)
}

async function reexecuteFailedWithdrawal() {
await stake.transfer(contract.address, depositAmount)
await contract.executeSystemWithdrawals(1, [], [])
}

// Call with bool array where true = successful withdrawal
async function assertWithdrawalsState(withdrawalState) {
for (let i = 0; i < withdrawalState.length; i++) {
expect(await contract.isWithdrawalProcessed(i)).equal(
withdrawalState[i],
`wrong isWithdrawalProcessed(${i}), withdrawalState ${JSON.stringify(withdrawalState)}`
)
}
// Should revert with out-of-bounds on the next withdrawalState
await contract.isWithdrawalProcessed(withdrawalState.length).should.be.rejected
}
})

function assertSuccessfulWithdrawal(tx) {
const withdrawEvent = tx.logs.find(log => log.event.startsWith("Withdrawal"))
if (!withdrawEvent) throw Error('tx has no Withdraw* events')
expect(withdrawEvent.event).equal('WithdrawalExecuted')
}
}

0 comments on commit 13e1555

Please sign in to comment.