Skip to content

Commit 824a1d2

Browse files
committed
add retroactive airdrop linear releases contract
1 parent 13937a6 commit 824a1d2

File tree

3 files changed

+214
-1
lines changed

3 files changed

+214
-1
lines changed

.github/workflows/node.js.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ name: Node.js CI
55

66
on:
77
push:
8-
branches: [ main, tax-token-support, bsc, dev, limit-order ]
8+
branches: [ main, tax-token-support, bsc, dev, limit-order, retroactive_airdrop_linear_releases ]
99
pull_request:
1010
branches: [ main ]
1111

contracts/RetroactiveAirdropLock.sol

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
pragma solidity 0.7.6;
3+
4+
5+
import "@openzeppelin/contracts/math/SafeMath.sol";
6+
import "./gov/OLEToken.sol";
7+
import "./Adminable.sol";
8+
9+
/// @title OLE token Locked
10+
/// @author OpenLeverage
11+
/// @notice Release retroactive airdrop OLE to beneficiaries linearly.
12+
contract RetroactiveAirdropLock is Adminable{
13+
using SafeMath for uint256;
14+
uint128 public startTime;
15+
uint128 public endTime;
16+
uint128 public expireTime;
17+
OLEToken public token;
18+
mapping(address => ReleaseVar) public releaseVars;
19+
20+
event Release(address beneficiary, uint amount);
21+
22+
struct ReleaseVar {
23+
uint256 amount;
24+
uint128 lastUpdateTime;
25+
}
26+
27+
constructor(OLEToken token_, address payable _admin, uint128 startTime_, uint128 endTime_, uint128 expireTime_) {
28+
require(endTime_ > startTime_, "StartTime must be earlier than endTime");
29+
require(expireTime_ > endTime_, "EndTime must be earlier than expireTime");
30+
startTime = startTime_;
31+
endTime = endTime_;
32+
expireTime = expireTime_;
33+
admin = _admin;
34+
token = token_;
35+
}
36+
37+
function setReleaseBatch(address[] memory beneficiaries, uint256[] memory amounts) external onlyAdmin{
38+
require(beneficiaries.length == amounts.length, "Length must be same");
39+
for (uint i = 0; i < beneficiaries.length; i++) {
40+
address beneficiary = beneficiaries[i];
41+
require(releaseVars[beneficiary].amount == 0, 'Beneficiary is exist');
42+
releaseVars[beneficiary] = ReleaseVar(amounts[i], startTime);
43+
}
44+
}
45+
46+
function release() external {
47+
require(expireTime >= block.timestamp, "time expired");
48+
releaseInternal(msg.sender);
49+
}
50+
51+
function withdraw(address to) external onlyAdmin{
52+
uint256 amount = token.balanceOf(address(this));
53+
require(amount > 0, "no amount available");
54+
token.transfer(to, amount);
55+
}
56+
57+
function releaseInternal(address beneficiary) internal {
58+
uint256 amount = token.balanceOf(address(this));
59+
uint256 releaseAmount = releaseAbleAmount(beneficiary);
60+
// The transfer out limit exceeds the available limit of the account
61+
require(releaseAmount > 0, "no releasable amount");
62+
require(amount >= releaseAmount, "transfer out limit exceeds");
63+
releaseVars[beneficiary].lastUpdateTime = uint128(block.timestamp > endTime ? endTime : block.timestamp);
64+
token.transfer(beneficiary, releaseAmount);
65+
emit Release(beneficiary, releaseAmount);
66+
}
67+
68+
function releaseAbleAmount(address beneficiary) public view returns (uint256){
69+
ReleaseVar memory releaseVar = releaseVars[beneficiary];
70+
require(block.timestamp >= startTime, "not time to unlock");
71+
require(releaseVar.amount > 0, "beneficiary does not exist");
72+
uint256 calTime = block.timestamp > endTime ? endTime : block.timestamp;
73+
return calTime.sub(releaseVar.lastUpdateTime).mul(releaseVar.amount)
74+
.div(endTime - startTime);
75+
}
76+
77+
function lockedAmount(address beneficiary) public view returns (uint256){
78+
ReleaseVar memory releaseVar = releaseVars[beneficiary];
79+
require(endTime >= block.timestamp, 'locked end');
80+
return releaseVar.amount.mul(endTime - releaseVar.lastUpdateTime)
81+
.div(endTime - startTime);
82+
}
83+
84+
}

