-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Signed-off-by: Logan Nguyen <[email protected]>
- Loading branch information
1 parent
fca678a
commit 7864827
Showing
2 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
}) |