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

Feature/withdrawals PoC #446

Closed
wants to merge 127 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
8bb5de8
chore: add .editorconfig
Aug 22, 2022
5d0c1f1
feat: bootstrap a simple TDD loop
Aug 23, 2022
e28991f
feat: mint a wannabe-NFT when enqueue
Aug 24, 2022
c42deee
chore: abi
Aug 24, 2022
d92c55c
feat: add another bunch of functions
Aug 24, 2022
50753ea
feat: add ether transfer after burn
Aug 25, 2022
6f7a2c5
feat: withdrawals to be used from Lido contract
Aug 30, 2022
7932cc2
feat: Some intermediate PoC code
folkyatina Sep 15, 2022
518a999
feat: radically simplify withdrawal queue
folkyatina Sep 30, 2022
46b9f94
feat: don't finalize if not enough ether
folkyatina Oct 3, 2022
a03c8de
fix: don't finalize if not enogh ether
folkyatina Oct 3, 2022
271ae4b
feat: simple discount for withdrawals
folkyatina Oct 4, 2022
48d82b0
feat: a bit more of comments
folkyatina Oct 4, 2022
700a297
fix: remove enumeration from withdrawals
folkyatina Oct 5, 2022
cc80ca1
fix: test run
folkyatina Oct 5, 2022
dce6530
feat: Add withdrawal events
folkyatina Oct 6, 2022
d9e3efe
fix: finalization ignoring passed id
folkyatina Oct 6, 2022
e5473b7
feat: simplify naming in queue
folkyatina Oct 11, 2022
9033a3e
feat: new TVL calculations
folkyatina Oct 13, 2022
b06644f
fix: fix reentrancy issue
folkyatina Oct 13, 2022
b02ce05
feat: change Request struct to respect the spec
folkyatina Oct 17, 2022
00441fc
feat: unlooped withdrawals (WIP)
folkyatina Oct 21, 2022
8b5fcfb
feat: unlooped withdrawals 2 (WIP)
folkyatina Oct 27, 2022
a2bed40
feat: Fix withdrawals queue tests
folkyatina Oct 31, 2022
8678584
test: lido tests restore
folkyatina Oct 31, 2022
e3fc672
test: fix lidoHandleOracleReport tests
folkyatina Oct 31, 2022
a567566
test: fix other tests
folkyatina Oct 31, 2022
ea6d090
feat: initial draft version of ValidatorExitBus with rate limit
arwer13 Nov 2, 2022
98a2488
refactor(RateLimitUtils): cleaning up
arwer13 Nov 3, 2022
782623b
fix: counter underflow in reverse for loop
folkyatina Nov 3, 2022
059c97a
fix: skipped visibility in interface
folkyatina Nov 3, 2022
6cf6b59
feat: add restake from withdrawal queue
folkyatina Nov 3, 2022
658f7e0
feature(ValidatorExitBus): forbid to report empty keys list
arwer13 Nov 4, 2022
dfed600
feature(oracle): initial version of python oracle logic draft
arwer13 Nov 4, 2022
cfe28f9
chore: internalize OZ util methods
folkyatina Nov 4, 2022
6346fc8
oracle-logic: refactor and add TODOs
arwer13 Nov 8, 2022
46bc202
feat(ValidatorExitBus): report epochId and skip non-first reports
arwer13 Nov 15, 2022
56e145b
feat(LidoOracle reportBeacon): handle withdrawal-related numbers
arwer13 Nov 16, 2022
2b53a5c
feat: add price chain to oracle reports
folkyatina Nov 17, 2022
0d6c20c
fix: remove _wcBuffer from TVL
folkyatina Nov 17, 2022
07d8a82
feat: align withdrawalRequestStatus to the spec
folkyatina Nov 17, 2022
63dd0af
feat: add BufferWithdrawalsReserve handle
folkyatina Nov 17, 2022
955ddf1
Merge branch 'feature/withdrawals-poc' of github.com:lidofinance/lido…
arwer13 Nov 23, 2022
31fd19b
intermediate: partially separate committee quorum from LidoOracle
arwer13 Nov 24, 2022
d2e34ca
add initial version of new LidoOracle, prune old LidoOracle
arwer13 Nov 28, 2022
7fbd6e2
chore: add _ to distributeFee
folkyatina Nov 23, 2022
c4b6ac6
fix: add auth to setBufferWithdrawalsReserve
folkyatina Nov 23, 2022
fd476aa
feat: rewards after withdrawal
folkyatina Nov 29, 2022
89219a4
chore: a bit of formatting for interfaces
folkyatina Nov 29, 2022
c31e5b6
chore: add proper prettier config for solidity
folkyatina Nov 29, 2022
acb8c77
fix: make claim work as expected
folkyatina Nov 29, 2022
76f91f8
Merge branch 'feature/withdrawals-poc' of github.com:lidofinance/lido…
arwer13 Nov 30, 2022
aeb6f2c
restore initial LidoOracle version (with old reportBeacon())
arwer13 Nov 30, 2022
3f58133
clean up LidoOracleNew.sol code and spaces in Lido.sol
arwer13 Dec 1, 2022
c535879
tests(lidooraclenew): fix to work with new contract version
arwer13 Dec 1, 2022
7b229cd
add setOwner to LidoOracleNew
arwer13 Dec 1, 2022
dffcac7
test(lidooraclenew): cleaning up
arwer13 Dec 1, 2022
acce873
refactor lib AragonUnstructuredStorage
arwer13 Dec 1, 2022
06f8915
restore ERC165Checker in LidoOracleNew
arwer13 Dec 1, 2022
f08bb86
feature(LidoOracleNew): plug OpenZeppelin AccessControlEnumerable
arwer13 Dec 1, 2022
ae85728
feature(LidoOracleNew): accept exitedValidatorsNumbers as uint64
arwer13 Dec 1, 2022
8023a4d
tests(new_rewards): handle rounding error
folkyatina Dec 4, 2022
1cd32e3
LidoOracleNew: fix setQuorum logic
arwer13 Dec 5, 2022
99d1844
Merge branch 'feature/parallel_tests' into feature/withdrawals-poc
folkyatina Dec 5, 2022
4967dbd
add yarn command to run tests sequentially
arwer13 Dec 6, 2022
74a9aaa
ValidatorExitBus: fixes and refactoring
arwer13 Dec 6, 2022
6be8ae2
delete temporary oracle-logic.py
arwer13 Dec 6, 2022
b980ccb
Merge branch 'feature/withdrawals-poc' into feature/validator-exit-bus
arwer13 Dec 6, 2022
dbc906f
Merge pull request #453 from lidofinance/feature/validator-exit-bus
arwer13 Dec 6, 2022
21d2be7
remove forgotten ".only" marks in tests
arwer13 Dec 6, 2022
adfbf15
massive rework of ValidatorExitBus and LidoOracleNew
arwer13 Dec 8, 2022
53f095d
update ethers library to 5.1.4
arwer13 Dec 8, 2022
a5d0e91
ValidatorExitBus: accept staking module address instead of id
arwer13 Dec 8, 2022
4ef112d
LidoOracleNew and ValidatorExitBus: use solidity custom errors
arwer13 Dec 11, 2022
b61f4ed
fix running all tests: mark as skipped a few ValidatorExitBus tests
arwer13 Dec 13, 2022
d7e0898
test(withdrawal): add some basic tests
folkyatina Dec 12, 2022
e708cc0
feat: remove totalExitedValidators from report
folkyatina Dec 13, 2022
542ad5c
feat: old-style reward calculation
folkyatina Dec 13, 2022
ee78e89
fix: withdrawal tests
folkyatina Dec 13, 2022
0717cf7
chore: add mocharc to run tests from vscode
folkyatina Dec 13, 2022
db7addf
fix: fix compiler version for UnstructuredStorage
folkyatina Dec 14, 2022
f8e01d1
fix: update solhint to support custom errors
folkyatina Dec 14, 2022
28cfe0d
chore: fix some linting errors
folkyatina Dec 14, 2022
9ca2069
chore: add pre-commit hook generating ABIs
folkyatina Dec 14, 2022
547a71a
fix: minor fixes
folkyatina Dec 14, 2022
af66c40
chore: remove solium
folkyatina Dec 14, 2022
f3ab760
chore: run linter before commit
folkyatina Dec 14, 2022
100b11f
chore: fix last linter warnings
folkyatina Dec 14, 2022
3111cec
LidoOracleNew: unify committee event names
arwer13 Dec 13, 2022
f2859b1
LidoOracleNew, ValidatorExitBus: add testnet admin functions
arwer13 Dec 14, 2022
8b79da8
ValidatorExitBus: add validatorId field to report
arwer13 Dec 14, 2022
b754242
ValidatorExitBus: store last requested validator id
arwer13 Dec 14, 2022
d4b8458
LidoOracleNew: rename reportBeacon to handleCommitteeMemberReport
arwer13 Dec 14, 2022
b4e4475
fix: rename finalizedQueueLength
folkyatina Dec 14, 2022
0b5cc98
WithdrawalQueue: optimize gas cost
folkyatina Dec 14, 2022
f8c4927
fix: forgotten abi
folkyatina Dec 14, 2022
928170a
chore: add hook for lint:js:fix
folkyatina Dec 14, 2022
c8bbfc0
LidoOracleNew report struct: replace staking module ids by addresses
arwer13 Dec 15, 2022
9e31366
fix: typos and spaces
arwer13 Dec 15, 2022
e137b5c
LidoOracleNew: add testnet helper function
arwer13 Dec 15, 2022
20e5396
refactor (LidoOracleNew, ValidatorExitBus)
arwer13 Dec 15, 2022
45a2a3a
fix formatting
arwer13 Dec 17, 2022
3b4e817
add clarifications regarding layout of structured storage
arwer13 Dec 22, 2022
901df0a
WithdrawalQueue: merge features from WithdrawalQueue V0
arwer13 Dec 23, 2022
f171e0e
Lido contract: get rid of Pausable modifiers to reduce contract size
arwer13 Dec 28, 2022
ceb203d
Lido: temporarily remove some code to fit to contract size limit
arwer13 Dec 28, 2022
b8de0d0
feat: remove some funcs shrinking Lido bytecode sz
TheDZhon Dec 28, 2022
0350274
Merge branch 'feature/size-reduce-proposals' into feature/withdrawals…
arwer13 Dec 28, 2022
ef007af
WithdrawalQueue: more consistent formatting
folkyatina Jan 3, 2023
ef043d0
fix: forgotten abi
folkyatina Jan 4, 2023
c0d59e8
test: fix tests after Lido contract shrunk
folkyatina Jan 4, 2023
c62ad4a
feat: remove restake counter
folkyatina Jan 4, 2023
8534ab5
test: reenable some tests
folkyatina Jan 4, 2023
040a3ed
fix: missed plus when updating lockedEther
folkyatina Jan 5, 2023
fba5840
add a few comments and tiny refactoring
arwer13 Jan 6, 2023
b15c8c1
Merge branch 'develop' into feature/withdrawals-poc
arwer13 Jan 8, 2023
e2a0e86
fix: remove too eager sanity check in WQ
folkyatina Jan 8, 2023
aed8597
feat: migtate to 1e27 share rate fomat
folkyatina Jan 8, 2023
fc18412
chore: electron for vscode-hardhat-solidity to work
folkyatina Jan 8, 2023
6dad351
feat: split WithdrawalQueue vault and queue
folkyatina Jan 8, 2023
be443de
chore: get rid of ILido interface
folkyatina Jan 8, 2023
c16a769
fix: fix getWithdrawalRequestStatus
folkyatina Jan 8, 2023
5fdf4df
chore: formatting and abi
folkyatina Jan 8, 2023
5b36d6a
fix: fix naming and cosmetics
folkyatina Jan 8, 2023
e5f13d2
fix: change MIN and MAX withdrawal amounts
folkyatina Jan 8, 2023
515297a
feat: add possibility to change recepient
folkyatina Jan 8, 2023
8ea2f32
chore: uncomment transferToVault
folkyatina Jan 9, 2023
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
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# top-most EditorConfig file
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4

