How to secure Touched rewards in Roblox
Coins, checkpoints and finish lines that pay on Touched without checking the character is there. How to add distance checks and debounces on the server.
What it looks like
A server script pays out when a character touches a part: a coin, a checkpoint, a finish line, a kill brick that awards the killer, a button that gives items. The Touched handler trusts the touch and never checks that the character is actually there.
Luau
coin.Touched:Connect(function(hit) local player = Players:GetPlayerFromCharacter(hit.Parent) if player then reward(player) end end)
How to detect it
- Find every
TouchedandTouchEndedconnection in server code, and every helper that wraps them, such as zone modules. - For each one that gives something to the player who touched it (currency, items, progress, a stage, a win), look for a server-side check that the touching part is really close to the part it touched, such as the distance between the character's
HumanoidRootPartand the part. - For checkpoints and finish lines, also check order: the server should only accept the next stage, not any stage.
- Look for a per-player debounce. Without one, a single touch can pay out many times as the character's parts touch one after another.
What an exploiter could do
A player's client simulates its own character, and touches involving the character are reported to the server from that client. An exploiter can make those touches happen without going near the part, so they can collect every coin on the map, skip straight to the last checkpoint or claim a finish they never reached.
How to fix it
- In the handler, find the character's
HumanoidRootPartand check its distance from the touched part against the part's size plus a small margin. - Add a per-player debounce, and for one-off pickups, record or remove the pickup before paying.
- For stages, accept a checkpoint only if it's the player's current stage plus one.
- The character's position also comes from the client, so pair this with server-side movement checks for anything valuable.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local coins = Workspace:WaitForChild("Coins") local value = 10 local respawn = 10 for _, coin in coins:GetChildren() do if coin:IsA("BasePart") then coin.Touched:Connect(function(hit) local player = Players:GetPlayerFromCharacter(hit.Parent) local profile = player and PlayerData.get(player) if not profile or coin.Transparency > 0 then return end coin.Transparency = 1 profile.Coins += value task.delay(respawn, function() coin.Transparency = 0 end) end) end end
Fixed example
Fixed
local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local coins = Workspace:WaitForChild("Coins") local value = 10 local respawn = 10 local margin = 4 for _, coin in coins:GetChildren() do if coin:IsA("BasePart") then coin.Touched:Connect(function(hit) local character = hit.Parent local player = Players:GetPlayerFromCharacter(character) local profile = player and PlayerData.get(player) local root = character and character:FindFirstChild("HumanoidRootPart") if not profile or not root or not root:IsA("BasePart") or coin.Transparency > 0 then return end if (root.Position - coin.Position).Magnitude > coin.Size.Magnitude / 2 + root.Size.Magnitude + margin then return end coin.Transparency = 1 profile.Coins += value task.delay(respawn, function() coin.Transparency = 0 end) end) end end
Common false positives
- Touches that only hurt the player who touched, such as a kill brick or lava, give an exploiter nothing. Avoiding them is their own business.
- Touches between two parts the server owns, such as an anchored trap and a server-driven NPC, aren't reported by a client.
- A zone module that checks positions on the server rather than using
Touchedalready has the distance check built in. Read it before reporting.