Skip to content

Commit 6474abf

Browse files
Refactor: Move price calculation and vault management to TalentPlus
- Moved calculateDiscountedPrice, getUserTalentHoldings, and vault management methods from TalentPlusSubscription to TalentPlus - Updated TalentPlus constructor to accept TALENT token address and initial vault addresses - Fixed refund functionality in subscribe method (contract now receives tokens before distributing) - Updated price validation to allow price 0 when discount is 100% - Removed TALENT_TOKEN and vaultAddresses from TalentPlusSubscription - Updated TalentPlusSubscription constructor to only require owner address - Added comprehensive tests for new functionality - Updated deployment scripts to use new constructor signatures This refactoring allows TalentPlusSubscription to be replaced while keeping existing subscriptions, as payment logic is now decoupled in TalentPlus.
1 parent 43e58a4 commit 6474abf

File tree

5 files changed

+90
-283
lines changed

5 files changed

+90
-283
lines changed

contracts/talent_plus/TalentPlusSubscription.sol

Lines changed: 3 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,9 @@ pragma solidity ^0.8.24;
33

44
import "@openzeppelin/contracts/access/Ownable.sol";
55
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
6-
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7-
8-
// Interface for the vault contract to get staked amounts
9-
interface IVault {
10-
function balanceOf(address account) external view returns (uint256);
11-
}
126

