LockedTokenVault test finished
This commit is contained in:
@@ -29,15 +29,17 @@ contract LockedTokenVault is Ownable {
|
|||||||
address _TOKEN_;
|
address _TOKEN_;
|
||||||
|
|
||||||
mapping(address => uint256) internal originBalances;
|
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;
|
mapping(address => address) internal holderTransferRequest;
|
||||||
|
|
||||||
|
uint256 public _UNDISTRIBUTED_AMOUNT_;
|
||||||
uint256 public _START_RELEASE_TIME_;
|
uint256 public _START_RELEASE_TIME_;
|
||||||
uint256 public _RELEASE_DURATION_;
|
uint256 public _RELEASE_DURATION_;
|
||||||
uint256 public _CLIFF_RATE_;
|
uint256 public _CLIFF_RATE_;
|
||||||
|
|
||||||
|
bool public _DISTRIBUTE_FINISHED_;
|
||||||
|
|
||||||
// ============ Modifiers ============
|
// ============ Modifiers ============
|
||||||
|
|
||||||
modifier beforeStartRelease() {
|
modifier beforeStartRelease() {
|
||||||
@@ -50,13 +52,8 @@ contract LockedTokenVault is Ownable {
|
|||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier holderConfirmed(address holder) {
|
modifier distributeNotFinished() {
|
||||||
require(confirmOriginBalance[holder], "HOLDER NOT CONFIRMED");
|
require(!_DISTRIBUTE_FINISHED_, "DISTRIBUTE FINISHED");
|
||||||
_;
|
|
||||||
}
|
|
||||||
|
|
||||||
modifier holderNotConfirmed(address holder) {
|
|
||||||
require(!confirmOriginBalance[holder], "HOLDER CONFIRMED");
|
|
||||||
_;
|
_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,82 +71,55 @@ contract LockedTokenVault is Ownable {
|
|||||||
_CLIFF_RATE_ = _cliffRate;
|
_CLIFF_RATE_ = _cliffRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deposit(uint256 amount) external onlyOwner beforeStartRelease {
|
function deposit(uint256 amount) external onlyOwner {
|
||||||
_tokenTransferIn(_OWNER_, amount);
|
_tokenTransferIn(_OWNER_, amount);
|
||||||
originBalances[_OWNER_] = originBalances[_OWNER_].add(amount);
|
_UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.add(amount);
|
||||||
remainingBalances[_OWNER_] = remainingBalances[_OWNER_].add(amount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function withdraw(uint256 amount) external onlyOwner beforeStartRelease {
|
function withdraw(uint256 amount) external onlyOwner {
|
||||||
originBalances[_OWNER_] = originBalances[_OWNER_].sub(amount);
|
_UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.sub(amount);
|
||||||
remainingBalances[_OWNER_] = remainingBalances[_OWNER_].sub(amount);
|
|
||||||
_tokenTransferOut(_OWNER_, amount);
|
_tokenTransferOut(_OWNER_, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finishDistribute() external onlyOwner {
|
||||||
|
_DISTRIBUTE_FINISHED_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
// ============ For Owner ============
|
// ============ For Owner ============
|
||||||
|
|
||||||
function grant(address holder, uint256 amount)
|
function grant(address[] calldata holderList, uint256[] calldata amountList)
|
||||||
external
|
external
|
||||||
onlyOwner
|
onlyOwner
|
||||||
beforeStartRelease
|
|
||||||
holderNotConfirmed(holder)
|
|
||||||
{
|
{
|
||||||
originBalances[holder] = originBalances[holder].add(amount);
|
require(holderList.length == amountList.length, "batch grant length not match");
|
||||||
remainingBalances[holder] = remainingBalances[holder].add(amount);
|
uint256 amount = 0;
|
||||||
|
for (uint256 i = 0; i < holderList.length; ++i) {
|
||||||
originBalances[_OWNER_] = originBalances[_OWNER_].sub(amount);
|
originBalances[holderList[i]] = originBalances[holderList[i]].add(amountList[i]);
|
||||||
remainingBalances[_OWNER_] = remainingBalances[_OWNER_].sub(amount);
|
amount = amount.add(amountList[i]);
|
||||||
|
}
|
||||||
|
_UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.sub(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
function recall(address holder)
|
function recall(address holder) external onlyOwner distributeNotFinished {
|
||||||
external
|
|
||||||
onlyOwner
|
|
||||||
beforeStartRelease
|
|
||||||
holderNotConfirmed(holder)
|
|
||||||
{
|
|
||||||
uint256 amount = originBalances[holder];
|
uint256 amount = originBalances[holder];
|
||||||
|
|
||||||
originBalances[holder] = 0;
|
originBalances[holder] = 0;
|
||||||
remainingBalances[holder] = 0;
|
_UNDISTRIBUTED_AMOUNT_ = _UNDISTRIBUTED_AMOUNT_.add(amount);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ For Holder ============
|
// ============ For Holder ============
|
||||||
|
|
||||||
function confirm() external {
|
function transferLockedToken(address to) external {
|
||||||
confirmOriginBalance[msg.sender] = true;
|
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 {
|
function claim() external {
|
||||||
confirmOriginBalance[msg.sender] = false;
|
uint256 claimableToken = getClaimableBalance(msg.sender);
|
||||||
}
|
_tokenTransferOut(msg.sender, claimableToken);
|
||||||
|
claimedBalances[msg.sender] = claimedBalances[msg.sender].add(claimableToken);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ View ============
|
// ============ View ============
|
||||||
@@ -158,32 +128,33 @@ contract LockedTokenVault is Ownable {
|
|||||||
return originBalances[holder];
|
return originBalances[holder];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemainingBalance(address holder) external view returns (uint256) {
|
function getClaimedBalance(address holder) external view returns (uint256) {
|
||||||
return remainingBalances[holder];
|
return claimedBalances[holder];
|
||||||
}
|
|
||||||
|
|
||||||
function isConfirmed(address holder) external view returns (bool) {
|
|
||||||
return confirmOriginBalance[holder];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHolderTransferRequest(address holder) external view returns (address) {
|
function getHolderTransferRequest(address holder) external view returns (address) {
|
||||||
return holderTransferRequest[holder];
|
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_) {
|
if (block.timestamp < _START_RELEASE_TIME_) {
|
||||||
return 0;
|
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_);
|
uint256 timePast = block.timestamp.sub(_START_RELEASE_TIME_);
|
||||||
if (timePast < _RELEASE_DURATION_) {
|
if (timePast < _RELEASE_DURATION_) {
|
||||||
uint256 remainingTime = _RELEASE_DURATION_.sub(timePast);
|
uint256 remainingTime = _RELEASE_DURATION_.sub(timePast);
|
||||||
newRemaining = originBalances[holder]
|
remainingToken = originBalances[holder]
|
||||||
.sub(DecimalMath.mul(originBalances[holder], _CLIFF_RATE_))
|
.sub(DecimalMath.mul(originBalances[holder], _CLIFF_RATE_))
|
||||||
.mul(remainingTime)
|
.mul(remainingTime)
|
||||||
.div(_RELEASE_DURATION_);
|
.div(_RELEASE_DURATION_);
|
||||||
}
|
}
|
||||||
return remainingBalances[msg.sender].sub(newRemaining);
|
return remainingToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Internal Helper ============
|
// ============ Internal Helper ============
|
||||||
|
|||||||
180
test/TokenLock.test.ts
Normal file
180
test/TokenLock.test.ts
Normal file
@@ -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<void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user