Skip to content

Commit fb7f36b

Browse files
committed
TokenGrant is able to compute unlocked amount
1 parent 67eb566 commit fb7f36b

File tree

4 files changed

+202
-0
lines changed

4 files changed

+202
-0
lines changed
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.4;
4+
5+
interface IGrantStakingPolicy {
6+
function getStakeableAmount(
7+
uint256 _now,
8+
uint256 grantedAmount,
9+
uint256 duration,
10+
uint256 start,
11+
uint256 cliff,
12+
uint256 withdrawn) external view returns (uint256);
13+
}
14+
15+
// TODO: add more policies

contracts/grant/TokenGrant.sol

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity 0.8.4;
4+
5+
import "./GrantStakingPolicy.sol";
6+
7+
contract TokenGrant {
8+
9+
address public grantee;
10+
bool public revocable;
11+
uint256 public amount;
12+
uint256 public duration;
13+
uint256 public start;
14+
uint256 public cliff;
15+
16+
uint256 public withdrawn;
17+
uint256 public staked;
18+
19+
uint256 public revokedAt;
20+
uint256 public revokedAmount;
21+
uint256 public revokedWithdrawn;
22+
23+
IGrantStakingPolicy public stakingPolicy;
24+
25+
function initialize(
26+
address _grantee,
27+
bool _revocable,
28+
uint256 _amount,
29+
uint256 _duration,
30+
uint256 _start,
31+
uint256 _cliff,
32+
IGrantStakingPolicy _stakingPolicy
33+
) public {
34+
grantee = _grantee;
35+
revocable = _revocable;
36+
amount = _amount;
37+
duration = _duration;
38+
start = _start;
39+
cliff = _cliff;
40+
stakingPolicy = _stakingPolicy;
41+
}
42+
43+
function unlockedAmount() public view returns (uint256) {
44+
if (block.timestamp < start) { // start reached?
45+
return 0;
46+
}
47+
48+
if (block.timestamp < cliff) { // cliff reached?
49+
return 0;
50+
}
51+
52+
uint256 timeElapsed = block.timestamp - start;
53+
54+
bool unlockingPeriodFinished = timeElapsed >= duration;
55+
if (unlockingPeriodFinished) { return amount; }
56+
57+
return amount * timeElapsed / duration;
58+
}
59+
}

test/grant/TokenGrant.test.js

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
const { expect } = require("chai")
2+
const {
3+
ZERO_ADDRESS,
4+
to1e18,
5+
lastBlockTime,
6+
} = require("../helpers/contract-test-helpers")
7+
8+
describe("TokenGrant", () => {
9+
let grantee
10+
11+
beforeEach(async () => {
12+
;[grantee] = await ethers.getSigners()
13+
})
14+
15+
describe("unlockedAmount", () => {
16+
const assertionPrecision = to1e18(1) // +- 1 token
17+
18+
const amount = to1e18(100000) // 100k tokens
19+
const duration = 15552000 // 180 days
20+
21+
let now
22+
23+
beforeEach(async () => {
24+
now = await lastBlockTime()
25+
})
26+
27+
context("before the schedule start", () => {
28+
it("should return no tokens unlocked", async () => {
29+
const start = now + 60
30+
const cliff = now
31+
const grant = await createGrant(false, amount, duration, start, cliff)
32+
expect(await grant.unlockedAmount()).to.equal(0)
33+
})
34+
})
35+
36+
context("before the cliff ended", () => {
37+
it("should return no tokens unlocked", async () => {
38+
const start = now - 60
39+
const cliff = now + 60
40+
const grant = await createGrant(false, amount, duration, start, cliff)
41+
expect(await grant.unlockedAmount()).to.equal(0)
42+
})
43+
})
44+
45+
context("after the cliff ended", () => {
46+
it("should return token amount unlocked from the start", async () => {
47+
const start = now - duration / 2
48+
const cliff = now - 1
49+
const grant = await createGrant(false, amount, duration, start, cliff)
50+
expect(await grant.unlockedAmount()).is.closeTo(
51+
to1e18(50000),
52+
assertionPrecision
53+
)
54+
})
55+
})
56+
57+
context("with no cliff", () => {
58+
it("should return token amount unlocked so far", async () => {
59+
const start = now - duration / 4
60+
const cliff = now - duration / 4
61+
const grant = await createGrant(false, amount, duration, start, cliff)
62+
expect(await grant.unlockedAmount()).is.closeTo(
63+
to1e18(25000),
64+
assertionPrecision
65+
)
66+
})
67+
})
68+
69+
context("when unlocking period finished", () => {
70+
it("should return all tokens", async () => {
71+
const start = now - duration - 1
72+
const cliff = now - duration - 1
73+
const grant = await createGrant(false, amount, duration, start, cliff)
74+
expect(await grant.unlockedAmount()).is.closeTo(
75+
to1e18(100000),
76+
assertionPrecision
77+
)
78+
})
79+
})
80+
81+
context("when in the middle of unlocking period", () => {
82+
it("should return token amount unlocked from the start", async () => {
83+
const start = now - duration / 2
84+
const cliff = now - duration / 2
85+
const grant = await createGrant(false, amount, duration, start, cliff)
86+
expect(await grant.unlockedAmount()).is.closeTo(
87+
to1e18(50000),
88+
assertionPrecision
89+
)
90+
})
91+
})
92+
93+
context("when the unlocking period just started", () => {
94+
it("should return token amount unlocked so far", async () => {
95+
const start = now - 3600 // one hour earlier
96+
const cliff = now - 3600
97+
const grant = await createGrant(false, amount, duration, start, cliff)
98+
expect(await grant.unlockedAmount()).is.closeTo(
99+
to1e18(23), // 3600 / 15552000 * 100k = ~23 tokens
100+
assertionPrecision
101+
)
102+
})
103+
})
104+
})
105+
106+
async function createGrant(revocable, amount, duration, start, cliff) {
107+
const TokenGrant = await ethers.getContractFactory("TokenGrant")
108+
const tokenGrant = await TokenGrant.deploy()
109+
await tokenGrant.deployed()
110+
111+
await tokenGrant.initialize(
112+
grantee.address,
113+
revocable,
114+
amount,
115+
duration,
116+
start,
117+
cliff,
118+
ZERO_ADDRESS
119+
)
120+
121+
return tokenGrant
122+
}
123+
})

test/helpers/contract-test-helpers.js

+5
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ async function getBlockTime(blockNumber) {
1212
return (await ethers.provider.getBlock(blockNumber)).timestamp
1313
}
1414

15+
async function lastBlockTime() {
16+
return (await ethers.provider.getBlock("latest")).timestamp
17+
}
18+
1519
module.exports.to1e18 = to1e18
1620
module.exports.to1ePrecision = to1ePrecision
1721
module.exports.getBlockTime = getBlockTime
22+
module.exports.lastBlockTime = lastBlockTime
1823

1924
module.exports.ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"

0 commit comments

Comments
 (0)