Skip to content

Commit 8dfa7be

Browse files
0xModenecontrotie
andauthored
Ftc vesting contract (#53)
* ftc vesting contract created * started setting up unit tests * completed unit testing * updated README * janitorial work * improved unit tests * split claim test * fixing otcEscrow lint issues * satisfying linter Co-authored-by: Dylan Tran <[email protected]>
1 parent b654195 commit 8dfa7be

File tree

5 files changed

+443
-34
lines changed

5 files changed

+443
-34
lines changed

contracts/token/FTCVesting.sol

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.6.10;
3+
4+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol";
6+
7+
/// @title A vesting contract for full time contributors
8+
/// @author 0xModene
9+
/// @notice You can use this contract to set up vesting for full time DAO contributors
10+
/// @dev All function calls are currently implemented without side effects
11+
contract FTCVesting {
12+
using SafeMath for uint256;
13+
14+
address public index;
15+
address public recipient;
16+
address public treasury;
17+
18+
uint256 public vestingAmount;
19+
uint256 public vestingBegin;
20+
uint256 public vestingCliff;
21+
uint256 public vestingEnd;
22+
23+
uint256 public lastUpdate;
24+
25+
constructor(
26+
address index_,
27+
address recipient_,
28+
address treasury_,
29+
uint256 vestingAmount_,
30+
uint256 vestingBegin_,
31+
uint256 vestingCliff_,
32+
uint256 vestingEnd_
33+
) public {
34+
require(vestingCliff_ >= vestingBegin_, "FTCVester.constructor: cliff is too early");
35+
require(vestingEnd_ > vestingCliff_, "FTCVester.constructor: end is too early");
36+
37+
index = index_;
38+
recipient = recipient_;
39+
treasury = treasury_;
40+
41+
vestingAmount = vestingAmount_;
42+
vestingBegin = vestingBegin_;
43+
vestingCliff = vestingCliff_;
44+
vestingEnd = vestingEnd_;
45+
46+
lastUpdate = vestingBegin;
47+
}
48+
49+
modifier onlyTreasury {
50+
require(msg.sender == treasury, "FTCVester.onlyTreasury: unauthorized");
51+
_;
52+
}
53+
54+
modifier onlyRecipient {
55+
require(msg.sender == recipient, "FTCVester.onlyRecipient: unauthorized");
56+
_;
57+
}
58+
59+
modifier overCliff {
60+
require(block.timestamp >= vestingCliff, "FTCVester.overCliff: cliff not reached");
61+
_;
62+
}
63+
64+
/// @notice Sets new recipient address
65+
/// @param recipient_ new recipient address
66+
function setRecipient(address recipient_) external onlyRecipient {
67+
recipient = recipient_;
68+
}
69+
70+
/// @notice Sets new treasury address
71+
/// @param treasury_ new treasury address
72+
function setTreasury(address treasury_) external onlyTreasury {
73+
treasury = treasury_;
74+
}
75+
76+
/// @notice Allows recipient to claim all currently vested tokens
77+
function claim() external onlyRecipient overCliff {
78+
uint256 amount;
79+
if (block.timestamp >= vestingEnd) {
80+
amount = IERC20(index).balanceOf(address(this));
81+
} else {
82+
amount = vestingAmount.mul(block.timestamp.sub(lastUpdate)).div(vestingEnd.sub(vestingBegin));
83+
lastUpdate = block.timestamp;
84+
}
85+
IERC20(index).transfer(recipient, amount);
86+
}
87+
88+
/// @notice Allows treasury to claw back funds in event of separation from recipient
89+
function clawback() external onlyTreasury {
90+
IERC20(index).transfer(treasury, IERC20(index).balanceOf(address(this)));
91+
}
92+
}

test/token/FTCVesting.spec.ts

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import "module-alias/register";
2+
import { BigNumber } from "@ethersproject/bignumber";
3+
4+
import { Account } from "@utils/types";
5+
import { IndexToken, FTCVesting } from "@utils/contracts";
6+
import DeployHelper from "@utils/deploys";
7+
import {
8+
addSnapshotBeforeRestoreAfterEach,
9+
ether,
10+
getAccounts,
11+
getLastBlockTimestamp,
12+
getRandomAccount,
13+
getWaffleExpect,
14+
increaseTimeAsync,
15+
} from "@utils/index";
16+
import { ContractTransaction } from "ethers";
17+
import { ONE_YEAR_IN_SECONDS } from "@utils/constants";
18+
19+
const expect = getWaffleExpect();
20+
21+
describe("FTCVesting", () => {
22+
let owner: Account;
23+
let treasury: Account;
24+
let recipient: Account;
25+
26+
let deployer: DeployHelper;
27+
let index: IndexToken;
28+
29+
before(async () => {
30+
[owner, treasury, recipient] = await getAccounts();
31+
32+
deployer = new DeployHelper(owner.wallet);
33+
34+
index = await deployer.token.deployIndexToken(owner.address);
35+
});
36+
37+
addSnapshotBeforeRestoreAfterEach();
38+
39+
describe("#constructor", async () => {
40+
let subjectVestingAmount: BigNumber;
41+
let subjectVestingStart: BigNumber;
42+
let subjectVestingCliff: BigNumber;
43+
let subjectVestingEnd: BigNumber;
44+
45+
beforeEach(async () => {
46+
const now = await getLastBlockTimestamp();
47+
subjectVestingStart = now;
48+
subjectVestingCliff = now.add(60 * 60 * 24 * 183);
49+
subjectVestingEnd = now.add(60 * 60 * 24 * 547);
50+
subjectVestingAmount = ether(100);
51+
});
52+
53+
async function subject(): Promise<FTCVesting> {
54+
return await deployer.token.deployFtcVesting(
55+
index.address,
56+
recipient.address,
57+
treasury.address,
58+
subjectVestingAmount,
59+
subjectVestingStart,
60+
subjectVestingCliff,
61+
subjectVestingEnd,
62+
);
63+
}
64+
65+
it("should set the correct state variables", async () => {
66+
const ftcVest = await subject();
67+
68+
expect(await ftcVest.recipient()).to.eq(recipient.address);
69+
expect(await ftcVest.treasury()).to.eq(treasury.address);
70+
expect(await ftcVest.vestingAmount()).to.eq(subjectVestingAmount);
71+
expect(await ftcVest.vestingBegin()).to.eq(subjectVestingStart);
72+
expect(await ftcVest.vestingEnd()).to.eq(subjectVestingEnd);
73+
expect(await ftcVest.vestingCliff()).to.eq(subjectVestingCliff);
74+
expect(await ftcVest.lastUpdate()).to.eq(subjectVestingStart);
75+
});
76+
});
77+
78+
describe("#setRecipient", async () => {
79+
let subjectVestingAmount: BigNumber;
80+
let subjectVestingStart: BigNumber;
81+
let subjectVestingCliff: BigNumber;
82+
let subjectVestingEnd: BigNumber;
83+
let subjectFtcVesting: FTCVesting;
84+
let newRecipient: Account;
85+
86+
beforeEach(async () => {
87+
const now = await getLastBlockTimestamp();
88+
newRecipient = await getRandomAccount();
89+
subjectVestingStart = now;
90+
subjectVestingCliff = now.add(60 * 60 * 24 * 183);
91+
subjectVestingEnd = now.add(60 * 60 * 24 * 547);
92+
subjectVestingAmount = ether(100);
93+
94+
subjectFtcVesting = await deployer.token.deployFtcVesting(
95+
index.address,
96+
recipient.address,
97+
treasury.address,
98+
subjectVestingAmount,
99+
subjectVestingStart,
100+
subjectVestingCliff,
101+
subjectVestingEnd,
102+
);
103+
});
104+
105+
async function subject(): Promise<ContractTransaction> {
106+
return subjectFtcVesting.connect(recipient.wallet).setRecipient(newRecipient.address);
107+
}
108+
109+
it("should set new recipient address", async () => {
110+
await subject();
111+
112+
const newRecipientAddress = await subjectFtcVesting.recipient();
113+
114+
expect(newRecipientAddress).to.eq(newRecipient.address);
115+
});
116+
});
117+
118+
describe("#setTreasury", async () => {
119+
let subjectVestingAmount: BigNumber;
120+
let subjectVestingStart: BigNumber;
121+
let subjectVestingCliff: BigNumber;
122+
let subjectVestingEnd: BigNumber;
123+
let subjectFtcVesting: FTCVesting;
124+
let newTreasury: Account;
125+
126+
beforeEach(async () => {
127+
const now = await getLastBlockTimestamp();
128+
newTreasury = await getRandomAccount();
129+
subjectVestingStart = now;
130+
subjectVestingCliff = now.add(60 * 60 * 24 * 183);
131+
subjectVestingEnd = now.add(60 * 60 * 24 * 547);
132+
subjectVestingAmount = ether(100);
133+
134+
subjectFtcVesting = await deployer.token.deployFtcVesting(
135+
index.address,
136+
recipient.address,
137+
treasury.address,
138+
subjectVestingAmount,
139+
subjectVestingStart,
140+
subjectVestingCliff,
141+
subjectVestingEnd,
142+
);
143+
});
144+
145+
async function subject(): Promise<ContractTransaction> {
146+
return subjectFtcVesting.connect(treasury.wallet).setTreasury(newTreasury.address);
147+
}
148+
149+
it("should set new treasury address", async () => {
150+
const currentTreasuryAddress = await subjectFtcVesting.treasury();
151+
152+
expect(currentTreasuryAddress).to.eq(treasury.address);
153+
154+
await subject();
155+
156+
const newTreasuryAddress = await subjectFtcVesting.treasury();
157+
158+
expect(newTreasuryAddress).to.eq(newTreasury.address);
159+
});
160+
});
161+
162+
describe("#claim", async () => {
163+
let subjectVestingAmount: BigNumber;
164+
let subjectVestingStart: BigNumber;
165+
let subjectVestingCliff: BigNumber;
166+
let subjectVestingEnd: BigNumber;
167+
let subjectFtcVesting: FTCVesting;
168+
169+
beforeEach(async () => {
170+
const now = await getLastBlockTimestamp();
171+
subjectVestingStart = now.sub(60 * 60 * 24 * 185);
172+
subjectVestingCliff = now;
173+
subjectVestingEnd = now.add(60 * 60 * 24 * 546);
174+
subjectVestingAmount = ether(1);
175+
176+
subjectFtcVesting = await deployer.token.deployFtcVesting(
177+
index.address,
178+
recipient.address,
179+
treasury.address,
180+
subjectVestingAmount,
181+
subjectVestingStart,
182+
subjectVestingCliff,
183+
subjectVestingEnd,
184+
);
185+
186+
await index.transfer(subjectFtcVesting.address, ether(1000));
187+
});
188+
189+
async function subject(): Promise<ContractTransaction> {
190+
return await subjectFtcVesting.connect(recipient.wallet).claim();
191+
}
192+
193+
it("should make a claim", async () => {
194+
const initialAmount = await index.balanceOf(recipient.address);
195+
const amountInContract = await index.balanceOf(subjectFtcVesting.address);
196+
197+
await subject();
198+
199+
const claimedAmount = await index.balanceOf(recipient.address);
200+
const remainingInContract = await index.balanceOf(subjectFtcVesting.address);
201+
202+
const userHasClaimedSome = claimedAmount.gt(initialAmount);
203+
const contractHasLowerBalance = amountInContract.gt(remainingInContract);
204+
205+
expect(userHasClaimedSome).to.be.true;
206+
expect(contractHasLowerBalance).to.be.true;
207+
});
208+
209+
it("should claim all after 2 years time", async () => {
210+
const amountInContract = await index.balanceOf(subjectFtcVesting.address);
211+
212+
await increaseTimeAsync(ONE_YEAR_IN_SECONDS.mul(3));
213+
await subject();
214+
215+
const remainingInContract = await index.balanceOf(subjectFtcVesting.address);
216+
const claimedByContributor = await index.balanceOf(recipient.address);
217+
const noIndexLeftInContract = remainingInContract.isZero();
218+
const allClaimedByContributor = claimedByContributor.eq(amountInContract);
219+
220+
expect(noIndexLeftInContract).to.be.true;
221+
expect(allClaimedByContributor).to.be.true;
222+
});
223+
224+
context("when the caller is unauthorized", async () => {
225+
let subjectCaller: Account;
226+
227+
beforeEach(async () => {
228+
subjectCaller = await getRandomAccount();
229+
});
230+
231+
async function subject(): Promise<ContractTransaction> {
232+
return await subjectFtcVesting.connect(subjectCaller.wallet).claim();
233+
}
234+
235+
it("should revert", async () => {
236+
await expect(subject()).to.be.revertedWith("unauthorized");
237+
});
238+
});
239+
});
240+
241+
describe("#clawback", async () => {
242+
let subjectVestingAmount: BigNumber;
243+
let subjectVestingStart: BigNumber;
244+
let subjectVestingCliff: BigNumber;
245+
let subjectVestingEnd: BigNumber;
246+
let subjectFtcVesting: FTCVesting;
247+
248+
beforeEach(async () => {
249+
const now = await getLastBlockTimestamp();
250+
subjectVestingStart = now.sub(60 * 60 * 24 * 185);
251+
subjectVestingCliff = now;
252+
subjectVestingEnd = now.add(60 * 60 * 24 * 546);
253+
subjectVestingAmount = ether(1);
254+
255+
subjectFtcVesting = await deployer.token.deployFtcVesting(
256+
index.address,
257+
recipient.address,
258+
treasury.address,
259+
subjectVestingAmount,
260+
subjectVestingStart,
261+
subjectVestingCliff,
262+
subjectVestingEnd,
263+
);
264+
265+
await index.transfer(subjectFtcVesting.address, ether(1000));
266+
});
267+
268+
async function subject(): Promise<ContractTransaction> {
269+
return await subjectFtcVesting.connect(treasury.wallet).clawback();
270+
}
271+
272+
it("should clawback remaining funds", async () => {
273+
const initAmountInContract = await index.balanceOf(subjectFtcVesting.address);
274+
await subject();
275+
276+
const clawedBackAmount = await index.balanceOf(treasury.address);
277+
const balanceOfContract = await index.balanceOf(subjectFtcVesting.address);
278+
const hasSuccessfullyClawedBackAllFunds =
279+
initAmountInContract.eq(clawedBackAmount) && balanceOfContract.isZero();
280+
281+
expect(hasSuccessfullyClawedBackAllFunds).to.be.true;
282+
});
283+
284+
context("when the caller is unauthorized", async () => {
285+
let subjectCaller: Account;
286+
287+
beforeEach(async () => {
288+
subjectCaller = await getRandomAccount();
289+
});
290+
291+
async function subject(): Promise<ContractTransaction> {
292+
return await subjectFtcVesting.connect(subjectCaller.wallet).clawback();
293+
}
294+
295+
it("should revert", async () => {
296+
await expect(subject()).to.be.revertedWith("unauthorized");
297+
});
298+
});
299+
});
300+
});

0 commit comments

Comments
 (0)