Skip to content

Commit

Permalink
fix: add nft recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeday committed Jan 10, 2025
1 parent b6156ad commit 4b16505
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 9 deletions.
44 changes: 38 additions & 6 deletions contracts/0.8.25/vaults/Dashboard.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pragma solidity 0.8.25;
import {AccessControlEnumerable} from "@openzeppelin/contracts-v5.0.2/access/extensions/AccessControlEnumerable.sol";
import {OwnableUpgradeable} from "contracts/openzeppelin/5.0.2/upgradeable/access/OwnableUpgradeable.sol";
import {IERC20} from "@openzeppelin/contracts-v5.0.2/token/ERC20/IERC20.sol";
import {IERC721} from "@openzeppelin/contracts-v5.0.2/token/ERC721/IERC721.sol";
import {IERC20Permit} from "@openzeppelin/contracts-v5.0.2/token/ERC20/extensions/IERC20Permit.sol";

import {Math256} from "contracts/common/lib/Math256.sol";
Expand Down Expand Up @@ -199,8 +200,6 @@ contract Dashboard is AccessControlEnumerable {
return Math256.min(address(stakingVault).balance, stakingVault.unlocked());
}

// TODO: add preview view methods for minting and burning

// ==================== Vault Management Functions ====================

/**
Expand Down Expand Up @@ -410,16 +409,37 @@ contract Dashboard is AccessControlEnumerable {
}

/**
* @notice recovers ERC20 tokens or ether from the vault
* @notice recovers ERC20 tokens or ether from the dashboard contract to sender
* @param _token Address of the token to recover, 0 for ether
*/
function recover(address _token) external payable virtual onlyRole(DEFAULT_ADMIN_ROLE) {
function recoverERC20(address _token) external onlyRole(DEFAULT_ADMIN_ROLE) {
uint256 _amount;

if (_token == address(0)) {
payable(msg.sender).transfer(address(this).balance);
_amount = address(this).balance;
payable(msg.sender).transfer(_amount);
} else {
bool success = IERC20(_token).transfer(msg.sender, IERC20(_token).balanceOf(address(this)));
_amount = IERC20(_token).balanceOf(address(this));
bool success = IERC20(_token).transfer(msg.sender, _amount);
if (!success) revert("ERC20: Transfer failed");
}

emit ERC20Recovered(msg.sender, _token, _amount);
}

/**
* @notice Transfers a given token_id of an ERC721-compatible NFT (defined by the token contract address)
* from the dashboard contract to sender
*
* @param _token an ERC721-compatible token
* @param _tokenId token id to recover
*/
function recoverERC721(address _token, uint256 _tokenId) external onlyRole(DEFAULT_ADMIN_ROLE) {
if (_token == address(0)) revert ZeroArgument("_token");

emit ERC721Recovered(msg.sender, _token, _tokenId);

IERC721(_token).transferFrom(address(this), msg.sender, _tokenId);
}

// ==================== Internal Functions ====================
Expand Down Expand Up @@ -533,6 +553,18 @@ contract Dashboard is AccessControlEnumerable {
/// @notice Emitted when the contract is initialized
event Initialized();

/// @notice Emitted when the ERC20 `token` or Ether is recovered (i.e. transferred)
/// @param to The address of the recovery recipient
/// @param token The address of the recovered ERC20 token (zero address for Ether)
/// @param amount The amount of the token recovered
event ERC20Recovered(address indexed to, address indexed token, uint256 amount);

/// @notice Emitted when the ERC721-compatible `token` (NFT) recovered (i.e. transferred)
/// @param to The address of the recovery recipient
/// @param token The address of the recovered ERC721 token
/// @param tokenId id of token recovered
event ERC721Recovered(address indexed to, address indexed token, uint256 tokenId);

// ==================== Errors ====================

/// @notice Error for zero address arguments
Expand Down
14 changes: 14 additions & 0 deletions test/0.8.25/vaults/dashboard/contracts/ERC721_MockForDashboard.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
// for testing purposes only

pragma solidity 0.8.25;

import {ERC721} from "@openzeppelin/contracts-v5.0.2/token/ERC721/ERC721.sol";

contract ERC721_MockForDashboard is ERC721 {
constructor() ERC721("MockERC721", "M721") {}

function mint(address _recipient, uint256 _tokenId) external {
_mint(_recipient, _tokenId);
}
}
37 changes: 34 additions & 3 deletions test/0.8.25/vaults/dashboard/dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from "chai";
import { randomBytes } from "crypto";
import { zeroAddress } from "ethereumjs-util";
import { ZeroAddress } from "ethers";
import { ethers } from "hardhat";

Expand All @@ -10,6 +11,7 @@ import { setBalance } from "@nomicfoundation/hardhat-network-helpers";
import {
Dashboard,
DepositContract__MockForStakingVault,
ERC721_MockForDashboard,
StakingVault,
StETHPermit__HarnessForDashboard,
VaultFactory__MockForDashboard,
Expand All @@ -30,6 +32,7 @@ describe("Dashboard", () => {

let steth: StETHPermit__HarnessForDashboard;
let weth: WETH9__MockForVault;
let erc721: ERC721_MockForDashboard;
let wsteth: WstETH__HarnessForVault;
let hub: VaultHub__MockForDashboard;
let depositContract: DepositContract__MockForStakingVault;
Expand All @@ -54,6 +57,7 @@ describe("Dashboard", () => {
weth = await ethers.deployContract("WETH9__MockForVault");
wsteth = await ethers.deployContract("WstETH__HarnessForVault", [steth]);
hub = await ethers.deployContract("VaultHub__MockForDashboard", [steth]);
erc721 = await ethers.deployContract("ERC721_MockForDashboard");
depositContract = await ethers.deployContract("DepositContract__MockForStakingVault");
vaultImpl = await ethers.deployContract("StakingVault", [hub, depositContract]);
expect(await vaultImpl.vaultHub()).to.equal(hub);
Expand Down Expand Up @@ -1009,26 +1013,53 @@ describe("Dashboard", () => {
});

it("allows only admin to recover", async () => {
await expect(dashboard.connect(stranger).recover(ZeroAddress)).to.be.revertedWithCustomError(
await expect(dashboard.connect(stranger).recoverERC20(ZeroAddress)).to.be.revertedWithCustomError(
dashboard,
"AccessControlUnauthorizedAccount",
);
await expect(dashboard.connect(stranger).recoverERC721(erc721.getAddress(), 0)).to.be.revertedWithCustomError(
dashboard,
"AccessControlUnauthorizedAccount",
);
});

it("recovers all ether", async () => {
const preBalance = await ethers.provider.getBalance(vaultOwner);
const tx = await dashboard.recover(ZeroAddress);
const tx = await dashboard.recoverERC20(ZeroAddress);
const { gasUsed, gasPrice } = (await ethers.provider.getTransactionReceipt(tx.hash))!;

await expect(tx).to.emit(dashboard, "ERC20Recovered").withArgs(tx.from, zeroAddress(), amount);
expect(await ethers.provider.getBalance(dashboard.getAddress())).to.equal(0);
expect(await ethers.provider.getBalance(vaultOwner)).to.equal(preBalance + amount - gasUsed * gasPrice);
});

it("recovers all weth", async () => {
const preBalance = await weth.balanceOf(vaultOwner);
await dashboard.recover(weth.getAddress());
const tx = await dashboard.recoverERC20(weth.getAddress());

await expect(tx)
.to.emit(dashboard, "ERC20Recovered")
.withArgs(tx.from, await weth.getAddress(), amount);
expect(await weth.balanceOf(dashboard.getAddress())).to.equal(0);
expect(await weth.balanceOf(vaultOwner)).to.equal(preBalance + amount);
});

it("does not allow zero token address for erc721 recovery", async () => {
await expect(dashboard.recoverERC721(zeroAddress(), 0)).to.be.revertedWithCustomError(dashboard, "ZeroArgument");
});

it("recovers erc721", async () => {
const dashboardAddress = await dashboard.getAddress();
await erc721.mint(dashboardAddress, 0);
expect(await erc721.ownerOf(0)).to.equal(dashboardAddress);

const tx = await dashboard.recoverERC721(erc721.getAddress(), 0);

await expect(tx)
.to.emit(dashboard, "ERC721Recovered")
.withArgs(tx.from, await erc721.getAddress(), 0);

expect(await erc721.ownerOf(0)).to.equal(vaultOwner.address);
});
});
});

0 comments on commit 4b16505

Please sign in to comment.