Use Ownable in VestingWallet instead of an immutable beneficiary (#4508)
Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
5
.changeset/healthy-gorillas-applaud.md
Normal file
5
.changeset/healthy-gorillas-applaud.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'openzeppelin-solidity': major
|
||||||
|
---
|
||||||
|
|
||||||
|
`VestingWallet`: Use `Ownable` instead of an immutable `beneficiary`.
|
||||||
@ -6,24 +6,28 @@ import {IERC20} from "../token/ERC20/IERC20.sol";
|
|||||||
import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";
|
import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";
|
||||||
import {Address} from "../utils/Address.sol";
|
import {Address} from "../utils/Address.sol";
|
||||||
import {Context} from "../utils/Context.sol";
|
import {Context} from "../utils/Context.sol";
|
||||||
|
import {Ownable} from "../access/Ownable.sol";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @title VestingWallet
|
* @dev A vesting wallet is an ownable contract that can receive native currency and ERC20 tokens, and release these
|
||||||
* @dev This contract handles the vesting of Eth and ERC20 tokens for a given beneficiary. Custody of multiple tokens
|
* assets to the wallet owner, also referred to as "beneficiary", according to a vesting schedule.
|
||||||
* can be given to this contract, which will release the token to the beneficiary following a given vesting schedule.
|
|
||||||
* The vesting schedule is customizable through the {vestedAmount} function.
|
|
||||||
*
|
*
|
||||||
* Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning.
|
* 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)
|
* Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly)
|
||||||
* be immediately releasable.
|
* be immediately releasable.
|
||||||
*
|
*
|
||||||
* By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for
|
* 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.
|
* 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
|
* 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.
|
* to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended.
|
||||||
*/
|
*/
|
||||||
contract VestingWallet is Context {
|
contract VestingWallet is Context, Ownable {
|
||||||
event EtherReleased(uint256 amount);
|
event EtherReleased(uint256 amount);
|
||||||
event ERC20Released(address indexed token, uint256 amount);
|
event ERC20Released(address indexed token, uint256 amount);
|
||||||
|
|
||||||
@ -34,18 +38,18 @@ contract VestingWallet is Context {
|
|||||||
|
|
||||||
uint256 private _released;
|
uint256 private _released;
|
||||||
mapping(address => uint256) private _erc20Released;
|
mapping(address => uint256) private _erc20Released;
|
||||||
address private immutable _beneficiary;
|
|
||||||
uint64 private immutable _start;
|
uint64 private immutable _start;
|
||||||
uint64 private immutable _duration;
|
uint64 private immutable _duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet.
|
* @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 beneficiaryAddress, uint64 startTimestamp, uint64 durationSeconds) payable {
|
constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable Ownable(beneficiary) {
|
||||||
if (beneficiaryAddress == address(0)) {
|
if (beneficiary == address(0)) {
|
||||||
revert VestingWalletInvalidBeneficiary(address(0));
|
revert VestingWalletInvalidBeneficiary(address(0));
|
||||||
}
|
}
|
||||||
_beneficiary = beneficiaryAddress;
|
|
||||||
_start = startTimestamp;
|
_start = startTimestamp;
|
||||||
_duration = durationSeconds;
|
_duration = durationSeconds;
|
||||||
}
|
}
|
||||||
@ -55,13 +59,6 @@ contract VestingWallet is Context {
|
|||||||
*/
|
*/
|
||||||
receive() external payable virtual {}
|
receive() external payable virtual {}
|
||||||
|
|
||||||
/**
|
|
||||||
* @dev Getter for the beneficiary address.
|
|
||||||
*/
|
|
||||||
function beneficiary() public view virtual returns (address) {
|
|
||||||
return _beneficiary;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @dev Getter for the start timestamp.
|
* @dev Getter for the start timestamp.
|
||||||
*/
|
*/
|
||||||
@ -121,7 +118,7 @@ contract VestingWallet is Context {
|
|||||||
uint256 amount = releasable();
|
uint256 amount = releasable();
|
||||||
_released += amount;
|
_released += amount;
|
||||||
emit EtherReleased(amount);
|
emit EtherReleased(amount);
|
||||||
Address.sendValue(payable(beneficiary()), amount);
|
Address.sendValue(payable(owner()), amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,7 +130,7 @@ contract VestingWallet is Context {
|
|||||||
uint256 amount = releasable(token);
|
uint256 amount = releasable(token);
|
||||||
_erc20Released[token] += amount;
|
_erc20Released[token] += amount;
|
||||||
emit ERC20Released(token, amount);
|
emit ERC20Released(token, amount);
|
||||||
SafeERC20.safeTransfer(IERC20(token), beneficiary(), amount);
|
SafeERC20.safeTransfer(IERC20(token), owner(), amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -29,7 +29,7 @@ contract('VestingWallet', function (accounts) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('check vesting contract', async function () {
|
it('check vesting contract', async function () {
|
||||||
expect(await this.mock.beneficiary()).to.be.equal(beneficiary);
|
expect(await this.mock.owner()).to.be.equal(beneficiary);
|
||||||
expect(await this.mock.start()).to.be.bignumber.equal(this.start);
|
expect(await this.mock.start()).to.be.bignumber.equal(this.start);
|
||||||
expect(await this.mock.duration()).to.be.bignumber.equal(duration);
|
expect(await this.mock.duration()).to.be.bignumber.equal(duration);
|
||||||
expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration));
|
expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration));
|
||||||
|
|||||||
@ -59,7 +59,9 @@ contract('Create2', function (accounts) {
|
|||||||
addr: offChainComputed,
|
addr: offChainComputed,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await VestingWallet.at(offChainComputed).then(instance => instance.beneficiary())).to.be.equal(other);
|
const instance = await VestingWallet.at(offChainComputed);
|
||||||
|
|
||||||
|
expect(await instance.owner()).to.be.equal(other);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deploys a contract with funds deposited in the factory', async function () {
|
it('deploys a contract with funds deposited in the factory', async function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user