Why tycoon collectors need an owner check
A pad that pays whoever touches it lets anyone take another player's earnings. How to check the plot's owner on the server.
What it looks like
A tycoon or plot has pads and buttons: a collector that pays out the plot's earnings, buy buttons that build the next upgrade, or a door only the owner should open. The Touched handler works out which player touched it, then pays or charges that player, without checking they own the plot.
collector.Touched:Connect(function(hit) local player = Players:GetPlayerFromCharacter(hit.Parent) if player then pay(player, plot:GetAttribute("Stored")) end end)
How to detect it
- Find
Touchedconnections on parts inside plots, tycoons, houses and bases, and on prompts or click detectors there too. - For each one, find the plot's owner as the server recorded it, such as an
OwnerIdattribute the server set or a table kept by a plots module. - Check that the handler compares the touching player with that owner before it pays, charges, builds, opens or changes anything.
- Watch for handlers that use the toucher for one part and the owner for another, such as paying the toucher from the owner's stored earnings, or charging the toucher to build on someone else's plot.
- The touch itself still needs the usual checks: distance and a debounce, as Rewards paid on touch with no distance check describes.
What an exploiter could do
They walk onto, or fire a touch on, other players' collectors and take their earnings, over and over across the server. They press buy buttons on other plots to waste the owner's money or build things in the wrong order, and open doors meant only for the owner. Owners lose what they earned while they were away.
How to fix it
- Look up the plot's owner on the server and return straight away if the touching player isn't them.
- Keep the owner in server-side state, set only by the server when the plot is claimed. Don't trust a name or value on the plot that a client could have influenced.
- For shared features, such as friends being allowed to build, check against a list the owner manages through the server.
Vulnerable example
local Players = game:GetService("Players") local PlayerData = require(script.Parent.PlayerData) local tycoon = workspace:WaitForChild("Tycoons"):WaitForChild("Tycoon1") local collector = tycoon:WaitForChild("Collector") :: BasePart local paid: { [Player]: number } = {} collector.Touched:Connect(function(hit) local character = hit:FindFirstAncestorOfClass("Model") local player = character and Players:GetPlayerFromCharacter(character) local root = character and character:FindFirstChild("HumanoidRootPart") if not player or not root or not root:IsA("BasePart") then return end if (root.Position - collector.Position).Magnitude > 10 or os.clock() - (paid[player] or 0) < 1 then return end local profile = PlayerData.get(player) local stored = tycoon:GetAttribute("Stored") if profile and typeof(stored) == "number" and stored > 0 then paid[player] = os.clock() tycoon:SetAttribute("Stored", 0) profile.Coins += stored end end)
Fixed example
local Players = game:GetService("Players") local PlayerData = require(script.Parent.PlayerData) local tycoon = workspace:WaitForChild("Tycoons"):WaitForChild("Tycoon1") local collector = tycoon:WaitForChild("Collector") :: BasePart local paid: { [Player]: number } = {} collector.Touched:Connect(function(hit) local character = hit:FindFirstAncestorOfClass("Model") local player = character and Players:GetPlayerFromCharacter(character) local root = character and character:FindFirstChild("HumanoidRootPart") if not player or not root or not root:IsA("BasePart") then return end if tycoon:GetAttribute("OwnerId") ~= player.UserId then return end if (root.Position - collector.Position).Magnitude > 10 or os.clock() - (paid[player] or 0) < 1 then return end local profile = PlayerData.get(player) local stored = tycoon:GetAttribute("Stored") if profile and typeof(stored) == "number" and stored > 0 then paid[player] = os.clock() tycoon:SetAttribute("Stored", 0) profile.Coins += stored end end)
OwnerId is set by the server when a player claims the tycoon.
Common false positives
- Pads meant for anyone, such as a shared sell pad or a lobby reward, don't need an owner check. Judge them with Rewards paid on touch with no distance check.
- Handlers that look up the toucher's own plot and act on that, rather than the plot that was touched, are fine.
- Remotes that act on plots belong to Instance arguments used without an ownership check or Remote handlers that act for the wrong player.