Skip to content

Commit

Permalink
Add support of multiple beneficiaries
Browse files Browse the repository at this point in the history
  • Loading branch information
Artjom Galaktionov committed Oct 20, 2023
1 parent 7473b8b commit f36880d
Showing 1 changed file with 145 additions and 54 deletions.
199 changes: 145 additions & 54 deletions contracts/finance/Vesting.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,74 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {EnumerableSet} from "../libs/arrays/SetHelper.sol";

contract VestingWallet is Ownable {
event EtherReleased(uint256 amount);
event ERC20Released(address indexed token, uint256 amount);
contract VestingWallet {
using EnumerableSet for EnumerableSet.AddressSet;

event EtherRevoked(uint256 amount);
event ERC20Revoked(address indexed token, uint256 amount);
event BeneficiaryAdded(address account, uint256 shares);

uint256 private _released;
mapping(address token => uint256) private _erc20Released;
event EtherReleased(address indexed beneficiary, uint256 amount);
event ERC20Released(address indexed beneficiary, address indexed token, uint256 amount);

bool private _revoked;
mapping(address token => bool) private _erc20Revoked;
event EtherRevoked(address indexed beneficiary, uint256 amount);
event ERC20Revoked(address indexed beneficiary, address indexed token, uint256 amount);

EnumerableSet.AddressSet private _beneficiaries;

uint64 private immutable _cliff;
uint64 private immutable _start;
uint64 private immutable _duration;

bool private immutable _revocable;

uint256 private _totalShares;
uint256 private _ethTotalReleased;

mapping(address beneficiary => uint256) private _shares;

mapping(address beneficiary => uint256) private _ethReleased;
mapping(address token => uint256) private _erc20TotalReleased;
mapping(address beneficiary => mapping(address token => uint256)) private _erc20Released;

mapping(address beneficiary => bool) private _ethRevoked;
mapping(address beneficiary => mapping(address token => bool)) private _erc20Revoked;

constructor(
address beneficiary_,
address[] memory beneficiaries_,
uint256[] memory shares_,
uint64 startTimestamp_,
uint64 cliffSeconds_,
uint64 durationSeconds_,
bool revocable_
) payable {
require(
beneficiaries_.length == shares_.length,
"Vesting: beneficiaries and shares length mismatch"
);
require(beneficiaries_.length > 0, "Vesting: no beneficiaries");
require(cliffSeconds_ <= durationSeconds_, "Vesting: cliff is longer than duration");
require(
startTimestamp_ + durationSeconds_ > block.timestamp,
"Vesting: final time is before current time"
);

for (uint256 i = 0; i < beneficiaries_.length; i++) {
_addBeneficiary(beneficiaries_[i], shares_[i]);
}

_start = startTimestamp_;
_duration = durationSeconds_;
_cliff = startTimestamp_ + cliffSeconds_;
_revocable = revocable_;

transferOwnership(beneficiary_);
}

receive() external payable virtual {}

function isBeneficiary(address account_) public view virtual returns (bool) {
return _beneficiaries.contains(account_);
}

function start() public view virtual returns (uint256) {
return _start;
}
Expand All @@ -67,115 +93,180 @@ contract VestingWallet is Ownable {
return _revocable;
}

function released() public view virtual returns (uint256) {
return _released;
function released(address account_) public view virtual returns (uint256) {
return _ethReleased[account_];
}

function released(address account_, address token_) public view virtual returns (uint256) {
return _erc20Released[account_][token_];
}

function totalReleased() public view virtual returns (uint256) {
return _ethTotalReleased;
}

function totalReleased(address token_) public view virtual returns (uint256) {
return _erc20TotalReleased[token_];
}

function released(address token_) public view virtual returns (uint256) {
return _erc20Released[token_];
function shares(address account_) public view virtual returns (uint256) {
return _shares[account_];
}

function revoked() public view virtual returns (bool) {
return _revoked;
function totalShares() public view virtual returns (uint256) {
return _totalShares;
}

function revoked(address token_) public view virtual returns (bool) {
return _erc20Revoked[token_];
function revoked(address account_) public view virtual returns (bool) {
return _ethRevoked[account_];
}

function revoked(address account_, address token_) public view virtual returns (bool) {
return _erc20Revoked[account_][token_];
}

function totalAllocation() public view virtual returns (uint256) {
return address(this).balance + released();
return address(this).balance + totalReleased();
}

function totalAllocation(address token_) public view virtual returns (uint256) {
return IERC20(token_).balanceOf(address(this)) + released(token_);
return IERC20(token_).balanceOf(address(this)) + totalReleased(token_);
}

// get the emount of eth that can be released at the current time
function releasable() public view virtual returns (uint256) {
return vestedAmount(uint64(block.timestamp)) - released();
function beneficiaryAllocation(address account_) public view virtual returns (uint256) {
return (totalAllocation() * shares(account_)) / totalShares();
}

// get the emount of erc20 that can be released at the current time
function releasable(address token_) public view virtual returns (uint256) {
return vestedAmount(token_, uint64(block.timestamp)) - released(token_);
function beneficiaryAllocation(
address account_,
address token_
) public view virtual returns (uint256) {
return (totalAllocation(token_) * shares(account_)) / totalShares();
}

function release() public virtual {
uint256 amount = releasable();
function releasable(address account_) public view virtual returns (uint256) {
return vestedAmount(account_, uint64(block.timestamp)) - released(account_);
}

_released += amount;
function releasable(address account_, address token_) public view virtual returns (uint256) {
return
vestedAmount(account_, token_, uint64(block.timestamp)) - released(account_, token_);
}

Address.sendValue(payable(owner()), amount);
function release(address account_) public virtual {
require(_beneficiaries.contains(account_), "Vesting: not a beneficiary");
require(_shares[account_] > 0, "PaymentSplitter: account has no shares");

emit EtherReleased(amount);
uint256 _amount = releasable(account_);

_ethTotalReleased += _amount;
_ethReleased[account_] += _amount;

Address.sendValue(payable(account_), _amount);

emit EtherReleased(account_, _amount);
}

function release(address token_) public virtual {
function release(address account_, address token_) public virtual {
require(_beneficiaries.contains(account_), "Vesting: not a beneficiary");

uint256 _amount = releasable(token_);

_erc20Released[token_] += _amount;
_erc20TotalReleased[token_] += _amount;
_erc20Released[account_][token_] += _amount;

SafeERC20.safeTransfer(IERC20(token_), owner(), _amount);
SafeERC20.safeTransfer(IERC20(token_), account_, _amount);

emit ERC20Released(token_, _amount);
emit ERC20Released(account_, token_, _amount);
}

function revoke() public virtual {
function revoke(address account_) public virtual {
require(_beneficiaries.contains(account_), "Vesting: not a beneficiary");

require(revocable(), "Vesting: cannot revoke");
require(!_revoked, "Vesting: already revoked");
require(!_ethRevoked[account_], "Vesting: already revoked");

uint256 _amount = address(this).balance - releasable();
uint256 _amount = beneficiaryAllocation(account_) - releasable(account_);

_revoked = true;
_ethRevoked[account_] = true;

Address.sendValue(payable(owner()), _amount);
Address.sendValue(payable(account_), _amount);

emit EtherRevoked(_amount);
emit EtherRevoked(account_, _amount);
}

function revoke(address token_) public virtual {
function revoke(address account_, address token_) public virtual {
require(_beneficiaries.contains(account_), "Vesting: not a beneficiary");

require(revocable(), "Vesting: cannot revoke");
require(!_erc20Revoked[token_], "Vesting: already revoked");
require(!_erc20Revoked[account_][token_], "Vesting: already revoked");

uint256 _amount = IERC20(token_).balanceOf(address(this)) - releasable(token_);
uint256 _amount = beneficiaryAllocation(account_, token_) - releasable(account_, token_);

_erc20Revoked[token_] = true;
_erc20Revoked[account_][token_] = true;

SafeERC20.safeTransfer(IERC20(token_), owner(), _amount);
SafeERC20.safeTransfer(IERC20(token_), account_, _amount);

emit ERC20Revoked(token_, _amount);
emit ERC20Revoked(account_, token_, _amount);
}

function vestedAmount(uint64 timestamp_) public view virtual returns (uint256) {
return _vestingSchedule(totalAllocation(), timestamp_);
function vestedAmount(
address account_,
uint64 timestamp_
) public view virtual returns (uint256) {
return _vestingSchedule(account_, beneficiaryAllocation(account_), timestamp_);
}

function vestedAmount(
address account_,
address token_,
uint64 timestamp_
) public view virtual returns (uint256) {
return _vestingSchedule(totalAllocation(token_), timestamp_);
return
_vestingSchedule(
account_,
token_,
beneficiaryAllocation(account_, token_),
timestamp_
);
}

function _vestingSchedule(
address account_,
uint256 totalAllocation_,
uint64 timestamp_
) internal view virtual returns (uint256) {
return _vestingSchedule(address(0), totalAllocation_, timestamp_);
return _vestingSchedule(account_, address(0), totalAllocation_, timestamp_);
}

function _vestingSchedule(
address account_,
address token_,
uint256 totalAllocation_,
uint64 timestamp_
) internal view virtual returns (uint256) {
if (timestamp_ < cliff()) {
return 0;
} else if (timestamp_ >= end() || revoked() || revoked(token_)) {
} else if (
timestamp_ >= end() || token_ == address(0)
? revoked(account_)
: revoked(account_, token_)
) {
return totalAllocation_;
} else {
return (totalAllocation_ * (timestamp_ - start())) / duration();
}
}

function _addBeneficiary(address account_, uint256 shares_) private {
require(account_ != address(0), "Vesting: account is the zero address");
require(shares_ > 0, "Shares: shares are 0");
require(_shares[account_] == 0, "Shares: account already has shares");

_beneficiaries.add(account_);
_shares[account_] = shares_;
_totalShares += shares_;

emit BeneficiaryAdded(account_, shares_);
}
}

0 comments on commit f36880d

Please sign in to comment.