test/RetroactiveAirdropLockTest.js

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const {toBN} = require("./utils/EtheUtil");
2+
3+
const {toWei, lastBlockTime, toETH, firstStr, assertThrows} = require("./utils/OpenLevUtil");
4+
const RetroactiveAirdropLock = artifacts.require("RetroactiveAirdropLock");
5+
const OLEToken = artifacts.require("OLEToken");
6+
7+
8+
const m = require('mocha-logger');
9+
10+
const timeMachine = require('ganache-time-traveler');
11+
const utils = require("./utils/OpenLevUtil");
12+
const {from} = require("truffle/build/987.bundled");
13+
14+
contract("RetroactiveAirdropLock", async accounts => {
15+
let oleToken;
16+
let currentBlockTime;
17+
let timeLock;
18+
19+
beforeEach(async () => {
20+
oleToken = await OLEToken.new(accounts[0], accounts[0], 'TEST', 'TEST');
21+
currentBlockTime = parseInt(await lastBlockTime());
22+
timeLock = await utils.createTimelock(accounts[0]);
23+
});
24+
25+
it("Claim before start time", async () => {
26+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, currentBlockTime + 1000000, currentBlockTime + 2000000, currentBlockTime + 3000000);
27+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
28+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
29+
await oleToken.transfer(lock.address, toWei(100000));
30+
await assertThrows(lock.release({from: accounts[1]}), 'not time to unlock');
31+
});
32+
33+
it("Claim after end time and before expire time", async () => {
34+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 10000000);
35+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
36+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
37+
await oleToken.transfer(lock.address, toWei(100000));
38+
await lock.release({from: accounts[1]});
39+
assert.equal((await lock.releaseAbleAmount(accounts[1])), 0);
40+
assert.equal(toBN(100000).mul(toBN(1e18)).toString(), (await oleToken.balanceOf(accounts[1])).toString());
41+
});
42+
43+
it("Claim after expire time", async () => {
44+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime - 10000000, currentBlockTime - 1);
45+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
46+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
47+
await oleToken.transfer(lock.address, toWei(100000));
48+
await assertThrows(lock.release({from: accounts[1]}), 'time expired');
49+
});
50+
51+
it("Claim address is non beneficiary: ", async () => {
52+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 10000000);
53+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
54+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
55+
await oleToken.transfer(lock.address, toWei(100000));
56+
await assertThrows(lock.release({from: accounts[2]}), 'beneficiary does not exist');
57+
});
58+
59+
it("Claim many times, is the amount correct each time: ", async () => {
60+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, currentBlockTime - 100000, currentBlockTime + 100000, currentBlockTime + 200000);
61+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
62+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
63+
await oleToken.transfer(lock.address, toWei(100000));
64+
await lock.release({from: accounts[1]});
65+
assert.equal(50000, (await oleToken.balanceOf(accounts[1])).div(toBN(1e18)));
66+
67+
m.log("Wait for 50000 seconds ....");
68+
let takeSnapshot = await timeMachine.takeSnapshot();
69+
let shotId = takeSnapshot['result'];
70+
await timeMachine.advanceTime(50000);
71+
await lock.release({from: accounts[1]});
72+
assert.equal(75000, (await oleToken.balanceOf(accounts[1])).div(toBN(1e18)));
73+
74+
m.log("Wait for 50000 seconds again....");
75+
await timeMachine.advanceTime(50000);
76+
await lock.release({from: accounts[1]});
77+
assert.equal(toBN(100000).mul(toBN(1e18)).toString(), (await oleToken.balanceOf(accounts[1])).toString());
78+
// check lastUpdateTime
79+
let accountReleaseVar = await lock.releaseVars(accounts[1]);
80+
assert.equal(currentBlockTime + 100000, accountReleaseVar[1]);
81+
await timeMachine.revertToSnapshot(shotId);
82+
});
83+
84+
it("If the claim is completed, is it wrong to claim again: ", async () => {
85+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime - 1, currentBlockTime + 30000);
86+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
87+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1]], [toWei(100000)]]), 0);
88+
await oleToken.transfer(lock.address, toWei(100000));
89+
await lock.release({from: accounts[1]});
90+
assert.equal(toWei(100000).toString(), (await oleToken.balanceOf(accounts[1])).toString());
91+
await assertThrows(lock.release({from: accounts[1]}), 'no releasable amount');
92+
});
93+
94+
it("Two accounts, two addresses, partial claim: ", async () => {
95+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 60000);
96+
await timeLock.executeTransaction(lock.address, 0, 'setReleaseBatch(address[],uint256[])',
97+
web3.eth.abi.encodeParameters(['address[]', 'uint256[]'], [[accounts[1], accounts[2]], [toWei(100000), toWei(100000)]]), 0);
98+
await oleToken.transfer(lock.address, toWei(100000));
99+
await lock.release({from: accounts[1]});
100+
await oleToken.transfer(lock.address, toWei(100000));
101+
await lock.release({from: accounts[2]})
102+
103+
assert.equal(toWei(100000).toString(), (await oleToken.balanceOf(accounts[1])).toString());
104+
assert.equal(toWei(100000).toString(), (await oleToken.balanceOf(accounts[2])).toString());
105+
});
106+
107+
it("Withdraw not by timeLock", async () => {
108+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 10000000);
109+
await oleToken.transfer(lock.address, toWei(100000));
110+
await assertThrows(lock.withdraw(accounts[1], {from: accounts[1]}), 'caller must be admin');
111+
});
112+
113+
it("Set release not by timeLock", async () => {
114+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 10000000);
115+
await oleToken.transfer(lock.address, toWei(100000));
116+
await assertThrows(lock.setReleaseBatch([accounts[1]], [toWei(100000)], {from: accounts[3]}), 'caller must be admin');
117+
});
118+
119+
it("Withdraw by timeLock", async () => {
120+
let lock = await RetroactiveAirdropLock.new(oleToken.address, timeLock.address, '1599372311', currentBlockTime, currentBlockTime + 10000000);
121+
await oleToken.transfer(lock.address, toWei(100000));
122+
let beforeWithdraw = await oleToken.balanceOf(accounts[1]);
123+
await timeLock.executeTransaction(lock.address, 0, 'withdraw(address)', web3.eth.abi.encodeParameters(['address'], [accounts[1]]), 0);
124+
let afterWithdraw = await oleToken.balanceOf(accounts[1]);
125+
assert.equal(toBN(100000).mul(toBN(1e18)), afterWithdraw - beforeWithdraw);
126+
});
127+
})
128+
129+

0 commit comments

Comments
 (0)