Skip to content

Commit ad9e61a

Browse files
committed
create ShutdownRedeemer
1 parent 524a68f commit ad9e61a

File tree

16 files changed

+563
-0
lines changed

16 files changed

+563
-0
lines changed

deploy/ShutdownRedeemer.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { HardhatRuntimeEnvironment, Network } from "hardhat/types";
2+
import { DeployFunction } from "hardhat-deploy/types";
3+
import { deployShutdownRedeemer } from "./modules/ShutdownRedeemer";
4+
5+
const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
6+
const { shutdownRedeemer } = await deployShutdownRedeemer({
7+
hre,
8+
});
9+
};
10+
export default func;
11+
func.tags = ["ShutdownRedeemer"];
12+
func.dependencies = [];

deploy/modules/ShutdownRedeemer.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { HardhatRuntimeEnvironment } from "hardhat/types";
2+
import { getConfig, handleUpgradeDeploy } from "../utils";
3+
4+
export const deployShutdownRedeemer = async ({
5+
hre,
6+
}: {
7+
hre: HardhatRuntimeEnvironment;
8+
}) => {
9+
const { deployer, config } = await getConfig(hre);
10+
11+
const shutdownRedeemer = await handleUpgradeDeploy({
12+
hre,
13+
contractName: "ShutdownRedeemerUpgradeable",
14+
deployOptions: {
15+
from: deployer,
16+
args: [config.v2VaultFactory],
17+
proxy: {
18+
proxyContract: "OpenZeppelinTransparentProxy",
19+
execute: {
20+
init: {
21+
methodName: "__ShutdownRedeemer_init",
22+
args: [],
23+
},
24+
},
25+
},
26+
log: true,
27+
},
28+
});
29+
30+
return {
31+
shutdownRedeemer: shutdownRedeemer.address,
32+
};
33+
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity =0.8.15;
3+
4+
// inheriting
5+
import {PausableUpgradeable} from "@src/custom/PausableUpgradeable.sol";
6+
7+
// libs
8+
import {TransferLib} from "@src/lib/TransferLib.sol";
9+
10+
// interfaces
11+
import {INFTXVaultFactoryV2} from "@src/v2/interfaces/INFTXVaultFactoryV2.sol";
12+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
13+
14+
/**
15+
* @title Shutdown Redeemer
16+
* @author @apoorvlathey
17+
*
18+
* @notice Allows users to exchange their vault tokens for ETH, after the vault shutdown.
19+
*/
20+
contract ShutdownRedeemerUpgradeable is PausableUpgradeable {
21+
// =============================================================
22+
// CONSTANTS
23+
// =============================================================
24+
25+
uint256 constant PAUSE_REDEEM_LOCKID = 0;
26+
uint256 constant PRECISION = 1 ether;
27+
28+
INFTXVaultFactoryV2 public immutable V2VaultFactory;
29+
30+
// =============================================================
31+
// VARIABLES
32+
// =============================================================
33+
34+
// vaultId -> ethPerVToken
35+
/// @notice Stores value multiplied by 10^18 (PRECISION)
36+
mapping(uint256 => uint256) public ethPerVToken;
37+
38+
// =============================================================
39+
// EVENTS
40+
// =============================================================
41+
42+
event Redeemed(
43+
uint256 indexed vaultId,
44+
uint256 vTokenAmount,
45+
uint256 ethAmount
46+
);
47+
event EthPerVTokenSet(uint256 indexed vaultId, uint256 value);
48+
49+
// =============================================================
50+
// ERRORS
51+
// =============================================================
52+
53+
error RedeemNotEnabled();
54+
error NoETHSent();
55+
56+
// =============================================================
57+
// INIT
58+
// =============================================================
59+
60+
constructor(INFTXVaultFactoryV2 V2VaultFactory_) {
61+
V2VaultFactory = V2VaultFactory_;
62+
}
63+
64+
function __ShutdownRedeemer_init() external initializer {
65+
__Pausable_init();
66+
}
67+
68+
// =============================================================
69+
// PUBLIC / EXTERNAL WRITE
70+
// =============================================================
71+
72+
/**
73+
* @notice Burn sender's vTokens by locking in this contract and redeem ETH in exchange
74+
*
75+
* @param vaultId The id of the vault
76+
* @param vTokenAmount Vault tokens amount to burn and redeem
77+
*/
78+
function redeem(uint256 vaultId, uint256 vTokenAmount) external {
79+
onlyOwnerIfPaused(PAUSE_REDEEM_LOCKID);
80+
81+
uint256 _ethPerVToken = ethPerVToken[vaultId];
82+
if (_ethPerVToken == 0) revert RedeemNotEnabled();
83+
84+
// burn sender's vToken by locking in this contract
85+
address vToken = V2VaultFactory.vault(vaultId);
86+
IERC20(vToken).transferFrom(msg.sender, address(this), vTokenAmount);
87+
88+
// transfer ETH to the sender in exchange
89+
uint256 ethToSend = (vTokenAmount * _ethPerVToken) / PRECISION;
90+
TransferLib.transferETH(msg.sender, ethToSend);
91+
92+
emit Redeemed(vaultId, vTokenAmount, ethToSend);
93+
}
94+
95+
// =============================================================
96+
// ONLY OWNER WRITE
97+
// =============================================================
98+
99+
/**
100+
* @notice Add new vault for redemption. Send total ETH corresponding to the sale of all NFTs after the vault shutdown.
101+
*
102+
* @param vaultId The id of the vault
103+
*/
104+
function addVaultForRedeem(uint256 vaultId) external payable onlyOwner {
105+
if (msg.value == 0) revert NoETHSent();
106+
107+
address vToken = V2VaultFactory.vault(vaultId);
108+
uint256 vTokenTotalSupply = IERC20(vToken).totalSupply();
109+
110+
uint256 _ethPerVToken = (msg.value * PRECISION) / vTokenTotalSupply;
111+
ethPerVToken[vaultId] = _ethPerVToken;
112+
113+
emit EthPerVTokenSet(vaultId, _ethPerVToken);
114+
}
115+
116+
/**
117+
* @notice Modify ethPerVToken
118+
*
119+
* @param vaultId The id of the vault
120+
* @param ethPerVToken_ New ethPerVToken value for the vault
121+
*/
122+
function setEthPerVToken(
123+
uint256 vaultId,
124+
uint256 ethPerVToken_
125+
) external payable onlyOwner {
126+
ethPerVToken[vaultId] = ethPerVToken_;
127+
128+
emit EthPerVTokenSet(vaultId, ethPerVToken_);
129+
}
130+
131+
/**
132+
* @notice Allows owner to withdraw ETH from the contract
133+
*
134+
* @param ethAmount Amount of ETH to withdraw
135+
*/
136+
function recoverETH(uint256 ethAmount) external onlyOwner {
137+
TransferLib.transferETH(msg.sender, ethAmount);
138+
}
139+
140+
// allow receiving more ETH externally
141+
receive() external payable {}
142+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import {ShutdownRedeemerUpgradeable} from "@src/ShutdownRedeemerUpgradeable.sol";
5+
import {NFTXVaultFactoryUpgradeableV3} from "@src/NFTXVaultFactoryUpgradeableV3.sol";
6+
7+
import {INFTXVaultFactoryV2} from "@src/v2/interfaces/INFTXVaultFactoryV2.sol";
8+
9+
import {NewTestBase} from "@test/NewTestBase.sol";
10+
11+
contract ShutdownRedeemer_Unit_Test is NewTestBase {
12+
ShutdownRedeemerUpgradeable shutdownRedeemer;
13+
NFTXVaultFactoryUpgradeableV3 vaultFactory;
14+
15+
function setUp() public virtual override {
16+
super.setUp();
17+
18+
switchPrank(users.owner);
19+
(, , , , vaultFactory, ) = deployNFTXV3Core();
20+
21+
// `vault` function same in V2 and V3 vault factories along with ERC20 functions for V2 and V3 vaults, so using v3 here for simplicity
22+
shutdownRedeemer = new ShutdownRedeemerUpgradeable(
23+
INFTXVaultFactoryV2(address(vaultFactory))
24+
);
25+
shutdownRedeemer.__ShutdownRedeemer_init();
26+
27+
switchPrank(users.alice);
28+
}
29+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
addVaultForRedeem.t.sol
2+
├── when the caller is not the owner
3+
│ └── it should revert
4+
└── when the caller is the owner
5+
├── when no eth is sent
6+
│ └── it should revert
7+
└── when eth is sent
8+
├── it should set eth per vtoken value
9+
└── it should emit {EthPerVTokenSet} event
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import {ShutdownRedeemerUpgradeable} from "@src/ShutdownRedeemerUpgradeable.sol";
5+
import {NFTXVaultUpgradeableV3} from "@src/NFTXVaultUpgradeableV3.sol";
6+
import {MockNFT} from "@mocks/MockNFT.sol";
7+
8+
import {ShutdownRedeemer_Unit_Test} from "../ShutdownRedeemer.t.sol";
9+
10+
contract ShutdownRedeemer_addVaultForRedeem_Unit_Test is
11+
ShutdownRedeemer_Unit_Test
12+
{
13+
uint256 vaultId;
14+
uint256 ethToSend;
15+
16+
event EthPerVTokenSet(uint256 indexed vaultId, uint256 value);
17+
18+
function setUp() public virtual override {
19+
super.setUp();
20+
21+
(vaultId, ) = deployVToken721(vaultFactory);
22+
}
23+
24+
function test_RevertWhen_TheCallerIsNotTheOwner() external {
25+
// it should revert
26+
vm.expectRevert(OWNABLE_NOT_OWNER_ERROR);
27+
shutdownRedeemer.addVaultForRedeem(vaultId);
28+
}
29+
30+
modifier whenTheCallerIsTheOwner() {
31+
switchPrank(users.owner);
32+
_;
33+
}
34+
35+
function test_RevertWhen_NoEthIsSent() external whenTheCallerIsTheOwner {
36+
ethToSend = 0;
37+
38+
// it should revert
39+
vm.expectRevert(ShutdownRedeemerUpgradeable.NoETHSent.selector);
40+
shutdownRedeemer.addVaultForRedeem(vaultId);
41+
}
42+
43+
function test_WhenEthIsSent() external whenTheCallerIsTheOwner {
44+
ethToSend = 0.5 ether;
45+
uint256 nftQty = 2;
46+
uint256 expectedEthPerVToken = 0.25 ether;
47+
48+
// mint vTokens to have totalSupply non zero
49+
NFTXVaultUpgradeableV3 vault = NFTXVaultUpgradeableV3(
50+
vaultFactory.vault(vaultId)
51+
);
52+
53+
MockNFT nft = MockNFT(vault.assetAddress());
54+
uint256[] memory tokenIds = nft.mint(nftQty);
55+
nft.setApprovalForAll(address(vault), true);
56+
57+
vault.mint({
58+
tokenIds: tokenIds,
59+
amounts: emptyAmounts,
60+
depositor: users.owner,
61+
to: users.owner
62+
});
63+
// shutdown vault
64+
vault.shutdown({recipient: users.owner, tokenIds: tokenIds});
65+
66+
// it should emit {EthPerVTokenSet} event
67+
vm.expectEmit(true, false, false, true);
68+
emit EthPerVTokenSet(vaultId, expectedEthPerVToken);
69+
shutdownRedeemer.addVaultForRedeem{value: ethToSend}(vaultId);
70+
71+
// it should set eth per vtoken value
72+
assertEq(shutdownRedeemer.ethPerVToken(vaultId), expectedEthPerVToken);
73+
}
74+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import {ShutdownRedeemerUpgradeable} from "@src/ShutdownRedeemerUpgradeable.sol";
5+
import {INFTXVaultFactoryV2} from "@src/v2/interfaces/INFTXVaultFactoryV2.sol";
6+
7+
import {ShutdownRedeemer_Unit_Test} from "../ShutdownRedeemer.t.sol";
8+
9+
contract ShutdownRedeemer_Init_Unit_Test is ShutdownRedeemer_Unit_Test {
10+
function setUp() public virtual override {
11+
super.setUp();
12+
13+
// this contract should be initialized by the deployer(owner)
14+
switchPrank(users.owner);
15+
// use uninitialized ShutdownRedeemer for these tests
16+
shutdownRedeemer = new ShutdownRedeemerUpgradeable(
17+
INFTXVaultFactoryV2(address(vaultFactory))
18+
);
19+
}
20+
21+
function test_RevertGiven_TheContractIsInitialized() external {
22+
// initialize the contract
23+
shutdownRedeemer.__ShutdownRedeemer_init();
24+
25+
// it should revert, if initialized again
26+
vm.expectRevert(REVERT_ALREADY_INITIALIZED);
27+
shutdownRedeemer.__ShutdownRedeemer_init();
28+
}
29+
30+
function test_GivenTheContractIsNotInitialized() external {
31+
shutdownRedeemer.__ShutdownRedeemer_init();
32+
33+
// it should set the owner
34+
assertEq(shutdownRedeemer.owner(), users.owner);
35+
// it should have the vault factory already set
36+
assertEq(
37+
address(shutdownRedeemer.V2VaultFactory()),
38+
address(vaultFactory)
39+
);
40+
}
41+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
init.t.sol
2+
├── given the contract is initialized
3+
│ └── it should revert
4+
└── given the contract is not initialized
5+
├── it should set the owner
6+
└── it should have the vault factory already set
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.0;
3+
4+
import {ShutdownRedeemer_Unit_Test} from "../ShutdownRedeemer.t.sol";
5+
6+
contract ShutdownRedeemer_receive_Unit_Test is ShutdownRedeemer_Unit_Test {
7+
function test_ShouldAllowSendingETHExternally() external {
8+
// it should allow sending ETH externally
9+
(bool success, ) = address(shutdownRedeemer).call{value: 1 ether}("");
10+
11+
assertEq(success, true);
12+
}
13+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
receive.t.sol
2+
└── it should allow sending ETH externally

0 commit comments

Comments
 (0)