137
contract TalentPlusSubscription is Ownable, ReentrancyGuard {
148

15-
// TALENT token address for balance checking
16-
IERC20 public immutable TALENT_TOKEN;
17-
// Array of vault addresses to check staked amounts
18-
address[] public vaultAddresses;
19-
209
// Mapping to store trusted signers
2110
mapping(address => bool) public trustedSigners;
2211

@@ -57,18 +46,9 @@ contract TalentPlusSubscription is Ownable, ReentrancyGuard {
5746
event UserSubscriptionExtended(address indexed wallet, string subscriptionSlug, uint256 expirationTime, uint256 startTime, address payer, uint256 pricePaid);
5847
event TrustedSignerAdded(address indexed signer);
5948
event TrustedSignerRemoved(address indexed signer);
60-
event VaultAddressAdded(address indexed vaultAddress);
61-
event VaultAddressRemoved(address indexed vaultAddress);
6249

63-
constructor(address initialOwner, address talentTokenAddress, address[] memory initialVaultAddresses) Ownable(initialOwner) {
50+
constructor(address initialOwner) Ownable(initialOwner) {
6451
trustedSigners[initialOwner] = true;
65-
TALENT_TOKEN = IERC20(talentTokenAddress);
66-
67-
// Add initial vault addresses
68-
for (uint256 i = 0; i < initialVaultAddresses.length; i++) {
69-
require(initialVaultAddresses[i] != address(0), "Invalid vault address");
70-
vaultAddresses.push(initialVaultAddresses[i]);
71-
}
7252
}
7353

7454
/**
@@ -105,77 +85,6 @@ contract TalentPlusSubscription is Ownable, ReentrancyGuard {
10585
return trustedSigners[signer];
10686
}
10787

108-
/**
109-
* @notice Adds a vault address to the list of vaults to check for staked amounts
110-
* @param vaultAddress The vault address to add
111-
* @dev Can only be called by the owner or trusted signers
112-
*/
113-
function addVaultAddress(address vaultAddress) external {
114-
require(owner() == msg.sender || trustedSigners[msg.sender], "Only owner or trusted signers can add vault addresses");
115-
require(vaultAddress != address(0), "Invalid vault address");
116-
117-
// Check if vault address already exists
118-
for (uint256 i = 0; i < vaultAddresses.length; i++) {
119-
require(vaultAddresses[i] != vaultAddress, "Vault address already exists");
120-
}
121-
122-
vaultAddresses.push(vaultAddress);
123-
emit VaultAddressAdded(vaultAddress);
124-
}
125-
126-
/**
127-
* @notice Removes a vault address from the list of vaults
128-
* @param vaultAddress The vault address to remove
129-
* @dev Can only be called by the owner or trusted signers
130-
*/
131-
function removeVaultAddress(address vaultAddress) external {
132-
require(owner() == msg.sender || trustedSigners[msg.sender], "Only owner or trusted signers can remove vault addresses");
133-
134-
bool found = false;
135-
for (uint256 i = 0; i < vaultAddresses.length; i++) {
136-
if (vaultAddresses[i] == vaultAddress) {
137-
// Move the last element to the position of the element to delete
138-
vaultAddresses[i] = vaultAddresses[vaultAddresses.length - 1];
139-
// Remove the last element
140-
vaultAddresses.pop();
141-
found = true;
142-
break;
143-
}
144-
}
145-
146-
require(found, "Vault address not found");
147-
emit VaultAddressRemoved(vaultAddress);
148-
}
149-
150-
/**
151-
* @notice Gets all vault addresses
152-
* @return An array of all vault addresses
153-
*/
154-
function getVaultAddresses() external view returns (address[] memory) {
155-
return vaultAddresses;
156-
}
157-
158-
/**
159-
* @notice Gets the total number of vault addresses
160-
* @return The number of vault addresses
161-
*/
162-
function getVaultAddressCount() external view returns (uint256) {
163-
return vaultAddresses.length;
164-
}
165-
166-
/**
167-
* @notice Helper function to calculate total staked amount across all vaults
168-
* @param wallet The wallet address to check
169-
* @return totalStaked The total amount staked across all vaults
170-
*/
171-
function _getTotalVaultStaked(address wallet) internal view returns (uint256 totalStaked) {
172-
totalStaked = 0;
173-
for (uint256 i = 0; i < vaultAddresses.length; i++) {
174-
totalStaked += IVault(vaultAddresses[i]).balanceOf(wallet);
175-
}
176-
return totalStaked;
177-
}
178-
17988
/**
18089
* @notice Adds a new subscription model
18190
* @param subscriptionSlug The subscription slug string for the subscription model
@@ -195,7 +104,7 @@ contract TalentPlusSubscription is Ownable, ReentrancyGuard {
195104
require(owner() == msg.sender || trustedSigners[msg.sender], "Only owner or trusted signers can add subscription models");
196105
require(bytes(subscriptionSlug).length > 0, "Subscription slug cannot be empty");
197106
require(durationInSeconds > 0, "Duration must be greater than 0");
198-
require(price > 0, "Price must be greater than 0");
107+
require(price > 0 || discountPercentage == 100, "Price must be greater than 0 unless discount is 100%");
199108
require(discountPercentage <= 100, "Discount percentage cannot exceed 100%");
200109
require(!subscriptionModels[subscriptionSlug].active, "Subscription model already exists");
201110

@@ -234,7 +143,7 @@ contract TalentPlusSubscription is Ownable, ReentrancyGuard {
234143
require(bytes(subscriptionSlug).length > 0, "Subscription slug cannot be empty");
235144
require(subscriptionModels[subscriptionSlug].active, "Subscription model does not exist");
236145
require(durationInSeconds > 0, "Duration must be greater than 0");
237-
require(price > 0, "Price must be greater than 0");
146+
require(price > 0 || discountPercentage == 100, "Price must be greater than 0 unless discount is 100%");
238147
require(discountPercentage <= 100, "Discount percentage cannot exceed 100%");
239148

240149
subscriptionModels[subscriptionSlug].durationInSeconds = durationInSeconds;
@@ -439,60 +348,6 @@ contract TalentPlusSubscription is Ownable, ReentrancyGuard {
439348
return (model.durationInSeconds, model.price, model.discountPercentage, model.talentRequiredForDiscount, model.active);
440349
}
441350

442-
/**
443-
* @notice Calculates the discounted price for a subscription based on TALENT holdings (balance + vault staking across all vaults)
444-
* @param subscriptionSlug The subscription slug
445-
* @param wallet The wallet address to check TALENT balance and vault staking for
446-
* @return finalPrice The final price after applying discount (if applicable)
447-
* @return discountApplied Whether a discount was applied
448-
* @return discountAmount The amount of discount applied (0 if no discount)
449-
*/
450-
function calculateDiscountedPrice(string memory subscriptionSlug, address wallet) external view returns (uint256 finalPrice, bool discountApplied, uint256 discountAmount) {
451-
require(bytes(subscriptionSlug).length > 0, "Subscription slug cannot be empty");
452-
require(wallet != address(0), "Invalid wallet address");
453-
454-
SubscriptionModel memory model = subscriptionModels[subscriptionSlug];
455-
require(model.active, "Subscription model is not active");
456-
457-
uint256 talentBalance = TALENT_TOKEN.balanceOf(wallet);
458-
uint256 vaultStaked = _getTotalVaultStaked(wallet);
459-
uint256 totalTalentHoldings = talentBalance + vaultStaked;
460-
461-
// Check if wallet qualifies for discount (TALENT balance + vault staking)
462-
if (totalTalentHoldings >= model.talentRequiredForDiscount && model.discountPercentage > 0) {
463-
discountAmount = (model.price * model.discountPercentage) / 100;
464-
finalPrice = model.price - discountAmount;
465-
discountApplied = true;
466-
} else {
467-
finalPrice = model.price;
468-
discountApplied = false;
469-
discountAmount = 0;
470-
}
471-
472-
return (finalPrice, discountApplied, discountAmount);
473-
}
474-
475-
/**
476-
* @notice Gets the total TALENT holdings for a user (balance + vault staking across all vaults)
477-
* @param wallet The wallet address to check TALENT holdings for
478-
* @return totalTalentHoldings The total TALENT holdings (balance + vault staked across all vaults)
479-
* @return talentBalance The TALENT token balance in the wallet
480-
* @return vaultStaked The TALENT tokens staked across all vaults
481-
*/
482-
function getUserTalentHoldings(address wallet) external view returns (
483-
uint256 totalTalentHoldings,
484-
uint256 talentBalance,
485-
uint256 vaultStaked
486-
) {
487-
require(wallet != address(0), "Invalid wallet address");
488-
489-
talentBalance = TALENT_TOKEN.balanceOf(wallet);
490-
vaultStaked = _getTotalVaultStaked(wallet);
491-
totalTalentHoldings = talentBalance + vaultStaked;
492-
493-
return (totalTalentHoldings, talentBalance, vaultStaked);
494-
}
495-
496351
/**
497352
* @notice Gets the total number of subscription models
498353
* @return The number of subscription models

scripts/shared/index.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,16 +167,12 @@ export async function deployTalentVault(
167167
}
168168

169169
export async function deployTalentPlusSubscription(
170-
initialOwner: string,
171-
talentTokenAddress: string,
172-
vaultAddresses: string[]
170+
initialOwner: string
173171
): Promise<TalentPlusSubscription> {
174172
const talentPlusSubscriptionContract = await ethers.getContractFactory("TalentPlusSubscription");
175173

176174
const deployedTalentPlusSubscription = await talentPlusSubscriptionContract.deploy(
177-
initialOwner,
178-
talentTokenAddress,
179-
vaultAddresses
175+
initialOwner
180176
);
181177
await deployedTalentPlusSubscription.deployed();
182178

scripts/talent_plus/deployTalentPlus.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ async function main() {
4646

4747
// Step 1: Deploy TalentPlusSubscription
4848
console.log("\n📦 Deploying TalentPlusSubscription...");
49-
const talentPlusSubscription = await deployTalentPlusSubscription(admin.address, talentTokenAddress, vaultAddresses);
49+
const talentPlusSubscription = await deployTalentPlusSubscription(admin.address);
5050
console.log(`✅ TalentPlusSubscription deployed at: ${talentPlusSubscription.address}`);
5151

5252
// Step 2: Deploy TalentPlus
@@ -140,8 +140,8 @@ async function main() {
140140

141141
console.log("\n🔍 Contract Verification Commands:");
142142
console.log("=" .repeat(80));
143-
console.log(`# Verify TalentPlusSubscription (using custom task):`);
144-
console.log(`npx hardhat verify-talent-plus-subscription --network ${network.name} --address ${talentPlusSubscription.address} --owner ${admin.address} --token ${talentTokenAddress} --vaults ${vaultAddresses.join(",")}`);
143+
console.log(`# Verify TalentPlusSubscription:`);
144+
console.log(`npx hardhat verify --network ${network.name} ${talentPlusSubscription.address} ${admin.address}`);
145145
console.log("");
146146
console.log(`# Verify TalentPlus:`);
147147
console.log(`npx hardhat verify --network ${network.name} ${talentPlus.address} ${talentPlusSubscription.address} ${feeReceiver} ${USDC_ADDRESS} ${talentTokenAddress} "[${vaultAddresses.join(",")}]"`);

test/contracts/talent_plus/TalentPlus.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ describe("TalentPlus", () => {
4242
// Deploy TalentPlusSubscription
4343
talentPlusSubscription = (await deployContract(admin, Artifacts.TalentPlusSubscription, [
4444
admin.address,
45-
talentToken.address,
46-
[mockVault.address],
4745
])) as TalentPlusSubscription;
4846

4947
// Deploy TalentPlus with payment token, TALENT token, and initial vault addresses
@@ -107,8 +105,6 @@ describe("TalentPlus", () => {
107105
it("should allow owner to update TalentPlusSubscription address", async () => {
108106
const newSubscription = (await deployContract(admin, Artifacts.TalentPlusSubscription, [
109107
admin.address,
110-
talentToken.address,
111-
[mockVault.address],
112108
])) as TalentPlusSubscription;
113109

114110
await talentPlus.connect(admin).updateTalentPlusSubscription(newSubscription.address);
@@ -405,6 +401,28 @@ describe("TalentPlus", () => {
405401
expect(premiumDiscount).to.be.false;
406402
});
407403

404+
it("should handle price 0 with 100% discount correctly", async () => {
405+
const freeSlug = "free";
406+
407+
// Add a subscription model with price 0 and 100% discount
408+
await talentPlusSubscription.connect(admin).addSubscriptionModel(
409+
freeSlug,
410+
30 * 24 * 60 * 60, // 30 days
411+
0, // Price is 0
412+
100, // 100% discount
413+
parseEther("1000") // 1000 TALENT required
414+
);
415+
416+
// Give user1 enough TALENT tokens for discount
417+
await talentToken.connect(admin).transfer(user1.address, parseEther("1000"));
418+
419+
const [finalPrice, discountApplied, discountAmount] = await talentPlus.calculateDiscountedPrice(freeSlug, user1.address);
420+
421+
expect(finalPrice).to.eq(0); // Final price is 0
422+
expect(discountApplied).to.be.true; // Discount is applied
423+
expect(discountAmount).to.eq(0); // Discount amount is 0 (since base price is 0)
424+
});
425+
408426
it("should revert for inactive subscription model", async () => {
409427
await talentPlusSubscription.connect(admin).deactivateSubscriptionModel(subscriptionSlug);
410428

0 commit comments

Comments
 (0)