From 95638c3728d650fbd992528e2ee3d61d3f6b5dac Mon Sep 17 00:00:00 2001 From: mingda Date: Tue, 22 Sep 2020 00:48:03 +0800 Subject: [PATCH] LockedTokenVault test finished --- contracts/token/LockedTokenVault.sol | 119 +++++++----------- test/TokenLock.test.ts | 180 +++++++++++++++++++++++++++ 2 files changed, 225 insertions(+), 74 deletions(-) create mode 100644 test/TokenLock.test.ts diff --git a/contracts/token/LockedTokenVault.sol b/contracts/token/LockedTokenVault.sol index bb526fa..f79f6a1 100644 --- a/contracts/token/LockedTokenVault.sol +++ b/contracts/token/LockedTokenVault.sol @@ -29,15 +29,17 @@ contract LockedTokenVault is Ownable { address _TOKEN_; mapping(address => uint256) internal originBalances; - mapping(address => uint256) internal remainingBalances; + mapping(address => uint256) internal claimedBalances; - mapping(address => bool) internal confirmOriginBalance; mapping(address => address) internal holderTransferRequest; + uint256 public _UNDISTRIBUTED_AMOUNT_; uint256 public _START_RELEASE_TIME_; uint256 public _RELEASE_DURATION_; uint256 public _CLIFF_RATE_; + bool public _DISTRIBUTE_FINISHED_; + // ============ Modifiers ============ modifier beforeStartRelease() { @@ -50,13 +52,8 @@ contract LockedTokenVault is Ownable { _; } - modifier holderConfirmed(address holder) { - require(confirmOriginBalance[holder], "HOLDER NOT CONFIRMED"); - _; - } - - modifier holderNotConfirmed(address holder) { - require(!confirmOriginBalance[holder], "HOLDER CONFIRMED"); + modifier distributeNotFinished() { + require(!_DISTRIBUTE_FINISHED_, "DISTRIBUTE FINISHED"); _; } @@ -74,82 +71,55 @@ contract LockedTokenVault is Ownable { _CLIFF_RATE_ = _cliffRate; } - function deposit(uint256 amount) external onlyOwner beforeStartRelease { + function deposit(uint256 amount) external onlyOwner { _tokenTransferIn(_OWNER_, amount); - originBalances[_OWNER_] = originBalances[_OWNER_].add(amount); - remainingBalances[_OWNER_] = remainingBalances[_OWNER_].add(amount); + _UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.add(amount); } - function withdraw(uint256 amount) external onlyOwner beforeStartRelease { - originBalances[_OWNER_] = originBalances[_OWNER_].sub(amount); - remainingBalances[_OWNER_] = remainingBalances[_OWNER_].sub(amount); + function withdraw(uint256 amount) external onlyOwner { + _UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.sub(amount); _tokenTransferOut(_OWNER_, amount); } + function finishDistribute() external onlyOwner { + _DISTRIBUTE_FINISHED_ = true; + } + // ============ For Owner ============ - function grant(address holder, uint256 amount) + function grant(address[] calldata holderList, uint256[] calldata amountList) external onlyOwner - beforeStartRelease - holderNotConfirmed(holder) { - originBalances[holder] = originBalances[holder].add(amount); - remainingBalances[holder] = remainingBalances[holder].add(amount); - - originBalances[_OWNER_] = originBalances[_OWNER_].sub(amount); - remainingBalances[_OWNER_] = remainingBalances[_OWNER_].sub(amount); + require(holderList.length == amountList.length, "batch grant length not match"); + uint256 amount = 0; + for (uint256 i = 0; i < holderList.length; ++i) { + originBalances[holderList[i]] = originBalances[holderList[i]].add(amountList[i]); + amount = amount.add(amountList[i]); + } + _UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.sub(amount); } - function recall(address holder) - external - onlyOwner - beforeStartRelease - holderNotConfirmed(holder) - { + function recall(address holder) external onlyOwner distributeNotFinished { uint256 amount = originBalances[holder]; - originBalances[holder] = 0; - remainingBalances[holder] = 0; - - originBalances[_OWNER_] = originBalances[_OWNER_].add(amount); - remainingBalances[_OWNER_] = remainingBalances[_OWNER_].add(amount); - } - - function executeHolderTransfer(address holder) external onlyOwner { - address newHolder = holderTransferRequest[holder]; - require(newHolder != address(0), "INVALID NEW HOLDER"); - require(originBalances[newHolder] == 0, "NOT NEW HOLDER"); - - originBalances[newHolder] = originBalances[holder]; - remainingBalances[newHolder] = remainingBalances[holder]; - - originBalances[holder] = 0; - remainingBalances[holder] = 0; - - holderTransferRequest[holder] = address(0); + _UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.add(amount); } // ============ For Holder ============ - function confirm() external { - confirmOriginBalance[msg.sender] = true; + function transferLockedToken(address to) external { + originBalances[to] = originBalances[to].add(originBalances[msg.sender]); + claimedBalances[to] = claimedBalances[to].add(claimedBalances[msg.sender]); + + originBalances[msg.sender] = 0; + claimedBalances[msg.sender] = 0; } - function cancelConfirm() external { - confirmOriginBalance[msg.sender] = false; - } - - function requestTransfer(address newHolder) external holderConfirmed(msg.sender) { - require(originBalances[newHolder] == 0, "NOT NEW HOLDER"); - holderTransferRequest[msg.sender] = newHolder; - } - - function claimToken() external afterStartRelease { - uint256 unLocked = getUnlockedBalance(msg.sender); - - _tokenTransferOut(msg.sender, unLocked); - remainingBalances[msg.sender] = remainingBalances[msg.sender].sub(unLocked); + function claim() external { + uint256 claimableToken = getClaimableBalance(msg.sender); + _tokenTransferOut(msg.sender, claimableToken); + claimedBalances[msg.sender] = claimedBalances[msg.sender].add(claimableToken); } // ============ View ============ @@ -158,32 +128,33 @@ contract LockedTokenVault is Ownable { return originBalances[holder]; } - function getRemainingBalance(address holder) external view returns (uint256) { - return remainingBalances[holder]; - } - - function isConfirmed(address holder) external view returns (bool) { - return confirmOriginBalance[holder]; + function getClaimedBalance(address holder) external view returns (uint256) { + return claimedBalances[holder]; } function getHolderTransferRequest(address holder) external view returns (address) { return holderTransferRequest[holder]; } - function getUnlockedBalance(address holder) public view returns (uint256) { + function getClaimableBalance(address holder) public view returns (uint256) { if (block.timestamp < _START_RELEASE_TIME_) { return 0; } - uint256 newRemaining = 0; + uint256 remainingToken = getRemainingBalance(holder); + return originBalances[holder].sub(remainingToken).sub(claimedBalances[holder]); + } + + function getRemainingBalance(address holder) public view returns (uint256) { + uint256 remainingToken = 0; uint256 timePast = block.timestamp.sub(_START_RELEASE_TIME_); if (timePast < _RELEASE_DURATION_) { uint256 remainingTime = _RELEASE_DURATION_.sub(timePast); - newRemaining = originBalances[holder] + remainingToken = originBalances[holder] .sub(DecimalMath.mul(originBalances[holder], _CLIFF_RATE_)) .mul(remainingTime) .div(_RELEASE_DURATION_); } - return remainingBalances[msg.sender].sub(newRemaining); + return remainingToken; } // ============ Internal Helper ============ diff --git a/test/TokenLock.test.ts b/test/TokenLock.test.ts new file mode 100644 index 0000000..d678ebc --- /dev/null +++ b/test/TokenLock.test.ts @@ -0,0 +1,180 @@ +/* + + Copyright 2020 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 + +*/ + +import { DODOContext, getDODOContext } from './utils/Context'; +import { decimalStr, MAX_UINT256 } from './utils/Converter'; +// import * as assert from "assert" +import { newContract, DODO_TOKEN_CONTRACT_NAME, LOCKED_TOKEN_VAULT_CONTRACT_NAME } from './utils/Contracts'; +import { Contract } from 'web3-eth-contract'; +import * as assert from 'assert'; +import BigNumber from 'bignumber.js'; +import { logGas } from './utils/Log'; + +let DODOToken: Contract +let LockedTokenVault: Contract +let initTime: any + +let u1: string +let u2: string +let u3: string + +async function init(ctx: DODOContext): Promise { + u1 = ctx.spareAccounts[0]; + u2 = ctx.spareAccounts[1]; + u3 = ctx.spareAccounts[2]; + + initTime = (await ctx.Web3.eth.getBlock(await ctx.Web3.eth.getBlockNumber())).timestamp; + DODOToken = await newContract(DODO_TOKEN_CONTRACT_NAME) + + // release after 1 day, cliff 10% and vest in 1 day + LockedTokenVault = await newContract(LOCKED_TOKEN_VAULT_CONTRACT_NAME, [DODOToken.options.address, initTime + 86400, 86400, decimalStr("0.1")]) + + DODOToken.methods.approve(LockedTokenVault.options.address, MAX_UINT256).send(ctx.sendParam(ctx.Deployer)) + LockedTokenVault.methods.deposit(decimalStr("10000")).send(ctx.sendParam(ctx.Deployer)) +} + +describe("Lock DODO Token", () => { + + let snapshotId: string + let ctx: DODOContext + + before(async () => { + ctx = await getDODOContext() + await init(ctx); + }) + + beforeEach(async () => { + snapshotId = await ctx.EVM.snapshot(); + }); + + afterEach(async () => { + await ctx.EVM.reset(snapshotId) + }); + + describe("Lock operations", () => { + it("init states", async () => { + assert.equal(await LockedTokenVault.methods._UNDISTRIBUTED_AMOUNT_().call(), decimalStr("10000")) + await logGas(LockedTokenVault.methods.grant( + [u1], + [decimalStr("100")] + ), ctx.sendParam(ctx.Deployer), "grant 1 address") + }) + + it("grant", async () => { + await logGas(LockedTokenVault.methods.grant( + [u1, u2, u3], + [decimalStr("100"), decimalStr("200"), decimalStr("300")] + ), ctx.sendParam(ctx.Deployer), "grant 3 address") + + assert.equal(await LockedTokenVault.methods._UNDISTRIBUTED_AMOUNT_().call(), decimalStr("9400")) + + assert.equal(await LockedTokenVault.methods.getOriginBalance(u1).call(), decimalStr("100")) + assert.equal(await LockedTokenVault.methods.getOriginBalance(u2).call(), decimalStr("200")) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u1).call(), "0") + + await ctx.EVM.increaseTime(86400) + assert.ok(approxEqual(await LockedTokenVault.methods.getClaimableBalance(u1).call(), decimalStr("10"))) + + await ctx.EVM.increaseTime(30000) + assert.ok(approxEqual(await LockedTokenVault.methods.getClaimableBalance(u1).call(), decimalStr("41.25"))) + }) + + it("claim", async () => { + await LockedTokenVault.methods.grant( + [u1, u2, u3], + [decimalStr("100"), decimalStr("200"), decimalStr("300")] + ).send(ctx.sendParam(ctx.Deployer)) + + await ctx.EVM.increaseTime(86400) + await LockedTokenVault.methods.claim().send(ctx.sendParam(u1)) + assert.equal(await LockedTokenVault.methods.getOriginBalance(u1).call(), decimalStr("100")) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u1).call(), "0") + assert.ok(approxEqual(await DODOToken.methods.balanceOf(u1).call(), decimalStr("10"))) + + await ctx.EVM.increaseTime(30000) + await LockedTokenVault.methods.claim().send(ctx.sendParam(u1)) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u1).call(), "0") + assert.ok(approxEqual(await LockedTokenVault.methods.getRemainingBalance(u1).call(), decimalStr("58.75"))) + assert.ok(approxEqual(await DODOToken.methods.balanceOf(u1).call(), decimalStr("41.25"))) + + await LockedTokenVault.methods.claim().send(ctx.sendParam(u2)) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u2).call(), "0") + assert.ok(approxEqual(await LockedTokenVault.methods.getRemainingBalance(u2).call(), decimalStr("117.5"))) + assert.ok(approxEqual(await DODOToken.methods.balanceOf(u2).call(), decimalStr("82.5"))) + }) + + it("recall & transfer", async () => { + await LockedTokenVault.methods.grant( + [u1, u2, u3], + [decimalStr("100"), decimalStr("200"), decimalStr("300")] + ).send(ctx.sendParam(ctx.Deployer)) + + // recall u2 + await LockedTokenVault.methods.recall(u2).send(ctx.sendParam(ctx.Deployer)) + assert.equal(await LockedTokenVault.methods.getOriginBalance(u2).call(), "0") + + // transfer from u3 to u2 + await ctx.EVM.increaseTime(86400 + 30000) + await LockedTokenVault.methods.transferLockedToken(u2).send(ctx.sendParam(u3)) + + await LockedTokenVault.methods.claim().send(ctx.sendParam(u2)) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u2).call(), "0") + assert.ok(approxEqual(await LockedTokenVault.methods.getRemainingBalance(u2).call(), decimalStr("176.25"))) + assert.ok(approxEqual(await DODOToken.methods.balanceOf(u2).call(), decimalStr("123.75"))) + + // transfer from u2 to u3 + await ctx.EVM.increaseTime(30000) + await LockedTokenVault.methods.transferLockedToken(u3).send(ctx.sendParam(u2)) + + await LockedTokenVault.methods.claim().send(ctx.sendParam(u3)) + assert.equal(await LockedTokenVault.methods.getClaimableBalance(u3).call(), "0") + assert.ok(approxEqual(await LockedTokenVault.methods.getRemainingBalance(u3).call(), decimalStr("82.5"))) + assert.ok(approxEqual(await DODOToken.methods.balanceOf(u3).call(), decimalStr("93.75"))) + }) + + it("withdraw", async () => { + await LockedTokenVault.methods.grant( + [u1, u2, u3], + [decimalStr("100"), decimalStr("200"), decimalStr("300")] + ).send(ctx.sendParam(ctx.Deployer)) + + await LockedTokenVault.methods.withdraw(decimalStr("1000")).send(ctx.sendParam(ctx.Deployer)) + assert.equal(await LockedTokenVault.methods._UNDISTRIBUTED_AMOUNT_().call(), decimalStr("8400")) + + await assert.rejects( + LockedTokenVault.methods.withdraw(decimalStr("8500")).send(ctx.sendParam(ctx.Deployer)), + /SUB_ERROR/ + ) + }) + + it("finish distributed", async () => { + await LockedTokenVault.methods.grant( + [u1, u2, u3], + [decimalStr("100"), decimalStr("200"), decimalStr("300")] + ).send(ctx.sendParam(ctx.Deployer)) + await LockedTokenVault.methods.finishDistribute().send(ctx.sendParam(ctx.Deployer)) + + // can not recall + await assert.rejects( + LockedTokenVault.methods.recall(u2).send(ctx.sendParam(ctx.Deployer)), + /DISTRIBUTE FINISHED/ + ) + }) + }) + +}) + +function approxEqual(numStr1: string, numStr2: string) { + let num1 = new BigNumber(numStr1) + let num2 = new BigNumber(numStr2) + let ratio = num1.div(num2).minus(1).abs() + if (ratio.isLessThan(0.0002)) { + return true + } else { + return false + } +} \ No newline at end of file