diff --git a/contracts/veDistributionSnapshot.sol b/contracts/veDistributionSnapshot.sol new file mode 100644 index 0000000..ec5ddbf --- /dev/null +++ b/contracts/veDistributionSnapshot.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: bsl-1.1 +/** + * Copyright 2022 Unit Protocol V2: Artem Zakharov (hello@unit.xyz). + */ +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; + +/** + * @notice Rewards distribution in sidechains to veDUCK holders + * Snapshot of holders in mainnet is taken once + */ +contract veDistributionSnapshot is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + /** @notice balance of user from snapshot */ + mapping(address => uint) public balanceOf; + /** @notice users list */ + address[] public users; + /** @notice sum of users' balances from snapshot */ + uint public totalSupply; + + /** @notice amounts of reward token already sent to all users */ + mapping(IERC20 => uint) public rewardsSent; + /** @notice amounts of reward token already sent to user */ + mapping(IERC20 => mapping (address => uint)) public rewardsSentToUser; + + event RewardSent(IERC20 indexed token, address indexed user, uint amount); + + /** + * @notice add users' balances from snapshot + * @dev after all balances added `renounceOwnership` must be called + */ + function addBalances(address[] calldata users_, uint[] calldata balances_) public onlyOwner { + require(users_.length > 0, "DISTRIBUTION: EMPTY_ARRAYS"); + require(users_.length == balances_.length, "DISTRIBUTION: INVALID_ARRAYS_LENGTH"); + + for (uint i; i < users_.length; i++) { + require(balances_[i] > 0, "DISTRIBUTION: INVALID_AMOUNT"); + require(balanceOf[users_[i]] == 0, "DISTRIBUTION: USER_ALREADY_ADDED"); + + balanceOf[users_[i]] = balances_[i]; + totalSupply += balances_[i]; + users.push(users_[i]); + } + } + + function usersCount() public view returns (uint) { + return users.length; + } + + function allUsers() public view returns (address[] memory) { + return users; + } + + function availableReward(address user_, IERC20 token_) public view returns (uint) { + uint userBalance = balanceOf[user_]; + if (userBalance == 0) { + return 0; + } + + return _calcTotalUserReward(userBalance, token_) - rewardsSentToUser[token_][user_]; + } + + function withdrawReward(IERC20[] calldata tokens_) public nonReentrant { + require(owner() == address(0), 'DISTRIBUTION: CONTRACT_IS_NOT_FINALIZED'); + + for (uint i; i < tokens_.length; i++) { + _withdrawReward(tokens_[i]); + } + } + + function _withdrawReward(IERC20 token_) internal { + uint userBalance = balanceOf[msg.sender]; + require(userBalance > 0, 'DISTRIBUTION: AUTH_FAILED'); + + uint totalUserReward = _calcTotalUserReward(userBalance, token_); + require(totalUserReward > rewardsSentToUser[token_][msg.sender], 'DISTRIBUTION: NOTHING_TO_WITHDRAW'); + + uint amountToSend = totalUserReward - rewardsSentToUser[token_][msg.sender]; + rewardsSentToUser[token_][msg.sender] += amountToSend; + rewardsSent[token_] += amountToSend; + + token_.safeTransfer(msg.sender, amountToSend); + emit RewardSent(token_, msg.sender, amountToSend); + } + + function _calcTotalUserReward(uint userBalance_, IERC20 token_) internal view returns (uint) { + uint totalRewardsReceived = rewardsSent[token_] + token_.balanceOf(address(this)); + + return totalRewardsReceived * userBalance_ / totalSupply; + } +} \ No newline at end of file diff --git a/hardhat.config.js b/hardhat.config.js index 5b33ada..2b52b02 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -18,7 +18,7 @@ task("accounts", "Prints the list of accounts", async () => { * @type import('hardhat/config').HardhatUserConfig */ module.exports = { - solidity: "0.8.7", + solidity: "0.8.17", mocha: { timeout: 120000 }, diff --git a/scripts/deploySnapshotDistribution.js b/scripts/deploySnapshotDistribution.js new file mode 100644 index 0000000..1fe77d8 --- /dev/null +++ b/scripts/deploySnapshotDistribution.js @@ -0,0 +1,157 @@ +const hre = require("hardhat"); +const {ethers} = require("hardhat"); + +// snapshot of veduck holders on 01.09.2022 +// Contract 0x48DdD27a4d54CD3e8c34F34F7e66e998442DBcE3 +// Block number 15449617 +const snapshot = [ + ['0x0004d2A2f9a823C1A585fDE6514A17FF695E0001','785041821771336119010075'], + ['0x002183113F3Fedc5B494080954E5E8FAeB070Fd3','102357982353797689981100'], + ['0x00b1e61B175d87cD2824fD6a5cB730da3301f990','9235407280537236587875'], + ['0x022Ce4715b44EF6F0eAd8561B29dA676928D16f3','23604134692103753624375'], + ['0x056AE750B6fa8501f5bC9c8Acb0caF986Fa224ef','103293287692205155779600'], + ['0x0898a4c1E8602A2BC06446eD30b24CD91D65000f','78015484716456325'], + ['0x08e305F123FaFf04A6A24243f47984A7A9702417','10485337644163974001250'], + ['0x09173487b272311Edda01F45f97911aEB6aBd602','421068210790444710233125'], + ['0x0Cd1d691B6aE336302e882eB6DC3e9983BcA74E7','3472832476247227492750'], + ['0x0e43C2EBb8BbFBD724A2CCC0f8e0Ea336a00C401','55520559836377380266900'], + ['0x11c9Ac11ce9913E26faA7a9EE5B07c92b0C8c372','154493319661379826143950'], + ['0x1574472D2FC12905b73f7269352ca4aeFd88eDDf','6452426528115488339050'], + ['0x1BE537FF2Cc291caF8331eFcd3466fE7D1C80D4b','267856640078916417269350'], + ['0x1aF8d7F66e7043c9Bf52286385bf4d65D73204F6','42886953300854096206575'], + ['0x1c012b03f1c2DEa274D2eeEb566B0Eeabfe3Af1A','125137609376199163047475'], + ['0x1fFc4d0c003ddf386B93Aa099a2071cd92EbADE1','22822082803493243711700'], + ['0x23B886AcEEb71458C96792ffd447352c277f820e','121552551954087383931025'], + ['0x2DB04D075c2A96E65d8beef44879ED8d07167384','739485182519099843310375'], + ['0x2Fe9811E6B3ccEb5c14cCa6523F10FFDf4288aF6','28255722490506934519200'], + ['0x2b0f7b89454dC0Cdf2756A4d574a482d2Ba84C87','8130011652819231505957900'], + ['0x2bbfb15f1A903AB3579162A00F81821926422b0d','235890470414763981168175'], + ['0x3037DB6bE2AFd39F5ab4006AD2c1Cb33Ddd5F8c7','81084569635151915932150'], + ['0x3070f20f86fDa706Ac380F5060D256028a46eC29','18699801009911170589475'], + ['0x32f1dEe89452cD5A82C937006F49e9e8f12ceE6B','627423966570942642927450'], + ['0x3310E743fb7E596BE28DD7E6AB73Aa9e0469B719','673921995054941756150'], + ['0x367Ad4160a1cf17B05FA0699c593bCcC977E47cC','348259482847948455555500'], + ['0x37442eD85530B3ccA29339CB0a773ac229b91073','68766488219674396257375'], + ['0x3aFB0B4cA9aB60165E207CB14067B07A04114413','7987555502061029359525'], + ['0x3eDebBC4223c565c66305Bf3beE9Cfc4BD0f30c9','4810213235041049001425'], + ['0x42C53684147bD03645DF294Ca7aC57fDfAcEbfDF','13884060080755858299975'], + ['0x4444A136b1445B1f5e4a20656854adCF6Fa3d667','120439665431421237957075'], + ['0x45178Bd7bEdb3B394F1105bfCa466Ed6527229a1','402619128801012895875'], + ['0x45385eE71E2F6C3e9aF9BAD0bfD69788F1C59E35','43256991153309932585125'], + ['0x46bcF9c0f0A59EdA5D2350Abb5d584c07cD61D0B','129452649353120238750775'], + ['0x4C1968ede8d9b83160d8CE910C5913f157023447','111032889649899737391425'], + ['0x4c2aC40DE55ebB662370CA132aE7dfbF111F2324','1078132451245351965679700'], + ['0x4c7E08418382a962fb8abB15b78cBC8cb9b0C222','607294271525512376100'], + ['0x4da4A2f69Cb2f61A49EfB7423B1F093B79C6f1F2','252974774069739139115000'], + ['0x53b015573630d248a32b11b21E5443EFb92D9CFB','5431960649764375098700'], + ['0x55B0BC048011D59675E20e5760609e665683E342','1031934913075529778750'], + ['0x5668EAd1eDB8E2a4d724C8fb9cB5fFEabEB422dc','46584483532545011571725'], + ['0x56E773B0a620205E9f72265b6013b65B84c2c4aF','6336971686585158118175'], + ['0x5D9FbFCef7B3Ea6344dbB2F6bCE9aA3067aDfE75','3020549927067460671675'], + ['0x5f3Bce4B242d00ED748d48172C1f2D47A0bcB19B','13530233070582946296350'], + ['0x5f69205af146eD017FBfe7A99A1E668eb0152ba2','2998978111986067083250'], + ['0x60ec3C53FDd8358e7E68d49b1e4608FA4050A35b','4069735719099036518750'], + ['0x656B764DD52c14ddfAf1d8c48E6b8B217440Dbb1','10569845783818332807000'], + ['0x6C6F95aFfBB1753115FbAeC513cE0D21E8481d46','1936544777859324875'], + ['0x6C82aba562f2e38a5d474CD610F59e6F90774a6a','448131801764671499797975'], + ['0x6a41278687f2E431b034D209AE3c9DC16840f24F','2808599968030961346274875'], + ['0x6b879dD45cf492B0950e4Ac94f2657D811341169','17316668533778846178650'], + ['0x6dee051c76b6996a9992C73D03aA884826C3a087','89308242204152212952925'], + ['0x6f9BB7e454f5B3eb2310343f0E99269dC2BB8A1d','166619717612041569412825'], + ['0x740E7DAD9F7F5E3E7daCefb13675F574ee918ddb','5168130478868742182600'], + ['0x7dcd10df1FbB7F7045AB6bBfC96Fe4709e25FD8C','28528029751117764255650'], + ['0x80cD6493242917B17ba8B342B96F94F972e55eeE','195376761875317033198550'], + ['0x8442e4FCbbA519B4f4C1EA1FcE57a5379C55906C','354794916900601375'], + ['0x8583068704e9A9eBe8bA4FdDCafc57007E7FdADa','30036277052126872290000'], + ['0x887C3599c4826F7b3cDe82003b894430F27d5b92','4155787421121863899700'], + ['0x8C8024bf5f90a06CCeD7D32BAbcCB934942c82f6','9693961071929327392300'], + ['0x9343d80eb115B25285044419AE9dB4913F6b8cda','248166118682737529275'], + ['0x93ab7580493Da8e78a31c31250d3C5524B7FFD0E','3467387509515246877075'], + ['0x9500178853739b656E30A3E2e25995A131E4EBC0','19486696673323697109266250'], + ['0xB10a17fa1bD936F1058d9eAb01a742A7b60272D0','227497530374658711656425'], + ['0xB2dd7f7a0c94364067aDE38aB28Ad18d124bB329','194506469928846961435675'], + ['0xB5383501116a91C43664E18fe935B7C8957cd125','14104064227271008410300'], + ['0xBcF62BF928883ba87f8353C6C421542329250341','626860552134778058791300'], + ['0xC0701665EBE8c8530fB04401Abce4b2EF7739473','73662830052942787254325'], + ['0xC2A639C930a3E931171BE1136cA400D2B5138324','611753330326445010100'], + ['0xCcd358b112515Ab4E9c43D57E6269e783d3131C6','26571317765747818325150'], + ['0xCf8fcb37b7E4Af7E2F79A6ff813B4d899Ae70bDc','1707053908444300033451875'], + ['0xD952fB344DDAcC0c0e8c23cc686C10d5Dda11A97','61849951424929525220000'], + ['0xDa43689C18F625Bd5CA4Ec7bDd2c2E9F9a6D2552','6541017512184648727875'], + ['0xDf9E6e4856B104d973629d4E564AAEA7cc9d5E7F','258143588523823003950'], + ['0xE1Ea97b43C2d64C5CA71068A2428126b04EA69f5','47192805299691758452750'], + ['0xE60DF25872bdb8177E41675bB604D3D08F7d8060','857700220636589604741125'], + ['0xEC2D15d079cCA5cF068159B98e0cF87366c1fD9F','209714837757168770527675'], + ['0xEC4b1f3184B49191E0a35Ab27FeEa4bf84F7ed2d','4100447342772176866675'], + ['0xEaB337618F6E6a3aC744E06b42765116fd81B0D2','407986708792134319328000'], + ['0xF0C3118675B9DE25fB6A3d913153521b1c51323F','5667124476788432177754375'], + ['0xF2907288f88E234CC5E3b92e579880ce7e39D58D','11348780172795656157348125'], + ['0xFF301F3b4a51a1948aE021C77379D5dd56f1377D','167074303545387867379400'], + ['0xFbC593fCE7b9909916EE60cA4770708384E2988d','2375102266740702269925'], + ['0xa35ACdbfD22Bf313D7BC8e462930eDf1677e2936','78000364206002583844225'], + ['0xa7888F85BD76deeF3Bd03d4DbCF57765a49883b3','819141154700272255997650'], + ['0xbB4567beF67153E5A4D4Bd1a8c6aae5234b34e2b','92333682744986655681875'], + ['0xbaE316407B1187DdF39Fdc7ebEFae0558744FcfE','2490555273876395650100'], + ['0xcfdaE6FF195A99Ee0dEd376eaD5056BB679Ebec7','521067208323316786125'], + ['0xd6Fd705EE0B31a9300D3E2154bccE777270CBb6f','7462008420246785599500'], + ['0xd836da33a6629548271eb9ef4f62D083376eB4A6','13866628813912702611950'], + ['0xd8FD1aD0da93D380c96B9DE5F0b6624B03b7963A','131849810534126754738275'], + ['0xe8D46499994F4CF0F80fEea5Cfc9D9d502985286','18163985304798561979250'], + ['0xeC0c77c2b36283Ca81b4904432F35536d6aDd260','436332123035217389168050'], + ['0xf1c1cDF8a84A5e042eb2213623ADaEc37FE21EB6','2103964552030805583342000'], + ['0xf2a4cF14022617C8c167bC0E536B9fc08C49f8C3','20585126062584338166625'], + ['0xf56036f6a5D9b9991c209DcbC9C40b2C1cD46540','39075352375063386923225'], + ['0xfd4ae6890eEf7a664cF3371f1826909e9A07eE2c','43770096516119234922725'], +] + +const BATCH_SIZE = 30; + +async function main() { + const distribution = await deployContract('veDistributionSnapshot'); + console.log("deployed to:", distribution.address); + + let i = 0; + let usersCount = 0; + let totalBalance = 0n + while (true) { + const slice = snapshot.slice(i, i + BATCH_SIZE); + if (slice.length === 0) { + break; + } + i += BATCH_SIZE; + + let addresses = [] + let balances = [] + for (const [addr, balance] of slice) { + addresses.push(addr) + balances.push(balance) + usersCount++; + totalBalance += BigInt(balance); + } + await distribution.addBalances(addresses, balances); + } + + await distribution.renounceOwnership(); + + console.log('Owner:', await distribution.owner()) + console.log('Users (expected/set):', usersCount, '/', (await distribution.allUsers()).length) + console.log('Total supply (expected/set):', totalBalance, '/', (await distribution.totalSupply()).toBigInt()) +} + + +async function deployContract(contract, ...params) { + const Factory = await ethers.getContractFactory(contract); + const instance = await Factory.deploy(...params); + await instance.deployed(); + + return instance; +} + +// We recommend this pattern to be able to use async/await everywhere +// and properly handle errors. +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/test/veDistributionSnapshot.js b/test/veDistributionSnapshot.js new file mode 100644 index 0000000..98d844a --- /dev/null +++ b/test/veDistributionSnapshot.js @@ -0,0 +1,398 @@ +const { BigNumber } = require("ethers"); +const chai = require("chai"); +const { solidity } = require("ethereum-waffle"); +chai.use(solidity); +const { ethers } = require('hardhat'); +const {expect} = require("chai"); + +let context; +describe("veDistributionSnapshot", function() { + + before(async function () { + context = this; + [this.deployer, this.user1, this.user2, this.user3, this.user4] = await ethers.getSigners(); + this.TestTokenFactory = await ethers.getContractFactory("TestToken") + this.DistributionFactory = await ethers.getContractFactory("veDistributionSnapshot") + }) + + beforeEach(async function () { + this.token1 = await this.TestTokenFactory.deploy() + this.token2 = await this.TestTokenFactory.deploy() + this.token3 = await this.TestTokenFactory.deploy() + this.distribution = await this.DistributionFactory.deploy(); + }) + + it("add balances", async function() { + expect(await this.distribution.balanceOf(this.user1.address)).to.be.equal(0); + expect(await this.distribution.balanceOf(this.user2.address)).to.be.equal(0); + expect(await this.distribution.balanceOf(this.user3.address)).to.be.equal(0); + expect(await this.distribution.balanceOf(this.user4.address)).to.be.equal(0); + expect(await this.distribution.totalSupply()).to.be.equal(0); + expect(await this.distribution.allUsers()).to.be.deep.equal([]); + expect(await this.distribution.usersCount()).to.be.equal(0); + + await expect( + this.distribution.connect(this.user1).addBalances([this.user1.address, this.user2.address], [1, 2]) + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await expect( + this.distribution.addBalances([], []) + ).to.be.revertedWith("DISTRIBUTION: EMPTY_ARRAYS"); + + await expect( + this.distribution.addBalances([this.user1.address], []) + ).to.be.revertedWith("DISTRIBUTION: INVALID_ARRAYS_LENGTH"); + + await expect( + this.distribution.addBalances([this.user1.address], [1, 2]) + ).to.be.revertedWith("DISTRIBUTION: INVALID_ARRAYS_LENGTH"); + + await expect( + this.distribution.addBalances([this.user1.address, this.user2.address], [1, 0]) + ).to.be.revertedWith("DISTRIBUTION: INVALID_AMOUNT"); + + await expect( + this.distribution.addBalances([this.user1.address, this.user1.address], [1, 1]) + ).to.be.revertedWith("DISTRIBUTION: USER_ALREADY_ADDED"); + + await this.distribution.addBalances([this.user1.address], [1]) + expect(await this.distribution.balanceOf(this.user1.address)).to.be.equal(1); + expect(await this.distribution.balanceOf(this.user2.address)).to.be.equal(0); + expect(await this.distribution.balanceOf(this.user3.address)).to.be.equal(0); + expect(await this.distribution.balanceOf(this.user4.address)).to.be.equal(0); + expect(await this.distribution.totalSupply()).to.be.equal(1); + expect(await this.distribution.allUsers()).to.be.deep.equal([this.user1.address]); + expect(await this.distribution.usersCount()).to.be.equal(1); + expect(await this.distribution.users(0)).to.be.equal(this.user1.address); + + await this.distribution.addBalances([this.user2.address, this.user3.address, this.user4.address], [2, 3, 4]) + expect(await this.distribution.balanceOf(this.user1.address)).to.be.equal(1); + expect(await this.distribution.balanceOf(this.user2.address)).to.be.equal(2); + expect(await this.distribution.balanceOf(this.user3.address)).to.be.equal(3); + expect(await this.distribution.balanceOf(this.user4.address)).to.be.equal(4); + expect(await this.distribution.totalSupply()).to.be.equal(10); + expect(await this.distribution.allUsers()).to.be.deep.equal([this.user1.address, this.user2.address, this.user3.address, this.user4.address]); + expect(await this.distribution.usersCount()).to.be.equal(4); + expect(await this.distribution.users(0)).to.be.equal(this.user1.address); + expect(await this.distribution.users(3)).to.be.equal(this.user4.address); + + await expect( + this.distribution.connect(this.user1).renounceOwnership() + ).to.be.revertedWith("Ownable: caller is not the owner"); + + await this.distribution.renounceOwnership(); + + await expect( + this.distribution.addBalances([this.deployer.address], [1]) + ).to.be.revertedWith("Ownable: caller is not the owner"); + }); + + async function addBalances() { + await this.distribution.addBalances( + [this.user1.address, this.user2.address, this.user3.address, this.user4.address], + [1, 2, 3, 4] + ); + await this.distribution.renounceOwnership(); + } + + it("withdraw case1: full withdrawal and then additional reward", async function() { + await expect( + this.distribution.withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: CONTRACT_IS_NOT_FINALIZED"); + + await addBalances.bind(this)(); + + await expect( + this.distribution.withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: AUTH_FAILED"); + + await expect( + this.distribution.connect(this.user1).withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: NOTHING_TO_WITHDRAW"); + + //////// + await checkBalances(this.token1, 0, 0, 0, 0, 0); + await checkBalances(this.token2, 0, 0, 0, 0, 0); + await checkBalances(this.token3, 0, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 0, 0, 0); + await checkAvailableRewards(this.token2, 0, 0, 0, 0); + await checkAvailableRewards(this.token3, 0, 0, 0, 0); + + + await this.token1.mint(this.distribution.address, 10000); + await this.token2.mint(this.distribution.address, 33333); + await this.token3.mint(this.distribution.address, 8); + await checkBalances(this.token1, 10000, 0, 0, 0, 0); + await checkBalances(this.token2, 33333, 0, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 1000, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 3333, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + + await this.distribution.connect(this.user1).withdrawReward([this.token1.address]); + await checkBalances(this.token1, 9000, 1000, 0, 0, 0); + await checkBalances(this.token2, 33333, 0, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 3333, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + + await this.distribution.connect(this.user1).withdrawReward([this.token2.address]); + await checkBalances(this.token1, 9000, 1000, 0, 0, 0); + await checkBalances(this.token2, 30000, 3333, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 0, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + await checkWithdrawFailed(this.user1); + + /////// + + await withdrawFromAllTokens(this.user3); + await checkBalances(this.token1, 6000, 1000, 0, 3000, 0); + await checkBalances(this.token2, 20001, 3333, 0, 9999, 0); + await checkBalances(this.token3, 6, 0, 0, 2, 0); + await checkAvailableRewards(this.token1, 0, 2000, 0, 4000); + await checkAvailableRewards(this.token2, 0, 6666, 0, 13333); + await checkAvailableRewards(this.token3, 0, 1, 0, 3); + await checkWithdrawFailed(this.user3); + + /////// + + await withdrawFromAllTokens(this.user4); + await checkBalances(this.token1, 2000, 1000, 0, 3000, 4000); + await checkBalances(this.token2, 6668, 3333, 0, 9999, 13333); + await checkBalances(this.token3, 3, 0, 0, 2, 3); + await checkAvailableRewards(this.token1, 0, 2000, 0, 0); + await checkAvailableRewards(this.token2, 0, 6666, 0, 0); + await checkAvailableRewards(this.token3, 0, 1, 0, 0); + await checkWithdrawFailed(this.user4); + + /////// + + await withdrawFromAllTokens(this.user2); + await checkBalances(this.token1, 0, 1000, 2000, 3000, 4000); + await checkBalances(this.token2, 2, 3333, 6666, 9999, 13333); + await checkBalances(this.token3, 2, 0, 1, 2, 3); + await checkAvailableRewards(this.token1, 0, 0, 0, 0); + await checkAvailableRewards(this.token2, 0, 0, 0, 0); + await checkAvailableRewards(this.token3, 0, 0, 0, 0); + await checkWithdrawFailed(this.user2); + + /////// + + await this.token1.mint(this.distribution.address, 10000); + await this.token2.mint(this.distribution.address, 8); + await this.token3.mint(this.distribution.address, 2); + await checkBalances(this.token1, 10000, 1000, 2000, 3000, 4000); + await checkBalances(this.token2, 10, 3333, 6666, 9999, 13333); + await checkBalances(this.token3, 4, 0, 1, 2, 3); + await checkAvailableRewards(this.token1, 1000, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 1, 2, 3, 3); + await checkAvailableRewards(this.token3, 1, 1, 1, 1); + + ////// + + await withdrawFromAllTokens(this.user1); + await checkBalances(this.token1, 9000, 2000, 2000, 3000, 4000); + await checkBalances(this.token2, 9, 3334, 6666, 9999, 13333); + await checkBalances(this.token3, 3, 1, 1, 2, 3); + await checkAvailableRewards(this.token1, 0, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 0, 2, 3, 3); + await checkAvailableRewards(this.token3, 0, 1, 1, 1); + await checkWithdrawFailed(this.user1); + + ////// + + await withdrawFromAllTokens(this.user3); + await checkBalances(this.token1, 6000, 2000, 2000, 6000, 4000); + await checkBalances(this.token2, 6, 3334, 6666, 10002, 13333); + await checkBalances(this.token3, 2, 1, 1, 3, 3); + await checkAvailableRewards(this.token1, 0, 2000, 0, 4000); + await checkAvailableRewards(this.token2, 0, 2, 0, 3); + await checkAvailableRewards(this.token3, 0, 1, 0, 1); + await checkWithdrawFailed(this.user3); + + ////// + + await withdrawFromAllTokens(this.user2); + await checkBalances(this.token1, 4000, 2000, 4000, 6000, 4000); + await checkBalances(this.token2, 4, 3334, 6668, 10002, 13333); + await checkBalances(this.token3, 1, 1, 2, 3, 3); + await checkAvailableRewards(this.token1, 0, 0, 0, 4000); + await checkAvailableRewards(this.token2, 0, 0, 0, 3); + await checkAvailableRewards(this.token3, 0, 0, 0, 1); + await checkWithdrawFailed(this.user2); + + ////// + + await withdrawFromAllTokens(this.user4); + await checkBalances(this.token1, 0, 2000, 4000, 6000, 8000); + await checkBalances(this.token2, 1, 3334, 6668, 10002, 13336); + await checkBalances(this.token3, 0, 1, 2, 3, 4); + await checkAvailableRewards(this.token1, 0, 0, 0, 0); + await checkAvailableRewards(this.token2, 0, 0, 0, 0); + await checkAvailableRewards(this.token3, 0, 0, 0, 0); + await checkWithdrawFailed(this.user2); + }) + + it("withdraw case1: withdrawal with additional reward in the middle", async function() { + await expect( + this.distribution.withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: CONTRACT_IS_NOT_FINALIZED"); + + await addBalances.bind(this)(); + + await expect( + this.distribution.withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: AUTH_FAILED"); + + await expect( + this.distribution.connect(this.user1).withdrawReward([this.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: NOTHING_TO_WITHDRAW"); + + //////// + await checkBalances(this.token1, 0, 0, 0, 0, 0); + await checkBalances(this.token2, 0, 0, 0, 0, 0); + await checkBalances(this.token3, 0, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 0, 0, 0); + await checkAvailableRewards(this.token2, 0, 0, 0, 0); + await checkAvailableRewards(this.token3, 0, 0, 0, 0); + + + await this.token1.mint(this.distribution.address, 10000); + await this.token2.mint(this.distribution.address, 33333); + await this.token3.mint(this.distribution.address, 8); + await checkBalances(this.token1, 10000, 0, 0, 0, 0); + await checkBalances(this.token2, 33333, 0, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 1000, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 3333, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + + await this.distribution.connect(this.user1).withdrawReward([this.token1.address]); + await checkBalances(this.token1, 9000, 1000, 0, 0, 0); + await checkBalances(this.token2, 33333, 0, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 3333, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + + await this.distribution.connect(this.user1).withdrawReward([this.token2.address]); + await checkBalances(this.token1, 9000, 1000, 0, 0, 0); + await checkBalances(this.token2, 30000, 3333, 0, 0, 0); + await checkBalances(this.token3, 8, 0, 0, 0, 0); + await checkAvailableRewards(this.token1, 0, 2000, 3000, 4000); + await checkAvailableRewards(this.token2, 0, 6666, 9999, 13333); + await checkAvailableRewards(this.token3, 0, 1, 2, 3); + await checkWithdrawFailed(this.user1); + + /////// + + await withdrawFromAllTokens(this.user3); + await checkBalances(this.token1, 6000, 1000, 0, 3000, 0); + await checkBalances(this.token2, 20001, 3333, 0, 9999, 0); + await checkBalances(this.token3, 6, 0, 0, 2, 0); + await checkAvailableRewards(this.token1, 0, 2000, 0, 4000); + await checkAvailableRewards(this.token2, 0, 6666, 0, 13333); + await checkAvailableRewards(this.token3, 0, 1, 0, 3); + await checkWithdrawFailed(this.user3); + + /////// + + await this.token1.mint(this.distribution.address, 10000); + await this.token2.mint(this.distribution.address, 8); + await this.token3.mint(this.distribution.address, 2); + await checkBalances(this.token1, 16000, 1000, 0, 3000, 0); + await checkBalances(this.token2, 20009, 3333, 0, 9999, 0); + await checkBalances(this.token3, 8, 0, 0, 2, 0); + await checkAvailableRewards(this.token1, 1000, 4000, 3000, 8000); + await checkAvailableRewards(this.token2, 1, 6668, 3, 13336); + await checkAvailableRewards(this.token3, 1, 2, 1, 4); + + /////// + + await withdrawFromAllTokens(this.user4); + await checkBalances(this.token1, 8000, 1000, 0, 3000, 8000); + await checkBalances(this.token2, 6673, 3333, 0, 9999, 13336); + await checkBalances(this.token3, 4, 0, 0, 2, 4); + await checkAvailableRewards(this.token1, 1000, 4000, 3000, 0); + await checkAvailableRewards(this.token2, 1, 6668, 3, 0); + await checkAvailableRewards(this.token3, 1, 2, 1, 0); + await checkWithdrawFailed(this.user4); + + /////// + + await withdrawFromAllTokens(this.user3); + await checkBalances(this.token1, 5000, 1000, 0, 6000, 8000); + await checkBalances(this.token2, 6670, 3333, 0, 10002, 13336); + await checkBalances(this.token3, 3, 0, 0, 3, 4); + await checkAvailableRewards(this.token1, 1000, 4000, 0, 0); + await checkAvailableRewards(this.token2, 1, 6668, 0, 0); + await checkAvailableRewards(this.token3, 1, 2, 0, 0); + await checkWithdrawFailed(this.user3); + + /////// + + await withdrawFromAllTokens(this.user2); + await checkBalances(this.token1, 1000, 1000, 4000, 6000, 8000); + await checkBalances(this.token2, 2, 3333, 6668, 10002, 13336); + await checkBalances(this.token3, 1, 0, 2, 3, 4); + await checkAvailableRewards(this.token1, 1000, 0, 0, 0); + await checkAvailableRewards(this.token2, 1, 0, 0, 0); + await checkAvailableRewards(this.token3, 1, 0, 0, 0); + await checkWithdrawFailed(this.user2); + + /////// + + await withdrawFromAllTokens(this.user1); + await checkBalances(this.token1, 0, 2000, 4000, 6000, 8000); + await checkBalances(this.token2, 1, 3334, 6668, 10002, 13336); + await checkBalances(this.token3, 0, 1, 2, 3, 4); + await checkAvailableRewards(this.token1, 0, 0, 0, 0); + await checkAvailableRewards(this.token2, 0, 0, 0, 0); + await checkAvailableRewards(this.token3, 0, 0, 0, 0); + await checkWithdrawFailed(this.user1); + }) +}) + +async function checkBalances(token, expectedDistributionBalance, expectedBalance1, expectedBalance2, expectedBalance3, expectedBalance4) { + expect(await token.balanceOf(context.distribution.address)).to.be.equal(expectedDistributionBalance); + expect(await token.balanceOf(context.user1.address)).to.be.equal(expectedBalance1); + expect(await token.balanceOf(context.user2.address)).to.be.equal(expectedBalance2); + expect(await token.balanceOf(context.user3.address)).to.be.equal(expectedBalance3); + expect(await token.balanceOf(context.user4.address)).to.be.equal(expectedBalance4); + + expect(await context.distribution.rewardsSentToUser(token.address, context.user1.address)).to.be.equal(expectedBalance1); + expect(await context.distribution.rewardsSentToUser(token.address, context.user2.address)).to.be.equal(expectedBalance2); + expect(await context.distribution.rewardsSentToUser(token.address, context.user3.address)).to.be.equal(expectedBalance3); + expect(await context.distribution.rewardsSentToUser(token.address, context.user4.address)).to.be.equal(expectedBalance4); + + expect(await context.distribution.rewardsSent(token.address)).to.be.equal( + expectedBalance1+expectedBalance2+expectedBalance3+expectedBalance4 + ); +} + +async function checkAvailableRewards(token, expectedBalance1, expectedBalance2, expectedBalance3, expectedBalance4) { + expect(await context.distribution.availableReward(context.user1.address, token.address)).to.be.equal(expectedBalance1); + expect(await context.distribution.availableReward(context.user2.address, token.address)).to.be.equal(expectedBalance2); + expect(await context.distribution.availableReward(context.user3.address, token.address)).to.be.equal(expectedBalance3); + expect(await context.distribution.availableReward(context.user4.address, token.address)).to.be.equal(expectedBalance4); +} + +async function checkWithdrawFailed(user) { + await expect( + context.distribution.connect(user).withdrawReward([context.token1.address]) + ).to.be.revertedWith("DISTRIBUTION: NOTHING_TO_WITHDRAW"); + await expect( + context.distribution.connect(user).withdrawReward([context.token2.address]) + ).to.be.revertedWith("DISTRIBUTION: NOTHING_TO_WITHDRAW"); + await expect( + context.distribution.connect(user).withdrawReward([context.token3.address]) + ).to.be.revertedWith("DISTRIBUTION: NOTHING_TO_WITHDRAW"); +} + +async function withdrawFromAllTokens(user) { + await context.distribution.connect(user).withdrawReward([context.token1.address, context.token3.address]); + await context.distribution.connect(user).withdrawReward([context.token2.address]); +}