diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/Definitions.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/Definitions.sol index f9687901a1..554348fd25 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/Definitions.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/Definitions.sol @@ -192,6 +192,24 @@ library BatchOperation { * ) */ uint32 constant internal OPERATION_TYPE_SUPERTOKEN_DOWNGRADE = 2 + 100; + /** + * @dev SuperToken.upgradeTo batch operation type + * + * Call spec: + * ISuperToken(target).operationUpgradeTo( + * abi.decode(data, (address to, uint256 amount) + * ) + */ + uint32 constant internal OPERATION_TYPE_SUPERTOKEN_UPGRADE_TO = 3 + 100; + /** + * @dev SuperToken.downgradeTo batch operation type + * + * Call spec: + * ISuperToken(target).operationDowngradeTo( + * abi.decode(data, (address to, uint256 amount) + * ) + */ + uint32 constant internal OPERATION_TYPE_SUPERTOKEN_DOWNGRADE_TO = 4 + 100; /** * @dev Superfluid.callAgreement batch operation type * diff --git a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol index ef62a0e93c..43d609b279 100644 --- a/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol +++ b/packages/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol @@ -578,6 +578,27 @@ interface ISuperToken is ISuperfluidToken, IERC20Metadata, IERC777 { */ function operationDowngrade(address account, uint256 amount) external; + /** + * @dev Upgrade ERC20 to SuperToken by host contract and transfer immediately. + * @param account The account to be changed. + * @param amount Number of tokens to be upgraded (in 18 decimals) + * + * @custom:modifiers + * - onlyHost + */ + function operationUpgradeTo(address account, address to, uint256 amount) external; + + /** + * @dev Downgrade ERC20 to SuperToken by host contract and transfer immediately. + * @param account The account to be changed. + * @param to The account to receive upgraded tokens + * @param amount Number of tokens to be downgraded (in 18 decimals) + * + * @custom:modifiers + * - onlyHost + */ + function operationDowngradeTo(address account, address to, uint256 amount) external; + // Flow NFT events /** * @dev Constant Outflow NFT proxy created event diff --git a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol index d1aaee5b55..b2eeee0717 100644 --- a/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol +++ b/packages/ethereum-contracts/contracts/superfluid/SuperToken.sol @@ -882,6 +882,20 @@ contract SuperToken is _downgrade(msg.sender, account, account, amount, "", ""); } + function operationUpgradeTo(address account, address to, uint256 amount) + external virtual override + onlyHost + { + _upgrade(msg.sender, account, to, amount, "", ""); + } + + function operationDowngradeTo(address account, address to, uint256 amount) + external virtual override + onlyHost + { + _downgrade(msg.sender, account, to, amount, "", ""); + } + /************************************************************************** * Modifiers *************************************************************************/ diff --git a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol index 29ef49be95..d0cb6108c5 100644 --- a/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol +++ b/packages/ethereum-contracts/contracts/superfluid/Superfluid.sol @@ -312,7 +312,7 @@ contract Superfluid is //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Superfluid Upgradeable Beacon //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - + /// @inheritdoc ISuperfluid function updatePoolBeaconLogic(address newLogic) external override onlyGovernance { GeneralDistributionAgreementV1 gda = GeneralDistributionAgreementV1( @@ -847,6 +847,18 @@ contract Superfluid is ISuperToken(operations[i].target).operationDowngrade( msgSender, abi.decode(operations[i].data, (uint256))); // amount + } else if (operationType == BatchOperation.OPERATION_TYPE_SUPERTOKEN_UPGRADE_TO) { + (address to, uint256 amount) = abi.decode(operations[i].data, (address, uint256)); + ISuperToken(operations[i].target).operationUpgradeTo( + msgSender, + to, + amount); + } else if (operationType == BatchOperation.OPERATION_TYPE_SUPERTOKEN_DOWNGRADE_TO) { + (address to, uint256 amount) = abi.decode(operations[i].data, (address, uint256)); + ISuperToken(operations[i].target).operationDowngradeTo( + msgSender, + to, + amount); } else if (operationType == BatchOperation.OPERATION_TYPE_SUPERFLUID_CALL_AGREEMENT) { (bytes memory callData, bytes memory userData) = abi.decode(operations[i].data, (bytes, bytes)); _callAgreement( diff --git a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.BatchCall.t.sol b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.BatchCall.t.sol index 3c1a17fa71..2a08f56ef5 100644 --- a/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.BatchCall.t.sol +++ b/packages/ethereum-contracts/test/foundry/superfluid/Superfluid.BatchCall.t.sol @@ -208,4 +208,79 @@ contract SuperfluidBatchCallTest is FoundrySuperfluidTester { vm.expectRevert("CallUtils: target revert()"); sf.host.batchCall{value: 42}(ops); } + + function testRevertIfOperationUpgradeToIsNotCalledByHost(address notHost) public { + vm.assume(notHost != address(sf.host)); + + vm.expectRevert(ISuperfluidToken.SF_TOKEN_ONLY_HOST.selector); + vm.prank(notHost); + superToken.operationUpgradeTo(alice, bob, 100); + } + + function testUpgradeTo(uint256 amount) public { + vm.assume(amount < type(uint64).max); + + vm.prank(alice); + token.approve(address(superToken), amount); + + uint256 bobBalanceBefore = superToken.balanceOf(bob); + vm.prank(address(sf.host)); + superToken.operationUpgradeTo(alice, bob, amount); + uint256 bobBalanceAfter = superToken.balanceOf(bob); + assertEq(bobBalanceAfter, bobBalanceBefore + amount, "Bob has unexpected final balance"); + } + + function testUpgradeToBatchCall(uint256 amount) public { + vm.assume(amount < type(uint64).max); + + vm.prank(alice); + token.approve(address(superToken), amount); + + ISuperfluid.Operation[] memory ops = new ISuperfluid.Operation[](1); + uint256 bobBalanceBefore = superToken.balanceOf(bob); + ops[0] = ISuperfluid.Operation({ + operationType: BatchOperation.OPERATION_TYPE_SUPERTOKEN_UPGRADE_TO, + target: address(superToken), + data: abi.encode(bob, amount) + }); + vm.prank(alice); + sf.host.batchCall(ops); + uint256 bobBalanceAfter = superToken.balanceOf(bob); + assertEq(bobBalanceAfter, bobBalanceBefore + amount, "Bob has unexpected final balance"); + } + + function testRevertIfOperationDowngradeToIsNotCalledByHost(address notHost) public { + vm.assume(notHost != address(sf.host)); + + vm.expectRevert(ISuperfluidToken.SF_TOKEN_ONLY_HOST.selector); + vm.prank(notHost); + superToken.operationDowngradeTo(alice, bob, 100); + } + + function testDowngradeTo(uint256 amount) public { + vm.assume(amount < type(uint64).max); + + uint256 bobBalanceBefore = token.balanceOf(bob); + vm.prank(address(sf.host)); + superToken.operationDowngradeTo(alice, bob, amount); + uint256 bobBalanceAfter = token.balanceOf(bob); + assertEq(bobBalanceAfter, bobBalanceBefore + amount, "Bob has unexpected final balance"); + } + + function testDowngradeToBatchCall(uint256 amount) public { + vm.assume(amount < type(uint64).max); + + ISuperfluid.Operation[] memory ops = new ISuperfluid.Operation[](1); + uint256 bobBalanceBefore = token.balanceOf(bob); + ops[0] = ISuperfluid.Operation({ + operationType: BatchOperation.OPERATION_TYPE_SUPERTOKEN_DOWNGRADE_TO, + target: address(superToken), + data: abi.encode(bob, amount) + }); + vm.prank(alice); + sf.host.batchCall(ops); + uint256 bobBalanceAfter = token.balanceOf(bob); + assertEq(bobBalanceAfter, bobBalanceBefore + amount, "Bob has unexpected final balance"); + } + }