Skip to content

Commit

Permalink
feat: added VestingWallet solidity example contract (#554) (#570)
Browse files Browse the repository at this point in the history
Signed-off-by: Logan Nguyen <[email protected]>
  • Loading branch information
quiet-node authored Nov 7, 2023
1 parent fca678a commit 7864827
Show file tree
Hide file tree
Showing 2 changed files with 294 additions and 0 deletions.
157 changes: 157 additions & 0 deletions contracts/OZ/finance/VestingWallet.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.20;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

/**
* @dev A vesting wallet is an ownable contract that can receive native currency and ERC20 tokens, and release these
* assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule.
*
* Any assets transferred to this contract will follow the vesting schedule as if they were locked from the beginning.
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly)
* be immediately releasable.
*
* By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for
* a beneficiary until a specified time.
*
* NOTE: Since the wallet is {Ownable}, and ownership can be transferred, it is possible to sell unvested tokens.
* Preventing this in a smart contract is difficult, considering that: 1) a beneficiary address could be a
* counterfactually deployed contract, 2) there is likely to be a migration path for EOAs to become contracts in the
* near future.
*
* NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make
* sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended.
*/
contract VestingWallet is Context, Ownable {
event HbarReleased(address receiver, uint256 amount);
event ERC20Released(address receiver, address indexed token, uint256 amount);

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

/**
* @dev Sets the sender as the initial owner, the beneficiary as the pending owner, the start timestamp and the
* vesting duration of the vesting wallet.
*/
constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable Ownable(beneficiary) {
_start = startTimestamp;
_duration = durationSeconds;
}

/**
* @dev The contract should be able to receive native token.
*/
receive() external payable virtual {}

/**
* @dev Getter for the start timestamp.
*/
function start() public view virtual returns (uint256) {
return _start;
}

/**
* @dev Getter for the vesting duration.
*/
function duration() public view virtual returns (uint256) {
return _duration;
}

/**
* @dev Getter for the end timestamp.
*/
function end() public view virtual returns (uint256) {
return start() + duration();
}

/**
* @dev Amount of hbar already released
*/
function released() public view virtual returns (uint256) {
return _released;
}

/**
* @dev Amount of token already released
*/
function released(address token) public view virtual returns (uint256) {
return _erc20Released[token];
}

/**
* @dev Getter for the amount of releasable hbar.
*/
function releasable() public view virtual returns (uint256) {
return vestedAmount(uint64(block.timestamp)) - released();
}

/**
* @dev Getter for the amount of releasable `token` tokens. `token` should be the address of an
* IERC20 contract.
*/
function releasable(address token) public view virtual returns (uint256) {
return vestedAmount(token, uint64(block.timestamp)) - released(token);
}

/**
* @dev Release the native token (hbar) that have already vested.
*
* Emits a {HbarReleased} event.
*/
function release() public virtual {
uint256 amount = releasable();
_released += amount;
emit HbarReleased(owner(), amount);
Address.sendValue(payable(owner()), amount);
}

/**
* @dev Release the tokens that have already vested.
*
* Emits a {ERC20Released} event.
*/
function release(address token) public virtual {
uint256 amount = releasable(token);
_erc20Released[token] += amount;
emit ERC20Released(owner(), token, amount);
SafeERC20.safeTransfer(IERC20(token), owner(), amount);
}

/**
* @dev Calculates the amount of hbar that has already vested. Default implementation is a linear vesting curve.
*/
function vestedAmount(uint64 timestamp) public view virtual returns (uint256) {
return _vestingSchedule(address(this).balance + released(), timestamp);
}

/**
* @dev Calculates the amount of tokens that has already vested. Default implementation is a linear vesting curve.
*/
function vestedAmount(address token, uint64 timestamp) public view virtual returns (uint256) {
return _vestingSchedule(IERC20(token).balanceOf(address(this)) + released(token), timestamp);
}

/**
* @dev Virtual implementation of the vesting formula. This returns the amount vested, as a function of time, for
* an asset given its total historical allocation.
*/
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();
}
}

function getCurrentTimestamp() external view returns (uint256 timestamp) {
timestamp = block.timestamp;
}
}
137 changes: 137 additions & 0 deletions test/OZ/finanace/VestingWallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*-
*
* Hedera JSON RPC Relay - Hardhat Example
*
* Copyright (C) 2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

