diff --git a/contracts/core/extensions/external-ft/TTUV2ExternalFT.sol b/contracts/core/extensions/external-ft/TTUV2ExternalFT.sol new file mode 100644 index 0000000..6529d98 --- /dev/null +++ b/contracts/core/extensions/external-ft/TTUV2ExternalFT.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: AGPL v3 +pragma solidity ^0.8.20; + +import {TokenTableUnlockerV2} from "../../TokenTableUnlockerV2.sol"; +import {Actual, Preset} from "../../../interfaces/TokenTableUnlockerV2DataModels.sol"; + +contract TTUV2ExternalFT is TokenTableUnlockerV2 { + error Unsupported(); + + function createActuals( + uint256[] calldata actualIds, + Actual[] calldata actuals_, + uint256[] calldata recipientIds, + uint256 batchId, + bytes calldata + ) external virtual onlyOwner { + TokenTableUnlockerV2Storage + storage $ = _getTokenTableUnlockerV2Storage(); + if (!$.isCreateable) revert NotPermissioned(); + for (uint256 i = 0; i < actualIds.length; i++) { + _createActual(actualIds[i], actuals_[i], recipientIds[i], batchId); + } + _callHook(_msgData()); + } + + function createActuals( + address[] calldata, + Actual[] calldata, + uint256[] calldata, + uint256, + bytes calldata + ) external pure virtual override { + revert Unsupported(); + } + + function _createActual( + uint256 actualId, + Actual memory actual, + uint256 recipientId, + uint256 batchId + ) internal virtual { + TokenTableUnlockerV2Storage + storage $ = _getTokenTableUnlockerV2Storage(); + address recipient = $.futureToken.ownerOf(actualId); + Preset storage preset = $._presets[actual.presetId]; + if (_presetIsEmpty(preset)) revert PresetDoesNotExist(); + if (actual.amountClaimed >= actual.totalAmount) + revert InvalidSkipAmount(); + $.actuals[actualId] = actual; + emit ActualCreated( + actual.presetId, + actualId, + recipient, + recipientId, + batchId + ); + } +} diff --git a/contracts/interfaces/ITTUDeployer.sol b/contracts/interfaces/ITTUDeployer.sol index 88bb1c1..3b40375 100644 --- a/contracts/interfaces/ITTUDeployer.sol +++ b/contracts/interfaces/ITTUDeployer.sol @@ -63,4 +63,24 @@ interface ITTUDeployer { ) external returns (ITokenTableUnlockerV2, ITTFutureTokenV2, ITTTrackerTokenV2); + + /** + * @notice Deploys and configures a new set of TokenTable products that uses an existing NFT as FutureToken. + * @dev Emits `TokenTableSuiteDeployed`. Throws: `AlreadyDeployed`. + * @param projectToken The project token address. + * @param projectId A unique projectId, otherwise it will revert. + * @param isUpgradeable When set to false, a `Clone` instead of a `BeaconProxy` is created to prevent future upgradeability. + * @param isCancelable Allow unlocking schedules to be cancelled in the Unlocker. + * @param isHookable Allow Unlocker to call an external hook. + * @param isWithdrawable Allow the founder to withdraw deposited funds. + */ + function deployTTSuite( + address projectToken, + address existingFutureToken, + string calldata projectId, + bool isUpgradeable, + bool isCancelable, + bool isHookable, + bool isWithdrawable + ) external returns (ITokenTableUnlockerV2, ITTTrackerTokenV2); } diff --git a/contracts/proxy/TTUDeployerLite.sol b/contracts/proxy/TTUDeployerLite.sol index 3e2b949..ba4a6c6 100644 --- a/contracts/proxy/TTUDeployerLite.sol +++ b/contracts/proxy/TTUDeployerLite.sol @@ -54,6 +54,7 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { if (!isUpgradeable) { (unlocker, futureToken, trackerToken) = _deployClonesAndInitialize( projectToken, + address(0), isTransferable, isCancelable, isHookable, @@ -63,7 +64,11 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { futureToken = ITTFutureTokenV2( address( new BeaconProxy( - address(beaconManager.futureTokenBeacon()), + address( + beaconManager.beacons( + beaconManager.getId("TTFutureTokenV2") + ) + ), abi.encodeWithSelector( ITTFutureTokenV2.initialize.selector, projectToken, @@ -75,7 +80,11 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { unlocker = ITokenTableUnlockerV2( address( new BeaconProxy( - address(beaconManager.unlockerBeacon()), + address( + beaconManager.beacons( + beaconManager.getId("TokenTableUnlockerV2") + ) + ), abi.encodeWithSelector( ITokenTableUnlockerV2.initialize.selector, projectToken, @@ -83,7 +92,8 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { this, isCancelable, isHookable, - isWithdrawable + isWithdrawable, + false ) ) ) @@ -91,7 +101,11 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { trackerToken = ITTTrackerTokenV2( address( new BeaconProxy( - address(beaconManager.trackerTokenBeacon()), + address( + beaconManager.beacons( + beaconManager.getId("TTTrackerTokenV2") + ) + ), abi.encodeWithSelector( ITTTrackerTokenV2.initialize.selector, address(unlocker) @@ -112,12 +126,86 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { return (unlocker, futureToken, trackerToken); } - function version() external pure virtual returns (string memory) { - return "2.5.7"; + function deployTTSuite( + address projectToken, + address existingFutureToken, + string calldata projectId, + bool isUpgradeable, + bool isCancelable, + bool isHookable, + bool isWithdrawable + ) external returns (ITokenTableUnlockerV2, ITTTrackerTokenV2) { + if (registry[projectId]) revert AlreadyDeployed(); + registry[projectId] = true; + + ITTFutureTokenV2 placeholder; + ITokenTableUnlockerV2 unlocker; + ITTTrackerTokenV2 trackerToken; + if (!isUpgradeable) { + (unlocker, placeholder, trackerToken) = _deployClonesAndInitialize( + projectToken, + existingFutureToken, + false, + isCancelable, + isHookable, + isWithdrawable + ); + } else { + unlocker = ITokenTableUnlockerV2( + address( + new BeaconProxy( + address( + beaconManager.beacons( + beaconManager.getId("TTUV2ExternalFT") + ) + ), + abi.encodeWithSelector( + ITokenTableUnlockerV2.initialize.selector, + projectToken, + existingFutureToken, + this, + isCancelable, + isHookable, + isWithdrawable, + true + ) + ) + ) + ); + trackerToken = ITTTrackerTokenV2( + address( + new BeaconProxy( + address( + beaconManager.beacons( + beaconManager.getId("TTTrackerTokenV2") + ) + ), + abi.encodeWithSelector( + ITTTrackerTokenV2.initialize.selector, + address(unlocker) + ) + ) + ) + ); + } + unlocker.transferOwnership(msg.sender); + emit TokenTableSuiteDeployed( + msg.sender, + projectId, + address(unlocker), + address(existingFutureToken), + address(trackerToken) + ); + return (unlocker, trackerToken); + } + + function version() public pure virtual returns (string memory) { + return "2.6.0"; } function _deployClonesAndInitialize( address projectToken, + address existingFutureToken, bool isTransferable, bool isCancelable, bool isHookable, @@ -131,12 +219,24 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { ITTTrackerTokenV2 trackerToken ) { - futureToken = ITTFutureTokenV2( - Clones.clone(beaconManager.futureTokenBeacon().implementation()) - ); - futureToken.initialize(projectToken, isTransferable); + if (existingFutureToken == address(0)) { + futureToken = ITTFutureTokenV2( + Clones.clone( + beaconManager + .beacons(beaconManager.getId("TTFutureTokenV2")) + .implementation() + ) + ); + futureToken.initialize(projectToken, isTransferable); + } else { + futureToken = ITTFutureTokenV2(existingFutureToken); + } unlocker = ITokenTableUnlockerV2( - Clones.clone(beaconManager.unlockerBeacon().implementation()) + Clones.clone( + beaconManager + .beacons(beaconManager.getId("TokenTableUnlockerV2")) + .implementation() + ) ); unlocker.initialize( projectToken, @@ -147,7 +247,11 @@ contract TTUDeployerLite is ITTUDeployer, Ownable, IVersionable { isWithdrawable ); trackerToken = ITTTrackerTokenV2( - Clones.clone(beaconManager.trackerTokenBeacon().implementation()) + Clones.clone( + beaconManager + .beacons(beaconManager.getId("TTTrackerTokenV2")) + .implementation() + ) ); trackerToken.initialize(address(unlocker)); } diff --git a/contracts/proxy/TTUDeployerLiteZkSync.sol b/contracts/proxy/TTUDeployerLiteZkSync.sol index 509035a..fd6c797 100644 --- a/contracts/proxy/TTUDeployerLiteZkSync.sol +++ b/contracts/proxy/TTUDeployerLiteZkSync.sol @@ -10,8 +10,8 @@ import {TTFutureTokenV2} from "../core/TTFutureTokenV2.sol"; import {TTTrackerTokenV2} from "../core/TTTrackerTokenV2.sol"; contract TTUDeployerLiteZkSync is TTUDeployerLite { - function version() external pure virtual override returns (string memory) { - return "2.5.7-zkSync"; + function version() public pure virtual override returns (string memory) { + return string.concat(super.version(), "-zkSync"); } function _deployClonesAndInitialize( diff --git a/contracts/proxy/TTUV2BeaconManager.sol b/contracts/proxy/TTUV2BeaconManager.sol index de86470..2500257 100644 --- a/contracts/proxy/TTUV2BeaconManager.sol +++ b/contracts/proxy/TTUV2BeaconManager.sol @@ -16,39 +16,28 @@ import {IVersionable} from "../interfaces/IVersionable.sol"; * This contract should be deployed using TTUDeployer. */ contract TTUV2BeaconManager is Ownable, IVersionable { - UpgradeableBeacon public immutable unlockerBeacon; - UpgradeableBeacon public immutable futureTokenBeacon; - UpgradeableBeacon public immutable trackerTokenBeacon; - - constructor( - address unlockerImpl, - address futureTokenImpl, - address trackerTokenImpl - ) Ownable(_msgSender()) { - unlockerBeacon = new UpgradeableBeacon(unlockerImpl, address(this)); - futureTokenBeacon = new UpgradeableBeacon( - futureTokenImpl, - address(this) - ); - trackerTokenBeacon = new UpgradeableBeacon( - trackerTokenImpl, - address(this) - ); - } - - function upgradeUnlocker(address newImpl) external onlyOwner { - unlockerBeacon.upgradeTo(newImpl); + mapping(bytes32 => UpgradeableBeacon) public beacons; + + constructor() Ownable(_msgSender()) {} + + function upgradeCustomBeacon( + bytes32 id, + address newImpl + ) external onlyOwner { + UpgradeableBeacon beacon = beacons[id]; + if (address(beacon) == address(0)) { + beacon = new UpgradeableBeacon(newImpl, address(this)); + } else { + beacon.upgradeTo(newImpl); + } } - function upgradeFutureToken(address newImpl) external onlyOwner { - futureTokenBeacon.upgradeTo(newImpl); - } - - function upgradePreviewToken(address newImpl) external onlyOwner { - trackerTokenBeacon.upgradeTo(newImpl); + function version() external pure returns (string memory) { + return "2.6.0"; } - function version() external pure returns (string memory) { - return "2.0.1"; + // Reserved names: TokenTableUnlockerV2, TTFutureTokenV2, TTTrackerTokenV2 + function getId(string memory name) external pure returns (bytes32) { + return keccak256(abi.encode(name)); } }