diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index a67183252d..793dfa0bcf 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol @@ -733,20 +733,26 @@ contract SuperToken is bytes memory userData, bytes memory operatorData ) internal { - if (address(_underlyingToken) == address(0)) revert SUPER_TOKEN_NO_UNDERLYING_TOKEN(); + bool hasNoUnderlying = address(_underlyingToken) == address(0); (uint256 underlyingAmount, uint256 adjustedAmount) = _toUnderlyingAmount(amount); - // _burn will check the (actual) amount availability again - _burn(operator, account, adjustedAmount, userData.length != 0, userData, operatorData); + adjustedAmount = hasNoUnderlying ? amount : adjustedAmount; - uint256 amountBefore = _underlyingToken.balanceOf(address(this)); - _underlyingToken.safeTransfer(to, underlyingAmount); - uint256 amountAfter = _underlyingToken.balanceOf(address(this)); - uint256 actualDowngradedAmount = amountBefore - amountAfter; - if (underlyingAmount != actualDowngradedAmount) revert SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); + // _burn will check the (actual) amount availability again + _burn(operator, account, adjustedAmount, userData.length != 0, userData, operatorData); - emit TokenDowngraded(account, adjustedAmount); + if (!hasNoUnderlying) { + uint256 amountBefore = _underlyingToken.balanceOf(address(this)); + _underlyingToken.safeTransfer(to, underlyingAmount); + uint256 amountAfter = _underlyingToken.balanceOf(address(this)); + uint256 actualDowngradedAmount = amountBefore - amountAfter; + if (underlyingAmount != actualDowngradedAmount) { + revert SUPER_TOKEN_INFLATIONARY_DEFLATIONARY_NOT_SUPPORTED(); + } + + emit TokenDowngraded(account, adjustedAmount); + } } /** diff --git a/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts b/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts index 7f03b825aa..29b0c585b6 100644 --- a/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts +++ b/packages/ethereum-contracts/test/contracts/superfluid/SuperToken.NonStandard.test.ts @@ -744,7 +744,7 @@ describe("SuperToken's Non Standard Functions", function () { await expectCustomError( customToken.downgrade(100), customToken, - reason + "SF_TOKEN_BURN_INSUFFICIENT_BALANCE" ); await web3tx(customToken.initialize, "customToken.initialize")( ZERO_ADDRESS, diff --git a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol index 3f80f3d779..69ec68e159 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/SuperToken.t.sol @@ -7,6 +7,7 @@ import { UUPSProxiable } from "../../../contracts/upgradability/UUPSProxiable.so import { IERC20, ISuperToken, SuperToken } from "../../../contracts/superfluid/SuperToken.sol"; import { ConstantOutflowNFT, IConstantOutflowNFT } from "../../../contracts/superfluid/ConstantOutflowNFT.sol"; import { ConstantInflowNFT, IConstantInflowNFT } from "../../../contracts/superfluid/ConstantInflowNFT.sol"; +import { IPureSuperToken } from "../../../contracts/interfaces/tokens/IPureSuperToken.sol"; import { FoundrySuperfluidTester } from "../FoundrySuperfluidTester.sol"; import { TestToken } from "../../../contracts/utils/TestToken.sol"; import { TokenDeployerLibrary } from "../../../contracts/utils/SuperfluidFrameworkDeploymentSteps.sol"; @@ -234,4 +235,114 @@ contract SuperTokenIntegrationTest is FoundrySuperfluidTester { UUPSProxiable(address(localSuperToken)).updateCode(address(newSuperTokenLogic)); vm.stopPrank(); } + + function testPureSuperTokenDowngrade(address holder, int256 initialSupply, int256 downgradeAmount) public { + _assumeValidPureSuperTokenBurnConditions(holder, initialSupply, downgradeAmount); + + vm.startPrank(holder); + (IPureSuperToken pureSuperToken) = sfDeployer.deployPureSuperToken("Mr. Token", "MR", uint256(initialSupply)); + uint256 initialTotalSupply = pureSuperToken.totalSupply(); + pureSuperToken.downgrade(uint256(downgradeAmount)); + vm.stopPrank(); + + assertEq( + pureSuperToken.totalSupply(), + initialTotalSupply - uint256(downgradeAmount), + "testPureSuperTokenDowngrade: total supply not updated correctly" + ); + } + + function testPureSuperTokenDowngradeTo(address holder, address to, int256 initialSupply, int256 downgradeAmount) + public + { + _assumeValidPureSuperTokenBurnConditions(holder, initialSupply, downgradeAmount); + + vm.startPrank(holder); + (IPureSuperToken pureSuperToken) = sfDeployer.deployPureSuperToken("Mr. Token", "MR", uint256(initialSupply)); + uint256 initialTotalSupply = pureSuperToken.totalSupply(); + // @note this function doesn't do anything except for burning the tokens + // that is, `to` receives nothing + pureSuperToken.downgradeTo(to, uint256(downgradeAmount)); + vm.stopPrank(); + + assertEq( + pureSuperToken.totalSupply(), + initialTotalSupply - uint256(downgradeAmount), + "testPureSuperTokenDowngradeTo: total supply not updated correctly" + ); + } + + function testPureSuperTokenBurn(address holder, int256 initialSupply, int256 downgradeAmount) public { + _assumeValidPureSuperTokenBurnConditions(holder, initialSupply, downgradeAmount); + + vm.startPrank(holder); + (IPureSuperToken pureSuperToken) = sfDeployer.deployPureSuperToken("Mr. Token", "MR", uint256(initialSupply)); + uint256 initialTotalSupply = pureSuperToken.totalSupply(); + pureSuperToken.burn(uint256(downgradeAmount), ""); + vm.stopPrank(); + + assertEq( + pureSuperToken.totalSupply(), + initialTotalSupply - uint256(downgradeAmount), + "testPureSuperTokenBurn: total supply not updated correctly" + ); + } + + function testPureSuperTokenOperatorBurn( + address operator, + address holder, + int256 initialSupply, + int256 downgradeAmount + ) public { + _assumeValidPureSuperTokenBurnConditions(holder, initialSupply, downgradeAmount); + + vm.startPrank(holder); + (IPureSuperToken pureSuperToken) = sfDeployer.deployPureSuperToken("Mr. Token", "MR", uint256(initialSupply)); + pureSuperToken.authorizeOperator(operator); + vm.stopPrank(); + + assertTrue( + pureSuperToken.isOperatorFor(operator, holder), "testPureSuperTokenOperatorBurn: operator not authorized" + ); + uint256 initialTotalSupply = pureSuperToken.totalSupply(); + + vm.startPrank(operator); + pureSuperToken.operatorBurn(holder, uint256(downgradeAmount), "", ""); + vm.stopPrank(); + + assertEq( + pureSuperToken.totalSupply(), + initialTotalSupply - uint256(downgradeAmount), + "testPureSuperTokenOperatorBurn: total supply not updated correctly" + ); + } + + function testPureSuperTokenOperationDowngrade(address holder, int256 initialSupply, int256 downgradeAmount) + public + { + _assumeValidPureSuperTokenBurnConditions(holder, initialSupply, downgradeAmount); + + vm.startPrank(holder); + (IPureSuperToken pureSuperToken) = sfDeployer.deployPureSuperToken("Mr. Token", "MR", uint256(initialSupply)); + uint256 initialTotalSupply = pureSuperToken.totalSupply(); + vm.stopPrank(); + vm.startPrank(address(sf.host)); + pureSuperToken.operationDowngrade(holder, uint256(downgradeAmount)); + vm.stopPrank(); + + assertEq( + pureSuperToken.totalSupply(), + initialTotalSupply - uint256(downgradeAmount), + "testPureSuperTokenOperatorBurn: total supply not updated correctly" + ); + } + + function _assumeValidPureSuperTokenBurnConditions(address holder, int256 initialSupply, int256 downgradeAmount) + internal + { + vm.assume(initialSupply > downgradeAmount); + vm.assume(initialSupply > 0); + vm.assume(downgradeAmount > 0); + vm.assume(holder != address(0)); + } }