const { expect } = require('chai')
const { ethers } = require('hardhat')
const constants = require('../../constants')
const utils = require('../../hts-precompile/utils')

describe('@solidityevmequiv5 Vesting Wallet tests', () => {
let vestingWallet, erc20Mock, signers, beneficiaryAddress
const DURATION = 3 // seconds
const GAS_LIMIT = 1_000_000
const INITIAL_FUND = 30_000_000_000
const TINY_BAR_TO_WEI_COEF = 10_000_000_000
const START = Math.round(Date.now() / 1000)
const INITIAL_ERC20TOKEN_AMOUNT = 3_000_000

beforeEach(async () => {
signers = await ethers.getSigners()
beneficiaryAddress = await signers[1].getAddress()

const vestingWalletFactory = await ethers.getContractFactory(
'VestingWallet'
)
vestingWallet = await vestingWalletFactory.deploy(
beneficiaryAddress,
START,
DURATION,
{ value: INITIAL_FUND, gasLimit: GAS_LIMIT }
)

const erc20MockFactory = await ethers.getContractFactory(
constants.Path.ERC20Mock
)

erc20Mock = await erc20MockFactory.deploy('Hedera', 'HBAR')

await erc20Mock.mint(vestingWallet.address, INITIAL_ERC20TOKEN_AMOUNT)
})

it('Deployment', async () => {
const vestingWalletEnd = await vestingWallet.end()
const vestingWalletStart = await vestingWallet.start()
const vestingWalletBeneficiary = await vestingWallet.owner()
const vestingWalletDuration = await vestingWallet.duration()
const vestingWalletBalance = await ethers.provider.getBalance(
vestingWallet.address
)
const vestingWalletErc20Balance = await erc20Mock.balanceOf(
vestingWallet.address
)

expect(vestingWalletStart).to.eq(START)
expect(vestingWalletDuration).to.eq(DURATION)
expect(vestingWalletBalance).to.eq(INITIAL_FUND)
expect(vestingWalletEnd).to.eq(START + DURATION)
expect(vestingWalletBeneficiary).to.eq(beneficiaryAddress)
expect(vestingWalletErc20Balance).to.eq(INITIAL_ERC20TOKEN_AMOUNT)
})

it('Should get the amount of releasable hbar', async () => {
const releasableHbar = await vestingWallet['releasable()']()
expect(releasableHbar).to.eq(
Math.round(INITIAL_FUND / TINY_BAR_TO_WEI_COEF)
)
})

it('Should get the amount of releasable erc20 tokens', async () => {
const releasableTokens = await vestingWallet['releasable(address)'](
erc20Mock.address
)
expect(releasableTokens).to.eq(INITIAL_ERC20TOKEN_AMOUNT)
})

it('Should release the native token that have already vested', async () => {
const tx = await vestingWallet['release()']()

const receipt = await tx.wait()

const [receiverAddress, releasedAmount] = receipt.events.map(
(e) => e.event === 'HbarReleased' && e
)[0].args

expect(receiverAddress).to.eq(beneficiaryAddress)
expect(releasedAmount).to.eq(
Math.round(INITIAL_FUND / TINY_BAR_TO_WEI_COEF)
)
})

it('Should release the erc20 tokens that have already vested', async () => {
const tx = await vestingWallet['release(address)'](erc20Mock.address)

const receipt = await tx.wait()

const [receiverAddress, releasedTokenAddress, releasedTokenAmount] =
receipt.events.map((e) => e.event === 'ERC20Released' && e)[0].args

expect(receiverAddress).to.eq(beneficiaryAddress)
expect(releasedTokenAddress).to.eq(erc20Mock.address)
expect(releasedTokenAmount).to.eq(INITIAL_ERC20TOKEN_AMOUNT)
})

it('Should get the amount of hbar already released', async () => {
await vestingWallet['release()']()

const hbarReleased = await vestingWallet['released()']()

expect(hbarReleased).to.eq(Math.round(INITIAL_FUND / TINY_BAR_TO_WEI_COEF))
})

it('Should get the amount of erc20 token already released', async () => {
await vestingWallet['release(address)'](erc20Mock.address)

const tokenReleased = await vestingWallet['released(address)'](
erc20Mock.address
)

expect(tokenReleased).to.eq(INITIAL_ERC20TOKEN_AMOUNT)
})
})

0 comments on commit 7864827

Please sign in to comment.