diff --git a/contracts/TokenVesting.sol b/contracts/TokenVesting.sol new file mode 100644 index 00000000..d69d2b3a --- /dev/null +++ b/contracts/TokenVesting.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import "./compound/SafeMath.sol"; +import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/IERC20Upgradeable.sol"; +import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol"; + +contract TokenVesting is OwnableUpgradeable { + using SafeMath for uint256; + + uint256 public startTime; + uint256 public constant duration = 86400 * 90; + uint256 public maxClaimedTokens; + uint256 public claimedTokens; + IERC20Upgradeable public ion; + + struct Vest { + uint256 total; + uint256 claimedAmount; + bool isClaimed; + } + + mapping(address => Vest) public vests; + + function initialize(IERC20Upgradeable _ion) external initializer { + ion = _ion; + } + + function setVestingAmounts( + uint256 _totalClaimable, + address[] memory _receivers, + uint256[] memory _amounts + ) onlyOwner external { + require(_receivers.length == _amounts.length); + uint256 claimable; + for (uint256 i = 0; i < _receivers.length; i++) { + require(vests[_receivers[i]].total == 0); + claimable = claimable.add(_amounts[i]); + vests[_receivers[i]].total = _amounts[i]; + } + require(claimable == _totalClaimable); + maxClaimedTokens = maxClaimedTokens.add(claimable); + } + + function start() onlyOwner external { + require(startTime == 0); + startTime = block.timestamp; + } + + function getVestingAmount(address _user) external returns (uint256) { + return vests[_user].total; + } + + function claimable(address _claimer) external view returns (uint256) { + if (startTime == 0) return 0; + Vest storage v = vests[_claimer]; + uint256 elapsedTime = block.timestamp.sub(startTime); + uint256 claimable; + if (elapsedTime > duration) claimable = v.total; + else { + uint256 m = v.total.mul(90).div(100).sub(v.total.mul(25).div(100)); + claimable = v.total.mul(25).div(100).add(m.mul(elapsedTime).div(duration)); + } + return claimable; + } + + function claim(address _receiver) external { + require(startTime != 0); + Vest storage v = vests[msg.sender]; + require(!v.isClaimed, "User already claimed."); + uint256 elapsedTime = block.timestamp.sub(startTime); + uint256 claimable; + if (elapsedTime > duration) claimable = v.total; + else claimable = v.total.mul(25).div(100).add((elapsedTime).div(duration).mul(650)); + v.claimedAmount = claimable; + claimedTokens = claimedTokens.add(claimable); + require(claimedTokens <= maxClaimedTokens); + ion.transfer(msg.sender, claimable); + } +} \ No newline at end of file diff --git a/contracts/test/TokenVestingTest.t.sol b/contracts/test/TokenVestingTest.t.sol new file mode 100644 index 00000000..b066f7d8 --- /dev/null +++ b/contracts/test/TokenVestingTest.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import "./config/BaseTest.t.sol"; +import "../TokenVesting.sol"; + +contract TokenVestingTest is BaseTest { + TokenVesting public tokenVesting; + address alice = vm.addr(1); + address bob = vm.addr(2); + + struct Vest { + uint256 total; + uint256 claimedAmount; + bool isClaimed; + } + + function setUp() public { + tokenVesting = new TokenVesting(); + } + + function test_settingVestingAmounts() public { + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + vm.prank(tokenVesting.owner()); + tokenVesting.setVestingAmounts(1000, addresses, amounts); + + assertEq(tokenVesting.getVestingAmount(alice), 700); + assertEq(tokenVesting.getVestingAmount(bob), 300); + } + + function testFail_nonOwnerSettingVestingAmounts() public { + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + vm.prank(alice); + tokenVesting.setVestingAmounts(1000, addresses, amounts); + } + + function testFail_totalClaimableSettingVestingAmounts() public { + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + vm.prank(tokenVesting.owner()); + tokenVesting.setVestingAmounts(900, addresses, amounts); + } + + function test_start() public { + vm.prank(tokenVesting.owner()); + tokenVesting.start(); + + assertEq(tokenVesting.startTime(), block.timestamp); + } + + function testFail_nonOwnerStart() public { + vm.prank(alice); + tokenVesting.start(); + } + + function testFail_secondCallStart() public { + vm.prank(tokenVesting.owner()); + tokenVesting.start(); + tokenVesting.start(); + } + + function test_claimableBeforeStart() public { + tokenVesting.claimable(alice); + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + vm.prank(tokenVesting.owner()); + tokenVesting.setVestingAmounts(1000, addresses, amounts); + + assertEq(tokenVesting.getVestingAmount(alice), 700); + assertEq(tokenVesting.getVestingAmount(bob), 300); + + assertEq(tokenVesting.claimable(alice), 0); + } + + function test_claimableAfter1Day() public { + vm.startPrank(tokenVesting.owner()); + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + tokenVesting.setVestingAmounts(1000, addresses, amounts); + tokenVesting.start(); + vm.stopPrank(); + + vm.warp(86400); + + uint256 m = uint256(700)*90/100-uint256(700)*25/100; + uint256 expectedAliceClaimableAmount = uint256(700)*25/100+m*1*86400/(90*86400); + assertEq(tokenVesting.claimable(alice), expectedAliceClaimableAmount); + uint256 mBob = uint256(300)*90/100-uint256(300)*25/100; + uint256 expectedBobClaimableAmount = uint256(300)*25/100+mBob*1*86400/(90*86400); + assertEq(tokenVesting.claimable(bob), expectedBobClaimableAmount); + } + + function test_claimableAfter50Days() public { + vm.startPrank(tokenVesting.owner()); + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + tokenVesting.setVestingAmounts(1000, addresses, amounts); + tokenVesting.start(); + vm.stopPrank(); + + vm.warp(50*86400); + + uint256 m = uint256(700)*90/100-uint256(700)*25/100; + uint256 expectedAliceClaimableAmount = uint256(700)*25/100+m*50*86400/(90*86400); + assertEq(tokenVesting.claimable(alice), expectedAliceClaimableAmount); + + uint256 mBob = uint256(300)*90/100-uint256(300)*25/100; + uint256 expectedBobClaimableAmount = uint256(300)*25/100+mBob*50*86400/(90*86400); + assertEq(tokenVesting.claimable(bob), expectedBobClaimableAmount); + } + + function test_claimableAfter90Days() public { + vm.startPrank(tokenVesting.owner()); + address[] memory addresses = new address[](2); + addresses[0] = alice; + addresses[1] = bob; + + uint256[] memory amounts = new uint256[](2); + amounts[0] = 700; + amounts[1] = 300; + + tokenVesting.setVestingAmounts(1000, addresses, amounts); + tokenVesting.start(); + vm.stopPrank(); + + vm.warp(10000000); + + uint256 expectedAliceClaimableAmount = 700; + uint256 expectedBobClaimableAmount = 300; + assertEq(tokenVesting.claimable(alice), expectedAliceClaimableAmount); + assertEq(tokenVesting.claimable(bob), expectedBobClaimableAmount); + } + + // TODO: Add tests for function claim() +}