Use Ownable in VestingWallet instead of an immutable beneficiary (#4508)

Co-authored-by: Francisco Giordano <fg@frang.io>
This commit is contained in:
Ernesto García
2023-08-04 15:57:53 -06:00
committed by GitHub
parent f715365ec4
commit b81bec4552
4 changed files with 26 additions and 22 deletions

View File

@ -0,0 +1,5 @@
---
'openzeppelin-solidity': major
---
`VestingWallet`: Use `Ownable` instead of an immutable `beneficiary`.

View File

@ -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);
} }
/** /**

View File

@ -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));

View File

@ -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 () {