Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Yield Staking Zap #31

Open
wants to merge 13 commits into
base: 0x
Choose a base branch
from
Open

Yield Staking Zap #31

wants to merge 13 commits into from

Conversation

tomwade
Copy link
Contributor

@tomwade tomwade commented Sep 2, 2022

@shortcut-integration
Copy link

This pull request has been linked to Shortcut Story #529: Yield Staking Zap.

@tomwade tomwade changed the base branch from master to 0x September 9, 2022 09:08
@alexgausman
Copy link
Contributor

Hey @tomwade. I'm concerned about removing require(msg.sender == nftxVaultFactory.zapContract(), "Not a zap"); from timelockMintFor on NFTXInventoryStaking.sol.

Originally we had only the require(nftxVaultFactory.excludedFromFees(msg.sender), "Not fee excluded"); check on the function, but we had begun adding more addresses to the list of fee exclusions (e.g. ScottLewis for market making, and a custom ROO community staking zap). The timelockMintFor function (on InventoryStaking) allows for xTokens to be minted without depositing vTokens, so we realized that the addresses we added to the fee exclusions were able to infinite mint xTokens without depositing vTokens, basically enabling them to rug vaults. So we added in the zapContract check as a way to ensure only the zap contract can use that function.

The timelockMintFor function on NFTXLPStaking.sol works differently because it makes sure the necessary SLP tokens are being deposited, so there is no way for any 3rd parties (who have been added to the fee exclusion list) to infinite mint xSLP tokens.

I think the benefit of the inventory staking version of timelockMintFor is that it is less gas. NFTXStakingZap handles passing in NFTs/vTokens and then just tells the NFTXInventoryStaking how many xTokens to mint. Whereas with LP staking, the NFTXStakingZap has to pass the SLP tokens to NFTXLPStaking.

I'm guessing that the best option would be for us to change NFTXInventoryStaking so that it mimics NFTXLPStaking. Then it would be possible for us to remove the require(zapContract()) check.

Next step would probably be checking with Kiwi about this.

@@ -110,8 +110,7 @@ contract NFTXInventoryStaking is PausableUpgradeable, UpgradeableBeacon, INFTXIn

function timelockMintFor(uint256 vaultId, uint256 amount, address to, uint256 timelockLength) external virtual override returns (uint256) {
onlyOwnerIfPaused(10);
require(msg.sender == nftxVaultFactory.zapContract(), "Not staking zap");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing this could allow any fee excluded address to unlimited mint any about of tokens

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've added this back but altered the logic so that it looks in a mapping, rather than a single address match


/// @notice A mapping of NFTX Vault IDs to their address corresponding
/// vault contract address
mapping(uint256 => address) public nftxVaultAddresses;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being saved, the factory stores this already no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just creates gas savings by allowing us by bypassing subsequent calls to the factory contract and instead just looking up the internal ID -> address mapping. Isn't an astronomical saving, but does add up.

Comment on lines 104 to 114
// Get our start WETH balance
uint wethBalance = WETH.balanceOf(address(this));

// Wrap ETH into WETH for our contract from the sender
if (msg.value > 0) {
WETH.deposit{value: msg.value}();
}

// Get our vaults base staking token. This is used to calculate the xToken
address baseToken = _vaultAddress(vaultId);
require(baseToken != address(0), 'Invalid vault provided');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Get our start WETH balance
uint wethBalance = WETH.balanceOf(address(this));
// Wrap ETH into WETH for our contract from the sender
if (msg.value > 0) {
WETH.deposit{value: msg.value}();
}
// Get our vaults base staking token. This is used to calculate the xToken
address baseToken = _vaultAddress(vaultId);
require(baseToken != address(0), 'Invalid vault provided');
// Get our vaults base staking token. This is used to calculate the xToken
address baseToken = _vaultAddress(vaultId);
require(baseToken != address(0), 'Invalid vault provided');
// Get our start WETH balance
uint wethBalance = WETH.balanceOf(address(this));
// Wrap ETH into WETH for our contract from the sender
if (msg.value > 0) {
WETH.deposit{value: msg.value}();
}

Reorder for readability

// Make a direct timelock mint using the default timelock duration. This sends directly
// to our user, rather than via the zap, to avoid the timelock locking the tx.
IERC20Upgradeable(baseToken).transfer(inventoryStaking.vaultXToken(vaultId), vaultTokenAmount);
inventoryStaking.timelockMintFor(vaultId, vaultTokenAmount, msg.sender, 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timelock is being applied here, is that ok?

*/

receive() external payable {
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably revert so if anyone just randomly sends ETH it doesn't get stuck

*/

function setSwapTarget(address payable _swapTarget) external onlyOwner {
swapTarget = _swapTarget;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could malicious ownership allow this zap to perform the vulnerability?


function _vaultAddress(uint256 vaultId) internal returns (address) {
if (nftxVaultAddresses[vaultId] == address(0)) {
nftxVaultAddresses[vaultId] = nftxFactory.vault(vaultId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this is needed when you can just use the factory no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mentioned briefly in previous comment, but it is for gas savings for repeat calls

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a rough estimate on savings?

contracts/solidity/NFTXYieldStakingZap.sol Show resolved Hide resolved
contracts/solidity/NFTXYieldStakingZap.sol Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants