diff --git a/contracts/vesting/VestingWalletRecoveryLight.sol b/contracts/vesting/VestingWalletRecoveryLight.sol new file mode 100644 index 00000000..7d3fd825 --- /dev/null +++ b/contracts/vesting/VestingWalletRecoveryLight.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: UNLICENSED +// See Forta Network License: https://github.com/forta-network/forta-contracts/blob/master/LICENSE.md + +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/StorageSlot.sol"; + +/** + * This contract is designed for recovering the in case the beneficiary was lost. + */ +contract VestingWalletRecoveryLight { + /// Storage + // Initializable + uint8 private _initialized; + bool private _initializing; + // ContextUpgradeable + uint256[50] private __gap_1; + // OwnableUpgradeable + address private _owner; + uint256[49] private __gap_2; + // UUPSUpgradeable + uint256[50] private __gap_3; + // ERC1967UpgradeUpgradeable + uint256[50] private __gap_4; + // VestingWallerV1 + mapping (address => uint256) private _released; + address private _beneficiary; + uint256 private _start; + uint256 private _cliff; + uint256 private _duration; + + /// Constants and Events + // ERC1967UpgradeUpgradeable + bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + event Upgraded(address indexed implementation); + + function changeOwnerAndUpgrade(address newBeneficiary, address newImplementation) external { + // change ownership + _beneficiary = newBeneficiary; + + // ERC1967Upgrade._setImplementation + require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract"); + StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + emit Upgraded(newImplementation); + } + + function proxiableUUID() external pure returns (bytes32) { + return _IMPLEMENTATION_SLOT; + } + + + function upgradeTo(address) external pure { + revert(); + } + + function upgradeToAndCall(address, bytes memory) external pure { + revert(); + } +} diff --git a/test/vesting/VestingWallet.recovery.test.js b/test/vesting/VestingWallet.recovery.test.js index 63dd5e3b..0301cd23 100644 --- a/test/vesting/VestingWallet.recovery.test.js +++ b/test/vesting/VestingWallet.recovery.test.js @@ -1,7 +1,7 @@ const hre = require('hardhat'); const { ethers } = hre; const { expect } = require('chai'); -const { prepare, deployUpgradeable, performUpgrade, deploy, attach } = require('../fixture'); +const { prepare, deployUpgradeable, performUpgrade } = require('../fixture'); const utils = require('../../scripts/utils'); const allocation = { @@ -17,6 +17,7 @@ describe('VestingWallet ', function () { describe('vesting with admin', function () { beforeEach(async function () { allocation.beneficiary = this.accounts.user1.address; + allocation.newBeneficiary = this.accounts.user2.address; allocation.owner = this.accounts.admin.address; this.vesting = await deployUpgradeable( @@ -37,7 +38,7 @@ describe('VestingWallet ', function () { ); }); - it('perform recovery', async function () { + it('perform recovery (full upgrade)', async function () { this.vesting = await performUpgrade(hre, this.vesting, 'VestingWalletRecovery', { unsafeAllow: 'delegatecall', }); @@ -47,15 +48,26 @@ describe('VestingWallet ', function () { .to.be.revertedWith(`Ownable: caller is not the owner`); // authorized - await expect(this.vesting.connect(this.accounts.admin).updateBeneficiary(this.accounts.user2.address)) - .to.emit(this.vesting, 'BeneficiaryUpdate').withArgs(this.accounts.user2.address); + await expect(this.vesting.connect(this.accounts.admin).updateBeneficiary(allocation.newBeneficiary)) + .to.emit(this.vesting, 'BeneficiaryUpdate').withArgs(allocation.newBeneficiary); + }); + + it('perform recovery (transitory upgrade)', async function () { + const implementation = await hre.upgrades.erc1967.getImplementationAddress(this.vesting.address); + + await performUpgrade(hre, this.vesting, 'VestingWalletRecoveryLight', { + call: { fn: 'changeOwnerAndUpgrade', args: [allocation.newBeneficiary, implementation] }, + unsafeAllow: 'delegatecall' + }); + }); + afterEach(async function () { await Promise.all([this.vesting.start(), this.vesting.cliff(), this.vesting.duration(), this.vesting.beneficiary(), this.vesting.owner()]).then( ([start, cliff, duration, beneficiary, owner]) => { expect(start).to.be.equal(allocation.start); expect(cliff).to.be.equal(allocation.cliff); expect(duration).to.be.equal(allocation.duration); - expect(beneficiary).to.be.equal(this.accounts.user2.address); + expect(beneficiary).to.be.equal(allocation.newBeneficiary); expect(owner).to.be.equal(allocation.owner); } );