VestingWallet from OpenZeppelin

The VestingWallet is a smart contract in the OpenZeppelin library which can be quite useful for companies that wish to pay bonuses to their employees that are tied to their employees continuing with the company. Alternatively, the VestingWallet can also be used to setup a trust fund for someone when they reach a certain age. The smart contract is quite simple, and is based on the Ownable smart contract which I wrote about earlier. By redefining (overriding) a few functions, the VestingWallet can be made to behave differently as per requirement.

I will demonstrate the use of the VestingWallet by creating a BenefitsWallet that inherits it. The code can be found here:

https://github.com/shivkiyer/smart-contracts-examples/tree/master/contracts/access/VestingWallet

contract VestingWallet is Context, Ownable {
    event EtherReleased(uint256 amount);
    event ERC20Released(address indexed token, uint256 amount);

    uint256 private _released;
    mapping(address token => uint256) private _erc20Released;
    uint64 private immutable _start;
    uint64 private immutable _duration;

    constructor(
        address beneficiary,
        uint64 startTimestamp,
        uint64 durationSeconds
    ) payable Ownable(beneficiary) {
        _start = startTimestamp;
        _duration = durationSeconds;
    }
}        

The VestingWallet has a start time, a duration and a beneficiary, and is usually created by the benefactor who can be either a company or an individual such as a guardian. The VestingWallet can receive Ether as well as ERC20 tokens. When Ether or ERC20 tokens are released to the beneficiary, events are emitted.

receive() external payable virtual {}

function start() public view virtual returns (uint256) {
    return _start;
}

function duration() public view virtual returns (uint256) {
    return _duration;
}

function end() public view virtual returns (uint256) {
    return start() + duration();
}        

The contract has the usual methods such as the receive method for the contract to receive either Ether or ERC20 tokens from the benefactor, and a few methods to view the properties such as the start of the contract, the duration of the contract and the end of the contract.

function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) {
    if (timestamp < start()) {
        return 0;
    } else if (timestamp >= end()) {
        return totalAllocation;
    } else {
        return (totalAllocation * (timestamp - start())) / duration();
    }
}        

The primary function in the contract that performs the calculation is the _vestingSchedule which is an internal function called by a few other functions within the smart contract. This function will check if the current block timestamp is greater than the start time, as before that no funds can be released. If the block time stamp is greater than the end time, then the contract has ended and all funds can be released to the beneficiary. If the block time stamp is between the start and end, then the function uses a linear relation to calculate the funds that can be released.

function vestedAmount(uint64 timestamp) public view virtual returns (uint256) {
    return _vestingSchedule(address(this).balance + released(), timestamp);
}

function vestedAmount(address token, uint64 timestamp) public view virtual returns (uint256) {
    return _vestingSchedule(IERC20(token).balanceOf(address(this)) + released(token), timestamp);
}        

The _vestingSchedule function can be called from several other functions, one of which is the vestedAmount function. This function is present in two different forms - in the default form with no arguments, it returns the total Ether that has been deposited, and if the address of the ERC20 token is passed as an argument, then it returns the number of ERC20 tokens that have been deposited. This function is only for determining the funds that are available and do not actually release any funds.

function releasable() public view virtual returns (uint256) {
    return vestedAmount(uint64(block.timestamp)) - released();
}

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

Another function releasable is similar to the vestedAmount but this instead will return the actual funds that can be released to the beneficiary deducting any prior releases.

function release() public virtual {
    uint256 amount = releasable();
    _released += amount;
    emit EtherReleased(amount);
    Address.sendValue(payable(owner()), amount);
}

function release(address token) public virtual {
    uint256 amount = releasable(token);
    _erc20Released[token] += amount;
    emit ERC20Released(token, amount);
    SafeERC20.safeTransfer(IERC20(token), owner(), amount);
}        

The function that actually release Ether or ERC20 tokens is the function release which is also available in two forms - for just Ether and also for ERC20 tokens. This function calls the releasable function to determine the funds that can be released to the beneficiary. It then increments the internal private variables for keeping track of the funds already released, and then transfers the funds to the beneficiary account.

As can be seen from the above description, quite a simple contract, but quite useful for any company of individual who wishes to set up a wallet for a beneficiary according to a payment schedule. To make this more appropriate for a company to setup a loyalty bonus for an employee, I extended this contract to create a BenefitsWallet contract. The only change I made is to also create an interval besides the start time and the duration. This interval can be used to release funds only if the employee completes that said interval in the company. For example, if the interval is set to a 1 year, the employee can withdraw funds only at the end of every year spent with the company.

contract BenefitsWallet is VestingWallet {
    address private _controller;
    uint64 private _intervalSeconds;
    uint64 private _intervalCounter;

    constructor(
        address beneficiary,
        uint64 startTimestamp,
        uint64 interval,
        uint64 durationSeconds
    ) VestingWallet(beneficiary, startTimestamp, durationSeconds) {
        _controller = _msgSender();
        _intervalSeconds = interval;
        _intervalCounter = 1;
    }
}        

The BenefitsWallet has the same start time, duration and beneficiary parameters as the VestingWallet, but now has also an intervalSeconds parameter and an internal counter for the release interval. The BenefitsWallet also keeps track of the creator of the contract as the controller, as the controller can close the contract when the employee leaves the company.

function controller() public view returns (address) {
    return _controller;
}

function intervalCounter() public view returns (uint64) {
    return _intervalCounter;
}

function intervalSeconds() public view returns (uint64) {
    return _intervalSeconds;
}        

There are the usual public methods to return the internal variables.

    function _vestingSchedule(
        uint256 totalAllocation,
        uint64 timestamp
    ) internal view virtual override returns (uint256) {
        if (timestamp > (start() + _intervalCounter * _intervalSeconds)) {
            return super._vestingSchedule(totalAllocation, timestamp);
        }
        return 0;
    }        

The _vestingSchedule method has now been overridden to include a logic for whether the time stamp exceeds the previous anniversary by the interval time. Only if this check passes, will the vestingSchedule of the parent contract VestingWallet be called. Otherwise, the funds available are 0.

function release() public virtual override onlyOwner {
    _intervalCounter += 1;
    super.release();
}        

The release function has also been overridden to ensure that only the owner (beneficiary) can call it using the onlyOwner modifier that is present in the Ownable contract. Before releasing funds, the interval counter is incremented.

modifier onlyController() {
    if (_msgSender() != _controller) {
        revert OwnableUnauthorizedAccount(_msgSender());
    }
    _;
}

function close() public onlyController {
    Address.sendValue(payable(_controller), address(this).balance);
}        

For the creator of the contract (controller) to be able to close the contract when the employee leaves the company, an onlyController modifier is created and a close function will transfer the balance to the controller's account.

要查看或添加评论,请登录

Shivkumar Iyer的更多文章

社区洞察

其他会员也浏览了