diff --git a/src/FluxToken.sol b/src/FluxToken.sol index c9306a4..5969a8b 100644 --- a/src/FluxToken.sol +++ b/src/FluxToken.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-3 pragma solidity ^0.8.15; -import "lib/forge-std/src/console2.sol"; - import "src/interfaces/IFluxToken.sol"; import "src/interfaces/IVotingEscrow.sol"; import "src/interfaces/IAlchemechNFT.sol"; @@ -112,35 +110,22 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { } /// @inheritdoc IFluxToken - function nftClaim(address _nft, uint256 _tokenId) external { + function nftClaim(address _nft, uint256 _tokenId, uint256 _minAmount) external { // require claim to be within a year of deploy date require(block.timestamp < deployDate + oneYear, "claim period has passed"); require(!claimed[_nft][_tokenId], "already claimed"); // value of the NFT - uint256 tokenData = 0; - - // determine which nft is being claimed - if (_nft == alchemechNFT) { - // require sender to be owner of the NFT - require(IAlchemechNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Alchemech NFT"); - - tokenData = IAlchemechNFT(_nft).tokenData(_tokenId); - } else if (_nft == patronNFT) { - // require sender to be owner of the NFT - require(IAlEthNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Patron NFT"); - - tokenData = IAlEthNFT(_nft).tokenData(_tokenId); - } else { - revert("invalid NFT"); - } + uint256 tokenData = getNFTValue(_nft, _tokenId); // mark the token as claimed claimed[_nft][_tokenId] = true; uint256 amount = getClaimableFlux(tokenData, _nft); + require(amount >= _minAmount, "Slippage Exceeded"); + _mint(msg.sender, amount); } @@ -196,13 +181,16 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { function getClaimableFlux(uint256 _amount, address _nft) public view returns (uint256 claimableFlux) { uint256 bpt = _calculateBPT(_amount); - uint256 veMul = IVotingEscrow(veALCX).MULTIPLIER(); - uint256 veMax = IVotingEscrow(veALCX).MAXTIME(); - uint256 fluxPerVe = IVotingEscrow(veALCX).fluxPerVeALCX(); - uint256 fluxMul = IVotingEscrow(veALCX).fluxMultiplier(); + uint256 slope = (bpt * IVotingEscrow(veALCX).MULTIPLIER()) / IVotingEscrow(veALCX).MAXTIME(); + + // Calculate as if time is maxtime + uint256 bias = (slope * ((block.timestamp + IVotingEscrow(veALCX).MAXTIME()) - block.timestamp)); + + // Total amount of flux that would be earned from the amount + uint256 totalFlux = (bias * (IVotingEscrow(veALCX).fluxPerVeALCX() + BPS)) / BPS; - // Amount of flux earned in 1 yr from _amount assuming it was deposited for maxtime - claimableFlux = (((bpt * veMul) / veMax) * veMax * (fluxPerVe + BPS)) / BPS / fluxMul; + // Amount of flux that would be claimable + claimableFlux = totalFlux / IVotingEscrow(veALCX).fluxMultiplier(); // Claimable flux for alchemechNFT is different than patronNFT if (_nft == alchemechNFT) { @@ -210,7 +198,30 @@ contract FluxToken is ERC20("Flux", "FLUX"), IFluxToken { } } - function _calculateBPT(uint256 _amount) public view returns (uint256 bptOut) { + // Retrieves tokenData from the specified nft + function getNFTValue(address _nft, uint256 _tokenId) public view returns (uint256) { + // value of the NFT + uint256 tokenData = 0; + + // determine which nft is being claimed + if (_nft == alchemechNFT) { + // require sender to be owner of the NFT + require(IAlchemechNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Alchemech NFT"); + + tokenData = IAlchemechNFT(_nft).tokenData(_tokenId); + } else if (_nft == patronNFT) { + // require sender to be owner of the NFT + require(IAlEthNFT(_nft).ownerOf(_tokenId) == msg.sender, "not owner of Patron NFT"); + + tokenData = IAlEthNFT(_nft).tokenData(_tokenId); + } else { + revert("invalid NFT"); + } + + return tokenData; + } + + function _calculateBPT(uint256 _amount) internal view returns (uint256 bptOut) { address distributor = IVotingEscrow(veALCX).distributor(); (bytes32 balancerPoolId, address balancerPool, address balancerVault) = IRewardsDistributor(distributor) diff --git a/src/interfaces/IFluxToken.sol b/src/interfaces/IFluxToken.sol index d86d969..5dc7a66 100644 --- a/src/interfaces/IFluxToken.sol +++ b/src/interfaces/IFluxToken.sol @@ -52,10 +52,11 @@ interface IFluxToken is IERC20 { * @notice Mints tokens to a recipient. * @param _nft NFT contract address to claim from. * @param _tokenId NFT tokenId to claim from. + * @param _minAmount Minimum tokens to accept from the claim. * @dev This will revert after set claim period, if the NFT has already been claimed, or if the caller is not the owner of the NFT * @dev Amount of FLUX minted is one year of potential FLUX rewards given the NFTs value. */ - function nftClaim(address _nft, uint256 _tokenId) external; + function nftClaim(address _nft, uint256 _tokenId, uint256 _minAmount) external; /** * @dev Burns `amount` tokens from `account`, deducting from the caller's allowance. diff --git a/src/test/FluxToken.t.sol b/src/test/FluxToken.t.sol index ba55839..719fdf9 100644 --- a/src/test/FluxToken.t.sol +++ b/src/test/FluxToken.t.sol @@ -25,12 +25,16 @@ contract FluxTokenTest is BaseTest { uint256 alchemechTotalNoMultiplier = flux.getClaimableFlux(tokenData2, patronNFT); assertGt(alchemechTotalNoMultiplier, alchemechTotal, "non multiplier calc should be higher"); + + uint256 expectedAmount = flux.getClaimableFlux(flux.getNFTValue(patronNFT, tokenId), patronNFT); hevm.prank(ownerOfPatronNFT); - flux.nftClaim(patronNFT, tokenId); + flux.nftClaim(patronNFT, tokenId, expectedAmount * 9_900 / 10_000); + + expectedAmount = flux.getClaimableFlux(flux.getNFTValue(alchemechNFT, tokenId), alchemechNFT); hevm.prank(ownerOfAlchemechNFT); - flux.nftClaim(alchemechNFT, tokenId); + flux.nftClaim(alchemechNFT, tokenId, expectedAmount * 9_900 / 10_000); assertEq(flux.balanceOf(ownerOfPatronNFT), patronTotal, "owner should have patron flux"); assertEq(flux.balanceOf(ownerOfAlchemechNFT), alchemechTotal, "owner should have alchemech flux"); @@ -43,28 +47,28 @@ contract FluxTokenTest is BaseTest { hevm.expectRevert(abi.encodePacked("invalid NFT")); hevm.prank(ownerOfPatronNFT); - flux.nftClaim(address(0), tokenId); + flux.nftClaim(address(0), tokenId, 0); hevm.expectRevert(abi.encodePacked("not owner of Patron NFT")); hevm.prank(ownerOfAlchemechNFT); - flux.nftClaim(patronNFT, tokenId); + flux.nftClaim(patronNFT, tokenId, 0); hevm.expectRevert(abi.encodePacked("not owner of Alchemech NFT")); hevm.prank(ownerOfPatronNFT); - flux.nftClaim(alchemechNFT, tokenId); + flux.nftClaim(alchemechNFT, tokenId, 0); // Attempt to claim twice hevm.startPrank(ownerOfPatronNFT); - flux.nftClaim(patronNFT, tokenId); + flux.nftClaim(patronNFT, tokenId, 0); hevm.expectRevert(abi.encodePacked("already claimed")); - flux.nftClaim(patronNFT, tokenId); + flux.nftClaim(patronNFT, tokenId, 0); hevm.stopPrank(); // Attempt to claim after claim period (1 year) hevm.warp(block.timestamp + 366 days); hevm.expectRevert(abi.encodePacked("claim period has passed")); - flux.nftClaim(patronNFT, tokenId); + flux.nftClaim(patronNFT, tokenId, 0); } function testFluxAccrual() external {