[*.js]
indent_size = 2
42 changes: 42 additions & 0 deletions contracts/0.4.24/Lido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import "./interfaces/ILido.sol";
import "./interfaces/INodeOperatorsRegistry.sol";
import "./interfaces/IDepositContract.sol";
import "./interfaces/ILidoExecutionLayerRewardsVault.sol";
import "./interfaces/IWithdrawalQueue.sol";

import "./StETH.sol";

Expand Down Expand Up @@ -458,6 +459,47 @@ contract Lido is ILido, StETH, AragonApp {
emit ELRewardsWithdrawalLimitSet(_limitPoints);
}

/**
* @notice this method is responsible for locking StETH and placing user
* in the queue
* @param _amountOfStETH StETH to be locked. `msg.sender` should have the `_amountOfStETH` StETH balance upon this call
* @return ticketId id string that can be used by user to claim their ETH later
*/
function requestWithdrawal(uint256 _amountOfStETH) external returns (uint256 ticketId) {
address withdrawal = address(uint160(getWithdrawalCredentials()));
// lock StETH to withdrawal contract
_transfer(msg.sender, withdrawal, _amountOfStETH);

uint shares = getSharesByPooledEth(_amountOfStETH);
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
ticketId = IWithdrawalQueue(withdrawal).createTicket(msg.sender, _amountOfStETH, shares);

emit WithdrawalRequested(msg.sender, _amountOfStETH, shares, ticketId);
}

/**
* @notice Burns a ticket and transfer ETH to ticket owner address
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
* @param _ticketId id of the ticket to burn
* Permissionless.
*/
function claimWithdrawal(uint256 _ticketId) external {
/// Just forward it to withdrawals
address withdrawal = address(uint160(getWithdrawalCredentials()));
address recipient = IWithdrawalQueue(withdrawal).withdraw(_ticketId);

emit WithdrawalClaimed(_ticketId, recipient, msg.sender);
}

function withdrawalRequestStatus(uint _ticketId) external view returns (
bool finalized,
uint256 ethToWithdraw,
address recipient
) {
IWithdrawalQueue withdrawal = IWithdrawalQueue(address(uint160(getWithdrawalCredentials())));

(recipient, ethToWithdraw,) = withdrawal.queue(_ticketId);
finalized = _ticketId < withdrawal.finalizedQueueLength();
}

/**
* @notice Updates beacon stats, collects rewards from LidoExecutionLayerRewardsVault and distributes all rewards if beacon balance increased
* @dev periodically called by the Oracle contract
Expand Down
16 changes: 12 additions & 4 deletions contracts/0.4.24/interfaces/ILido.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,25 @@ interface ILido {
*/
function submit(address _referral) external payable returns (uint256 StETH);

function requestWithdrawal(uint256 _amountOfStETH) external returns (uint256 ticketId);

