├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── brownie-config.yaml └── contracts ├── DynamicVestingEscrow.sol └── test └── SafeERC20.sol /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .history 3 | .hypothesis/ 4 | build/ 5 | reports/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Curve Finance 4 | Copyright (c) 2021 Yearn Finance 5 | Copyright (c) 2021 vasa (@vasa-develop) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## dynamic-vesting-escrow 2 | 3 | > A vesting escrow for dynamic teams, based on [Curve vesting escrow](https://github.com/curvefi/curve-dao-contracts) 4 | 5 | ### Features: 6 | 7 | - Ability to claim a specific amount. 8 | - Recipient specific `startTime`, `endTime`. Configurable flag `ALLOW_PAST_START_TIME` to decide if `startTime` can be in past. 9 | - Recipient specific cliff duration (min duration after start time to call claim). 10 | - Recipient specific pause, unpause, terminate. `Paused` freezes the recipient's vesting state (recipient can claim vested tokens), and updates the recipient specific parameters when `UnPaused` so that number of tokens vesting per second (can be unique for each recipient) remains the same for complete lifecycle of the recipient. 11 | - No limit on recipient pause duration. Any recipient can be paused, unpaused as many times as the owner wishes. 12 | - Ability to automatically transfer unvested tokens of the `Terminated` recipient to a "safe" address, and transfer all the vested tokens to the recipient. 13 | - Global vesting escrow termination. Ability to transfer (seize) unvested tokens to a "safe" address. The recipient(s) is/are still entitled to the vested portion. 14 | - Ability to add more recipients (no dilution) in the vesting escrow at any point in time (provided that escrow is not terminated). 15 | 16 | ### Known Issue 17 | 18 | This is a minor issue, which **does NOT effect the security of the contract in a significant way**. 19 | 20 | Due to rounding error, `totalLockedOf` returns a value different (slightly off) from the expected value. But the return values of `totalLockedOf` are capped so that at the end of the vesting period, the recipient can claim exactly the `totalVestingAmount` assigned to it. 21 | 22 | This rounding error also effects functions which use `totalLockedOf`: 23 | 24 | - `claimableAmountFor` 25 | - `batchTotalLockedOf` 26 | - `seizeLockedTokens` 27 | 28 | **This contract may have more issues, so test it yourself before using it in production.** 29 | -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | # use Ganache's forked mainnet mode as the default network 2 | networks: 3 | default: mainnet-fork 4 | development: 5 | cmd_settings: 6 | accounts: 100 7 | 8 | # automatically fetch contract sources from Etherscan 9 | autofetch_sources: True 10 | 11 | # require OpenZepplin Contracts 12 | dependencies: 13 | - OpenZeppelin/openzeppelin-contracts@3.2.0 14 | 15 | # path remapping to support OpenZepplin imports with NPM-style path 16 | compiler: 17 | solc: 18 | remappings: 19 | - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@3.2.0" 20 | 21 | reports: 22 | exclude_paths: 23 | - contracts/test/Token.sol 24 | exclude_contracts: 25 | - SafeMath 26 | 27 | dev_deployment_artifacts: True 28 | -------------------------------------------------------------------------------- /contracts/DynamicVestingEscrow.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.6.12; 4 | 5 | /// @title Dynamic Vesting Escrow 6 | /// @author Curve Finance, Yearn Finance, vasa (@vasa-develop) 7 | /// @notice A vesting escsrow for dynamic teams, based on Curve vesting escrow 8 | /// @dev A vesting escsrow for dynamic teams, based on Curve vesting escrow 9 | 10 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 11 | import "@openzeppelin/contracts/math/SafeMath.sol"; 12 | import "@openzeppelin/contracts/utils/Address.sol"; 13 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 14 | import "@openzeppelin/contracts/math/Math.sol"; 15 | import "@openzeppelin/contracts/access/Ownable.sol"; 16 | 17 | contract DynamicVestingEscrow is Ownable { 18 | using SafeERC20 for IERC20; 19 | using Address for address; 20 | using SafeMath for uint256; 21 | 22 | /** 23 | Paused: Vesting is paused. Recipient can be Unpaused by the owner. 24 | UnPaused: Vesting is unpaused. The vesting resumes from the time it was paused (in case the recipient was paused). 25 | Terminated: Recipient is terminated, meaning vesting is stopped and claims are blocked forever. No way to go back. 26 | */ 27 | enum Status {Terminated, Paused, UnPaused} 28 | 29 | struct Recipient { 30 | uint256 startTime; // timestamp at which vesting period will start (should be in future) 31 | uint256 endTime; // timestamp at which vesting period will end (should be in future) 32 | uint256 cliffDuration; // time duration after startTime before which the recipient cannot call claim 33 | uint256 lastPausedAt; // latest timestamp at which vesting was paused 34 | uint256 vestingPerSec; // constant number of tokens that will be vested per second. 35 | uint256 totalVestingAmount; // total amount that can be vested over the vesting period. 36 | uint256 totalClaimed; // total amount of tokens that have been claimed by the recipient. 37 | Status recipientVestingStatus; // current vesting status 38 | } 39 | 40 | mapping(address => Recipient) public recipients; // mapping from recipient address to Recipient struct 41 | mapping(address => bool) public lockedTokensSeizedFor; // in case of escrow termination, a mapping to keep track of which 42 | address public immutable token; // vesting token address 43 | // WARNING: The contract assumes that the token address is NOT malicious. 44 | 45 | uint256 public dust; // total amount of token that is sitting as dust in this contract (unallocatedSupply) 46 | uint256 public totalClaimed; // total number of tokens that have been claimed. 47 | uint256 public totalAllocatedSupply; // total token allocated to the recipients via addRecipients. 48 | uint256 public ESCROW_TERMINATED_AT; // timestamp at which escow terminated. 49 | address public immutable SAFE_ADDRESS; // an address where all the funds are sent in case any recipient or vesting escrow is terminated. 50 | bool public ALLOW_PAST_START_TIME = false; // a flag that decides if past startTime is allowed for any recipient. 51 | bool public ESCROW_TERMINATED = false; // global switch to terminate the vesting escrow. See more info in terminateVestingEscrow() 52 | 53 | modifier escrowNotTerminated() { 54 | // escrow should NOT be in terminated state 55 | require(!ESCROW_TERMINATED, "escrowNotTerminated: escrow terminated"); 56 | _; 57 | } 58 | 59 | modifier isNonZeroAddress(address recipient) { 60 | // recipient should NOT be a 0 address 61 | require(recipient != address(0), "isNonZeroAddress: 0 address"); 62 | _; 63 | } 64 | 65 | modifier recipientIsUnpaused(address recipient) { 66 | // recipient should NOT be a 0 address 67 | require(recipient != address(0), "recipientIsUnpaused: 0 address"); 68 | // recipient should be in UnPaused status 69 | require( 70 | recipients[recipient].recipientVestingStatus == Status.UnPaused, 71 | "recipientIsUnpaused: recipient NOT in UnPaused state" 72 | ); 73 | _; 74 | } 75 | 76 | modifier recipientIsNotTerminated(address recipient) { 77 | // recipient should NOT be a 0 address 78 | require(recipient != address(0), "recipientIsNotTerminated: 0 address"); 79 | // recipient should NOT be in Terminated status 80 | require( 81 | recipients[recipient].recipientVestingStatus != Status.Terminated, 82 | "recipientIsNotTerminated: recipient terminated" 83 | ); 84 | _; 85 | } 86 | 87 | constructor(address _token, address _safeAddress) public { 88 | // SAFE_ADDRESS should NOT be 0 address 89 | require( 90 | _safeAddress != address(0), 91 | "constructor: SAFE_ADDRESS cannot be 0 address" 92 | ); 93 | // token should NOT be 0 address 94 | require(_token != address(0), "constructor: token cannot be 0 address"); 95 | SAFE_ADDRESS = _safeAddress; 96 | token = _token; 97 | } 98 | 99 | /// @notice Terminates the vesting escrow forever. 100 | /// @dev All the vesting states will be freezed, recipients can still claim their vested tokens. 101 | /// Only owner of the vesting escrow can invoke this function. 102 | /// Can only be invoked if the escrow is NOT terminated. 103 | function terminateVestingEscrow() external onlyOwner escrowNotTerminated { 104 | // set termination variables 105 | ESCROW_TERMINATED = true; 106 | ESCROW_TERMINATED_AT = block.timestamp; 107 | } 108 | 109 | /// @notice Updates the SAFE_ADDRESS 110 | /// @dev It is assumed that the SAFE_ADDRESS is NOT malicious 111 | /// Only owner of the vesting escrow can invoke this function. 112 | /// Can only be invoked if the escrow is NOT terminated. 113 | /// @param safeAddress An address where all the tokens are transferred in case of a (recipient/escrow) termination 114 | function updateSafeAddress(address safeAddress) 115 | external 116 | onlyOwner 117 | escrowNotTerminated 118 | { 119 | // Check if the safeAddress is NOT a 0 address 120 | require( 121 | safeAddress != address(0), 122 | "updateSafeAddress: SAFE_ADDRESS cannot be 0 address" 123 | ); 124 | SAFE_ADDRESS = safeAddress; 125 | } 126 | 127 | /// @notice Add and fund new recipients. 128 | /// @dev Owner of the vesting escrow needs to approve tokens to this contract 129 | /// Only owner of the vesting escrow can invoke this function. 130 | /// Can only be invoked if the escrow is NOT terminated. 131 | /// @param _recipients An array of recipient addresses 132 | /// @param _amounts An array of amounts to be vested by the corresponding recipient addresses 133 | /// @param _startTimes An array of startTimes of the vesting schedule for the corresponding recipient addresses 134 | /// @param _endTimes An array of endTimes of the vesting schedule for the corresponding recipient addresses 135 | /// @param _cliffDurations An array of cliff durations of the vesting schedule for the corresponding recipient addresses 136 | /// @param _totalAmount Total sum of the amounts in the _amounts array 137 | function addRecipients( 138 | address[] calldata _recipients, 139 | uint256[] calldata _amounts, 140 | uint256[] calldata _startTimes, 141 | uint256[] calldata _endTimes, 142 | uint256[] calldata _cliffDurations, 143 | uint256 _totalAmount 144 | ) external onlyOwner escrowNotTerminated { 145 | // Every input should be of equal length (greater than 0) 146 | require( 147 | (_recipients.length == _amounts.length) && 148 | (_amounts.length == _startTimes.length) && 149 | (_startTimes.length == _endTimes.length) && 150 | (_endTimes.length == _cliffDurations.length) && 151 | (_recipients.length != 0), 152 | "addRecipients: invalid params" 153 | ); 154 | 155 | // _totalAmount should be greater than 0 156 | require( 157 | _totalAmount > 0, 158 | "addRecipients: zero totalAmount not allowed" 159 | ); 160 | 161 | // transfer funds from the msg.sender 162 | // Will fail if the allowance is less than _totalAmount 163 | IERC20(token).safeTransferFrom(msg.sender, address(this), _totalAmount); 164 | 165 | // register _totalAmount before allocation 166 | uint256 _before = _totalAmount; 167 | 168 | // populate recipients mapping 169 | for (uint256 i = 0; i < _amounts.length; i++) { 170 | // recipient should NOT be a 0 address 171 | require( 172 | _recipients[i] != address(0), 173 | "addRecipients: recipient cannot be 0 address" 174 | ); 175 | // if past startTime is NOT allowed, then the startTime should be in future 176 | require( 177 | ALLOW_PAST_START_TIME || (_startTimes[i] >= block.timestamp), 178 | "addRecipients: invalid startTime" 179 | ); 180 | // endTime should be greater than startTime 181 | require( 182 | _endTimes[i] > _startTimes[i], 183 | "addRecipients: endTime should be after startTime" 184 | ); 185 | // cliffDuration should be less than vesting duration 186 | require( 187 | _cliffDurations[i] < _endTimes[i].sub(_startTimes[i]), 188 | "addRecipients: cliffDuration too long" 189 | ); 190 | // amount should be greater than 0 191 | require( 192 | _amounts[i] > 0, 193 | "addRecipients: vesting amount cannot be 0" 194 | ); 195 | // add recipient to the recipients mapping 196 | recipients[_recipients[i]] = Recipient( 197 | _startTimes[i], 198 | _endTimes[i], 199 | _cliffDurations[i], 200 | 0, 201 | // vestingPerSec = totalVestingAmount/(endTimes-(startTime+cliffDuration)) 202 | _amounts[i].div( 203 | _endTimes[i].sub(_startTimes[i].add(_cliffDurations[i])) 204 | ), 205 | _amounts[i], 206 | 0, 207 | Status.UnPaused 208 | ); 209 | // reduce _totalAmount 210 | // Will revert if the _totalAmount is less than sum of _amounts 211 | _totalAmount = _totalAmount.sub(_amounts[i]); 212 | } 213 | // add the allocated token amount to totalAllocatedSupply 214 | totalAllocatedSupply = totalAllocatedSupply.add( 215 | _before.sub(_totalAmount) 216 | ); 217 | // register remaining _totalAmount as dust 218 | dust = dust.add(_totalAmount); 219 | } 220 | 221 | /// @notice Pause recipient vesting 222 | /// @dev This freezes the vesting schedule for the paused recipient. 223 | /// Recipient will NOT be able to claim until unpaused. 224 | /// Only owner of the vesting escrow can invoke this function. 225 | /// Can only be invoked if the escrow is NOT terminated. 226 | /// @param recipient The recipient address for which vesting will be paused. 227 | function pauseRecipient(address recipient) 228 | external 229 | onlyOwner 230 | escrowNotTerminated 231 | isNonZeroAddress(recipient) 232 | { 233 | // current recipient status should be UnPaused 234 | require( 235 | recipients[recipient].recipientVestingStatus == Status.UnPaused, 236 | "pauseRecipient: cannot pause" 237 | ); 238 | // set vesting status of the recipient as Paused 239 | recipients[recipient].recipientVestingStatus = Status.Paused; 240 | // set lastPausedAt timestamp 241 | recipients[recipient].lastPausedAt = block.timestamp; 242 | } 243 | 244 | /// @notice UnPause recipient vesting 245 | /// @dev This unfreezes the vesting schedule for the paused recipient. Recipient will be able to claim. 246 | /// In order to keep vestingPerSec for the recipient a constant, cliffDuration and endTime for the 247 | /// recipient are shifted by the pause duration so that the recipient resumes with the same state 248 | /// at the time it was paused. 249 | /// Only owner of the vesting escrow can invoke this function. 250 | /// Can only be invoked if the escrow is NOT terminated. 251 | /// @param recipient The recipient address for which vesting will be unpaused. 252 | function unPauseRecipient(address recipient) 253 | external 254 | onlyOwner 255 | escrowNotTerminated 256 | isNonZeroAddress(recipient) 257 | { 258 | // current recipient status should be Paused 259 | require( 260 | recipients[recipient].recipientVestingStatus == Status.Paused, 261 | "unPauseRecipient: cannot unpause" 262 | ); 263 | // set vesting status of the recipient as "UnPaused" 264 | recipients[recipient].recipientVestingStatus = Status.UnPaused; 265 | // calculate the time for which the recipient was paused for 266 | uint256 pausedFor = block.timestamp.sub( 267 | recipients[recipient].lastPausedAt 268 | ); 269 | // extend the cliffDuration by the pause duration 270 | recipients[recipient].cliffDuration = recipients[recipient] 271 | .cliffDuration 272 | .add(pausedFor); 273 | // extend the endTime by the pause duration 274 | recipients[recipient].endTime = recipients[recipient].endTime.add( 275 | pausedFor 276 | ); 277 | } 278 | 279 | /// @notice Terminate recipient vesting 280 | /// @dev This terminates the vesting schedule for the recipient forever. 281 | /// Recipient will NOT be able to claim. 282 | /// Only owner of the vesting escrow can invoke this function. 283 | /// Can only be invoked if the escrow is NOT terminated. 284 | /// @param recipient The recipient address for which vesting will be terminated. 285 | function terminateRecipient(address recipient) 286 | external 287 | onlyOwner 288 | escrowNotTerminated 289 | isNonZeroAddress(recipient) 290 | { 291 | // current recipient status should NOT be Terminated 292 | require( 293 | recipients[recipient].recipientVestingStatus != Status.Terminated, 294 | "terminateRecipient: cannot terminate" 295 | ); 296 | // claim for the user if possible 297 | if (canClaim(recipient)) { 298 | // transfer unclaimed tokens to the recipient 299 | _claimFor(claimableAmountFor(recipient), recipient); 300 | // transfer locked tokens to the SAFE_ADDRESS 301 | } 302 | uint256 _bal = recipients[recipient].totalVestingAmount.sub( 303 | recipients[recipient].totalClaimed 304 | ); 305 | IERC20(token).safeTransfer(SAFE_ADDRESS, _bal); 306 | // set vesting status of the recipient as "Terminated" 307 | recipients[recipient].recipientVestingStatus = Status.Terminated; 308 | } 309 | 310 | /// @notice Claim a specific amount of tokens. 311 | /// @dev Claim a specific amount of tokens. 312 | /// Will revert if amount parameter is greater than the claimable amount 313 | /// of tokens for the recipient at the time of function invocation. 314 | /// Can be invoked by any non-terminated recipient. 315 | /// @param amount The amount of tokens recipient wants to claim. 316 | function claim(uint256 amount) external { 317 | _claimFor(amount, msg.sender); 318 | } 319 | 320 | // claim tokens for a specific recipient 321 | function _claimFor(uint256 _amount, address _recipient) internal { 322 | // get recipient 323 | Recipient storage recipient = recipients[_recipient]; 324 | 325 | // recipient should be able to claim 326 | require(canClaim(_recipient), "_claimFor: recipient cannot claim"); 327 | 328 | // max amount the user can claim right now 329 | uint256 claimableAmount = claimableAmountFor(_recipient); 330 | 331 | // amount parameter should be less or equal to than claimable amount 332 | require( 333 | _amount <= claimableAmount, 334 | "_claimFor: cannot claim passed amount" 335 | ); 336 | 337 | // increase user specific totalClaimed 338 | recipient.totalClaimed = recipient.totalClaimed.add(_amount); 339 | 340 | // user's totalClaimed should NOT be greater than user's totalVestingAmount 341 | require( 342 | recipient.totalClaimed <= recipient.totalVestingAmount, 343 | "_claimFor: cannot claim more than you deserve" 344 | ); 345 | 346 | // increase global totalClaimed 347 | totalClaimed = totalClaimed.add(_amount); 348 | 349 | // totalClaimed should NOT be greater than total totalAllocatedSupply 350 | require( 351 | totalClaimed <= totalAllocatedSupply, 352 | "_claimFor: cannot claim more than allocated to escrow" 353 | ); 354 | 355 | // transfer the amount to the _recipient 356 | IERC20(token).safeTransfer(_recipient, _amount); 357 | } 358 | 359 | /// @notice Get total vested tokens for multiple recipients. 360 | /// @dev Reverts if any of the recipients is terminated. 361 | /// @param _recipients An array of non-terminated recipient addresses. 362 | /// @return totalAmount total vested tokens for all _recipients passed. 363 | function batchTotalVestedOf(address[] memory _recipients) 364 | public 365 | view 366 | returns (uint256 totalAmount) 367 | { 368 | for (uint256 i = 0; i < _recipients.length; i++) { 369 | totalAmount = totalAmount.add(totalVestedOf(_recipients[i])); 370 | } 371 | } 372 | 373 | /// @notice Get total vested tokens of a specific recipient. 374 | /// @dev Reverts if the recipient is terminated. 375 | /// @param recipient A non-terminated recipient address. 376 | /// @return Total vested tokens for the recipient address. 377 | function totalVestedOf(address recipient) 378 | public 379 | view 380 | recipientIsNotTerminated(recipient) 381 | returns (uint256) 382 | { 383 | // get recipient 384 | Recipient memory _recipient = recipients[recipient]; 385 | 386 | // totalVested = totalClaimed + claimableAmountFor 387 | return _recipient.totalClaimed.add(claimableAmountFor(recipient)); 388 | } 389 | 390 | /// @notice Check if a recipient address can successfully invoke claim. 391 | /// @dev Reverts if the recipient is a zero address. 392 | /// @param recipient A zero address recipient address. 393 | /// @return bool representing if the recipient can successfully invoke claim. 394 | function canClaim(address recipient) 395 | public 396 | view 397 | isNonZeroAddress(recipient) 398 | returns (bool) 399 | { 400 | Recipient memory _recipient = recipients[recipient]; 401 | 402 | // terminated recipients cannot claim 403 | if (_recipient.recipientVestingStatus == Status.Terminated) { 404 | return false; 405 | } 406 | 407 | // In case of a paused recipient 408 | if (_recipient.recipientVestingStatus == Status.Paused) { 409 | return 410 | _recipient.lastPausedAt >= 411 | _recipient.startTime.add(_recipient.cliffDuration); 412 | } 413 | 414 | // In case of a unpaused recipient, recipient can claim if the cliff duration (inclusive) has passed. 415 | return 416 | block.timestamp >= 417 | _recipient.startTime.add(_recipient.cliffDuration); 418 | } 419 | 420 | /// @notice Check the time after (inclusive) which recipient can successfully invoke claim. 421 | /// @dev Reverts if the recipient is a zero address. 422 | /// @param recipient A zero address recipient address. 423 | /// @return Returns the time after (inclusive) which recipient can successfully invoke claim. 424 | function claimStartTimeFor(address recipient) 425 | public 426 | view 427 | escrowNotTerminated 428 | recipientIsUnpaused(recipient) 429 | returns (uint256) 430 | { 431 | return 432 | recipients[recipient].startTime.add( 433 | recipients[recipient].cliffDuration 434 | ); 435 | } 436 | 437 | /// @notice Get amount of tokens that can be claimed by a recipient at the current timestamp. 438 | /// @dev Reverts if the recipient is terminated. 439 | /// @param recipient A non-terminated recipient address. 440 | /// @return Amount of tokens that can be claimed by a recipient at the current timestamp. 441 | function claimableAmountFor(address recipient) 442 | public 443 | view 444 | recipientIsNotTerminated(recipient) 445 | returns (uint256) 446 | { 447 | // get recipient 448 | Recipient memory _recipient = recipients[recipient]; 449 | 450 | // claimable = totalVestingAmount - (totalClaimed + locked) 451 | return 452 | _recipient.totalVestingAmount.sub( 453 | _recipient.totalClaimed.add(totalLockedOf(recipient)) 454 | ); 455 | } 456 | 457 | /// @notice Get total locked (non-vested) tokens for multiple non-terminated recipient addresses. 458 | /// @dev Reverts if any of the recipients is terminated. 459 | /// @param _recipients An array of non-terminated recipient addresses. 460 | /// @return totalAmount Total locked (non-vested) tokens for multiple non-terminated recipient addresses. 461 | function batchTotalLockedOf(address[] memory _recipients) 462 | public 463 | view 464 | returns (uint256 totalAmount) 465 | { 466 | for (uint256 i = 0; i < _recipients.length; i++) { 467 | totalAmount = totalAmount.add(totalLockedOf(_recipients[i])); 468 | } 469 | } 470 | 471 | /// @notice Get total locked tokens of a specific recipient. 472 | /// @dev Reverts if any of the recipients is terminated. 473 | /// @param recipient A non-terminated recipient address. 474 | /// @return Total locked tokens of a specific recipient. 475 | function totalLockedOf(address recipient) 476 | public 477 | view 478 | recipientIsNotTerminated(recipient) 479 | returns (uint256) 480 | { 481 | // get recipient 482 | Recipient memory _recipient = recipients[recipient]; 483 | 484 | // We know that vestingPerSec is constant for a recipient for entirety of their vesting period 485 | // locked = vestingPerSec*(endTime-max(lastPausedAt, startTime+cliffDuration)) 486 | if (_recipient.recipientVestingStatus == Status.Paused) { 487 | if (_recipient.lastPausedAt >= _recipient.endTime) { 488 | return 0; 489 | } 490 | return 491 | _recipient.vestingPerSec.mul( 492 | _recipient.endTime.sub( 493 | Math.max( 494 | _recipient.lastPausedAt, 495 | _recipient.startTime.add(_recipient.cliffDuration) 496 | ) 497 | ) 498 | ); 499 | } 500 | 501 | // Nothing is locked if the recipient passed the endTime 502 | if (block.timestamp >= _recipient.endTime) { 503 | return 0; 504 | } 505 | 506 | // in case escrow is terminated, locked amount stays the constant 507 | if (ESCROW_TERMINATED) { 508 | return 509 | _recipient.vestingPerSec.mul( 510 | _recipient.endTime.sub( 511 | Math.max( 512 | ESCROW_TERMINATED_AT, 513 | _recipient.startTime.add(_recipient.cliffDuration) 514 | ) 515 | ) 516 | ); 517 | } 518 | 519 | // We know that vestingPerSec is constant for a recipient for entirety of their vesting period 520 | // locked = vestingPerSec*(endTime-max(block.timestamp, startTime+cliffDuration)) 521 | if (_recipient.recipientVestingStatus == Status.UnPaused) { 522 | return 523 | _recipient.vestingPerSec.mul( 524 | _recipient.endTime.sub( 525 | Math.max( 526 | block.timestamp, 527 | _recipient.startTime.add(_recipient.cliffDuration) 528 | ) 529 | ) 530 | ); 531 | } 532 | } 533 | 534 | /// @notice Allows owner to transfer the ERC20 assets (other than token) to the "to" address in case of any emergency 535 | /// @dev It is assumed that the "to" address is NOT malicious 536 | /// Only owner of the vesting escrow can invoke this function. 537 | /// Reverts if the asset address is a zero address or the token address. 538 | /// Reverts if the to address is a zero address. 539 | /// @param asset Address of the ERC20 asset to be rescued 540 | /// @param to Address to which all ERC20 asset amount will be transferred 541 | /// @return rescued Total amount of asset transferred to the SAFE_ADDRESS. 542 | function inCaseAssetGetStuck(address asset, address to) 543 | external 544 | onlyOwner 545 | returns (uint256 rescued) 546 | { 547 | // asset address should NOT be a 0 address 548 | require( 549 | asset != address(0), 550 | "inCaseAssetGetStuck: asset cannot be 0 address" 551 | ); 552 | // asset address should NOT be the token address 553 | require(asset != token, "inCaseAssetGetStuck: cannot withdraw token"); 554 | // to address should NOT a 0 address 555 | require( 556 | to != address(0), 557 | "inCaseAssetGetStuck: to cannot be 0 address" 558 | ); 559 | // transfer all the balance of the asset this contract hold to the "to" address 560 | rescued = IERC20(asset).balanceOf(address(this)); 561 | IERC20(asset).safeTransfer(to, rescued); 562 | } 563 | 564 | /// @notice Transfers the dust to the SAFE_ADDRESS. 565 | /// @dev It is assumed that the SAFE_ADDRESS is NOT malicious. 566 | /// Only owner of the vesting escrow can invoke this function. 567 | /// @return Amount of dust to the SAFE_ADDRESS. 568 | function transferDust() external onlyOwner returns (uint256) { 569 | // precaution for reentrancy attack 570 | if (dust > 0) { 571 | uint256 _dust = dust; 572 | dust = 0; 573 | IERC20(token).safeTransfer(SAFE_ADDRESS, _dust); 574 | return _dust; 575 | } 576 | return 0; 577 | } 578 | 579 | /// @notice Transfers the locked (non-vested) tokens of the passed recipients to the SAFE_ADDRESS 580 | /// @dev It is assumed that the SAFE_ADDRESS is NOT malicious 581 | /// Only owner of the vesting escrow can invoke this function. 582 | /// Reverts if any of the recipients is terminated. 583 | /// Can only be invoked if the escrow is terminated. 584 | /// @param _recipients An array of non-terminated recipient addresses. 585 | /// @return totalSeized Total tokens seized from the recipients. 586 | function seizeLockedTokens(address[] calldata _recipients) 587 | external 588 | onlyOwner 589 | returns (uint256 totalSeized) 590 | { 591 | // only seize if escrow is terminated 592 | require(ESCROW_TERMINATED, "seizeLockedTokens: escrow not terminated"); 593 | // get the total tokens to be seized 594 | for (uint256 i = 0; i < _recipients.length; i++) { 595 | // only seize tokens from the recipients which have not been seized before 596 | if (!lockedTokensSeizedFor[_recipients[i]]) { 597 | totalSeized = totalSeized.add(totalLockedOf(_recipients[i])); 598 | lockedTokensSeizedFor[_recipients[i]] = true; 599 | } 600 | } 601 | // transfer the totalSeized amount to the SAFE_ADDRESS 602 | IERC20(token).safeTransfer(SAFE_ADDRESS, totalSeized); 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /contracts/test/SafeERC20.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 4 | 5 | // Test Token with Governance. 6 | contract TEST is ERC20("Test Token", "TEST") { 7 | function mint(address _to, uint256 _amount) public { 8 | _mint(_to, _amount); 9 | } 10 | } 11 | --------------------------------------------------------------------------------