function claimWithdrawal(uint256 _ticketId) external;

function withdrawalRequestStatus(uint _ticketId) external view returns (
bool finalized,
uint256 ethToWithdraw,
address recipient
);

// Records a deposit made by a user
event Submitted(address indexed sender, uint256 amount, address referral);

// The `amount` of ether was sent to the deposit_contract.deposit function
event Unbuffered(uint256 amount);

// Requested withdrawal of `etherAmount` to `pubkeyHash` on the ETH 2.0 side, `tokenAmount` burned by `sender`,
// `sentFromBuffer` was sent on the current Ethereum side.
event Withdrawal(address indexed sender, uint256 tokenAmount, uint256 sentFromBuffer,
bytes32 indexed pubkeyHash, uint256 etherAmount);
event WithdrawalRequested(address indexed receiver, uint256 amountOfStETH, uint256 amountOfShares, uint256 ticketId);

event WithdrawalClaimed(uint256 indexed ticketId, address indexed receiver, address initiator);

// Info functions

Expand Down
13 changes: 13 additions & 0 deletions contracts/0.4.24/interfaces/IWithdrawalQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2021 Lido <[email protected]>

// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.4.24;


interface IWithdrawalQueue {
function createTicket(address _from, uint256 _maxETHToWithdraw, uint256 _sharesToBurn) external returns (uint256);
function withdraw(uint256 _ticketId) external returns (address);
function queue(uint256 _ticketId) external view returns (address, uint, uint);
function finalizedQueueLength() external view returns (uint);
}
137 changes: 137 additions & 0 deletions contracts/0.8.9/WithdrawalQueue.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: 2022 Lido <[email protected]>
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.9;

/**
* @title A dedicated contract for handling stETH withdrawal request queue
* @notice it responsible for:
* - taking withdrawal requests, issuing a ticket in return
* - finalizing tickets in queue (making tickets withdrawable)
* - processing claims for finalized tickets
* @author folkyatina
*/
contract WithdrawalQueue {
folkyatina marked this conversation as resolved.
Show resolved Hide resolved
/**
* We don't want to deal with small amounts because there is a gas spent on oracle
* for each request.
* But exact threshhold should be defined later when it will be clear how much will
* it cost to withdraw.
*/
uint256 public constant MIN_WITHDRAWAL = 0.1 ether;

/**
* @notice All state-modifying calls are allowed only from owner protocol.
* @dev should be Lido
*/
address public immutable OWNER;

/**
* @notice amount of ETH on this contract balance that is locked for withdrawal and waiting for cashout
* @dev Invariant: `lockedETHAmount <= this.balance`
*/
uint256 public lockedETHAmount = 0;

/**
* @notice queue for withdrawas, implemented as mapping of incremental index to respective Ticket
* @dev We want to delete items on after cashout to save some gas, so we don't use array here.
*/
mapping(uint => Ticket) public queue;

uint256 public queueLength = 0;
uint256 public finalizedQueueLength = 0;
folkyatina marked this conversation as resolved.
Show resolved Hide resolved

struct Ticket {
address holder;
uint256 maxETHToClaim;
uint256 sharesToBurn;
}

constructor(address _owner) {
OWNER = _owner;
}

/**
* @notice reserve a place in queue for withdrawal and assign a Ticket to `_from` address
* @dev Assuming that _stethAmount is locked before invoking this function
* @return ticketId id of a ticket to withdraw funds once it is available
*/
function createTicket(address _from, uint256 _ETHToClaim, uint256 _shares) external onlyOwner returns (uint256) {
// issue a ticket
uint256 ticketId = queueLength++;
queue[ticketId] = Ticket(_from, _ETHToClaim, _shares);

return ticketId;
}

/**
* @notice Mark next tickets finalized up to `lastTicketIdToFinalize` index in the queue.
* @dev expected that `lastTicketIdToFinalize` is chosen by criteria:
* - it is the last ticket that come before the oracle report block
* - we have enough money to fullfill it
*/
function finalizeTickets(
uint256 lastTicketIdToFinalize,
uint256 totalPooledEther,
uint256 totalShares
) external payable onlyOwner returns (uint sharesToBurn) {
uint ethToLock = 0;
for (uint i = finalizedQueueLength; i <= lastTicketIdToFinalize; i++) {
uint ticketShares = queue[i].sharesToBurn;
uint ticketETH = queue[i].maxETHToClaim;

// discount for slashing
uint256 currentEth = totalPooledEther * ticketShares / totalShares;
if (currentEth < ticketETH) {
queue[i].maxETHToClaim = currentEth;
ticketETH = currentEth;
}

sharesToBurn += ticketShares;
ethToLock += ticketETH;
}

// check that tickets are came before report and move lastNonFinalizedTicketId
// to last ticket that came before report and we have enough ETH for
require(lockedETHAmount + ethToLock <= address(this).balance, "NOT_ENOUGH_ETHER");

lockedETHAmount += ethToLock;
finalizedQueueLength = lastTicketIdToFinalize + 1;
}

/**
* @notice Burns a `_ticketId` ticket and transfer reserver ether to `_to` address.
*/
function withdraw(uint256 _ticketId) external returns (address recipient) {
// ticket must be finalized
require(finalizedQueueLength > _ticketId, "TICKET_NOT_FINALIZED");

// transfer designated amount to ticket owner
recipient = holderOf(_ticketId);
uint256 ethAmount = queue[_ticketId].maxETHToClaim;

// find a discount if applicable

lockedETHAmount -= ethAmount;

payable(recipient).transfer(ethAmount);
folkyatina marked this conversation as resolved.
Show resolved Hide resolved

// free storage to save some gas
delete queue[_ticketId];
}
Fixed Show fixed Hide fixed

function holderOf(uint256 _ticketId) public view returns (address) {
address holder = queue[_ticketId].holder;
require(holder != address(0), "TICKET_NOT_FOUND");
return holder;
}

function _exists(uint256 _ticketId) internal view returns (bool) {
return queue[_ticketId].holder != address(0);
}

modifier onlyOwner() {
require(msg.sender == OWNER, "NOT_OWNER");
_;
}
}
2 changes: 1 addition & 1 deletion lib/abi/Lido.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions lib/abi/WithdrawalQueue.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"MIN_WITHDRAWAL","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"OWNER","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_from","type":"address"},{"internalType":"uint256","name":"_ETHToClaim","type":"uint256"},{"internalType":"uint256","name":"_shares","type":"uint256"}],"name":"createTicket","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"lastTicketIdToFinalize","type":"uint256"},{"internalType":"uint256","name":"totalPooledEther","type":"uint256"},{"internalType":"uint256","name":"totalShares","type":"uint256"}],"name":"finalizeTickets","outputs":[{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"finalizedQueueLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ticketId","type":"uint256"}],"name":"holderOf","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lockedETHAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"queue","outputs":[{"internalType":"address","name":"holder","type":"address"},{"internalType":"uint256","name":"maxETHToClaim","type":"uint256"},{"internalType":"uint256","name":"sharesToBurn","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"queueLength","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_ticketId","type":"uint256"}],"name":"withdraw","outputs":[{"internalType":"address","name":"recipient","type":"address"}],"stateMutability":"nonpayable","type":"function"}]
130 changes: 130 additions & 0 deletions test/0.8.9/withdrawal-queue.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
const { artifacts, contract } = require('hardhat')
const { bn } = require('@aragon/contract-helpers-test')
const { assertBn, assertRevert } = require('@aragon/contract-helpers-test/src/asserts')
const { assert } = require('chai')

const WithdrawalQueue = artifacts.require('WithdrawalQueue.sol')

contract('WithdrawalQueue', ([deployer, owner, holder, stranger]) => {
console.log('Addresses:')
console.log(`Deployer: ${deployer}`)
console.log(`Owner: ${owner}`)

let withdrawal

beforeEach('Deploy', async () => {
withdrawal = await WithdrawalQueue.new(owner)
})

context('Create a ticket', async () => {
let ticketId

beforeEach('Read some state', async () => {
ticketId = await withdrawal.queueLength()
})

it('Owner can create a ticket', async () => {
await withdrawal.createTicket(holder, 1, 1, { from: owner })

assertBn(await withdrawal.holderOf(ticketId), holder)
assertBn(await withdrawal.queueLength(), +ticketId + 1)
assert(ticketId >= (await withdrawal.finalizedQueueLength()))
const ticket = await withdrawal.queue(ticketId)
assert.equal(ticket[0], holder)
assertBn(ticket[1], bn(1))
assertBn(ticket[2], bn(1))
})

it('Only owner can create a ticket', async () => {
await assertRevert(withdrawal.createTicket(holder, 1, 1, { from: stranger }), 'NOT_OWNER')
await assertRevert(withdrawal.holderOf(ticketId), 'TICKET_NOT_FOUND')

assertBn(await withdrawal.queueLength(), ticketId)
})
})

context('Finalization', async () => {
let ticketId
let amountOfStETH
const amountOfShares = 1
beforeEach('Create a ticket', async () => {
amountOfStETH = 100
ticketId = await withdrawal.queueLength()
await withdrawal.createTicket(holder, amountOfStETH, amountOfShares, { from: owner })
})

it('Only owner can finalize a ticket', async () => {
await withdrawal.finalizeTickets(0, amountOfStETH, amountOfShares, { from: owner, value: amountOfStETH })
await assertRevert(
withdrawal.finalizeTickets(0, amountOfStETH, amountOfShares, { from: stranger, value: amountOfStETH }),
'NOT_OWNER'
)

assertBn(await withdrawal.lockedETHAmount(), bn(amountOfStETH))
})

it('One cannot finalize tickets with no ether', async () => {
await assertRevert(
withdrawal.finalizeTickets(0, amountOfStETH, amountOfShares, { from: owner, value: amountOfStETH - 1 }),
'NOT_ENOUGH_ETHER'
)

assertBn(await withdrawal.lockedETHAmount(), bn(0))
})

it('One can finalize tickets with discount', async () => {
shares = 2

await withdrawal.finalizeTickets(0, amountOfStETH, shares, { from: owner, value: amountOfStETH / shares })

assertBn(await withdrawal.lockedETHAmount(), bn(amountOfStETH / shares))
})

it('One can finalize part of the queue', async () => {
await withdrawal.createTicket(holder, amountOfStETH, amountOfShares, { from: owner })

await withdrawal.finalizeTickets(0, amountOfStETH, amountOfShares, { from: owner, value: amountOfStETH })

assertBn(await withdrawal.queueLength(), +ticketId + 2)
assertBn(await withdrawal.finalizedQueueLength(), +ticketId + 1)
assertBn(await withdrawal.lockedETHAmount(), bn(amountOfStETH))
})
})

context('Withdraw', async () => {
let ticketId
beforeEach('Create a ticket', async () => {
ticketId = await withdrawal.queueLength()
await withdrawal.createTicket(holder, 100, 1, { from: owner })
})

it('One cant withdraw not finalized ticket', async () => {
await assertRevert(withdrawal.withdraw(ticketId, { from: owner }), 'TICKET_NOT_FINALIZED')
})

it('Anyone can withdraw a finalized token', async () => {
const balanceBefore = bn(await web3.eth.getBalance(holder))
await withdrawal.finalizeTickets(0, 100, 1, { from: owner, value: 100 })

await withdrawal.withdraw(ticketId, { from: stranger })

assertBn(await web3.eth.getBalance(holder), balanceBefore.add(bn(100)))
})

it('Cant withdraw token two times', async () => {
await withdrawal.finalizeTickets(0, 100, 1, { from: owner, value: 100 })
await withdrawal.withdraw(ticketId)

await assertRevert(withdrawal.withdraw(ticketId, { from: stranger }), 'TICKET_NOT_FOUND')
})

it('Discounted withdrawals produce less eth', async () => {
const balanceBefore = bn(await web3.eth.getBalance(holder))
await withdrawal.finalizeTickets(0, 50, 1, { from: owner, value: 50 })

await withdrawal.withdraw(ticketId, { from: stranger })

assertBn(await web3.eth.getBalance(holder), balanceBefore.add(bn(50)))
})
})
})