Skip to content

Commit

Permalink
feat(gar): slash operators by 20% of minimum gateway stake if they fa…
Browse files Browse the repository at this point in the history
…il 30 consecutive epochs (#57)
  • Loading branch information
dtfiedler authored Sep 11, 2024
2 parents 2276d73 + bd5f1f6 commit 10775bd
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 65 deletions.
144 changes: 84 additions & 60 deletions spec/gar_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,28 @@ describe("gar", function()
end)
end)

describe("slashOperatorStake", function()
it("should slash operator stake by the provided slash amount and return it to the protocol balance", function()
local slashAmount = 10000
Balances[ao.id] = 0
GatewayRegistry["test-this-is-valid-arweave-wallet-address-1"] = {
operatorStake = gar.getSettings().operators.minStake,
totalDelegatedStake = 0,
vaults = {},
delegates = {},
}
local status, err =
pcall(gar.slashOperatorStake, "test-this-is-valid-arweave-wallet-address-1", slashAmount)
assert.is_true(status)
assert.is_nil(err)
assert.are.equal(
gar.getSettings().operators.minStake - slashAmount,
GatewayRegistry["test-this-is-valid-arweave-wallet-address-1"].operatorStake
)
assert.are.equal(slashAmount, Balances[ao.id])
end)
end)

describe("getGatewayWeightsAtTimestamp", function()
it("shoulud properly compute weights based on gateways for a given timestamp", function()
GatewayRegistry["test-this-is-valid-arweave-wallet-address-1"] = {
Expand Down Expand Up @@ -554,72 +576,74 @@ describe("gar", function()
end)

describe("pruneGateways", function()
it("should remove gateways with endTimestamp < currentTimestamp", function()
local currentTimestamp = 1000000
local msgId = "msgId"
it(
"should remove gateways with endTimestamp < currentTimestamp, slash gateways with failedConsecutiveEpochs > 30 and mark them for leaving",
function()
local currentTimestamp = 1000000
local msgId = "msgId"

-- Set up test gateways
_G.GatewayRegistry = {
["address1"] = {
startTimestamp = currentTimestamp - 1000,
endTimestamp = currentTimestamp - 100, -- Expired
status = "leaving",
operatorStake = gar.getSettings().operators.minStake,
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 30,
-- Set up test gateways
_G.GatewayRegistry = {
["address1"] = {
startTimestamp = currentTimestamp - 1000,
endTimestamp = currentTimestamp - 100, -- Expired
status = "leaving",
operatorStake = gar.getSettings().operators.minStake,
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 30,
},
-- Other gateway properties...
},
-- Other gateway properties...
},
["address2"] = {
startTimestamp = currentTimestamp - 100,
endTimestamp = currentTimestamp + 100, -- Not expired
status = "joined",
operatorStake = gar.getSettings().operators.minStake,
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 30,
["address2"] = {
startTimestamp = currentTimestamp - 100,
endTimestamp = currentTimestamp + 100, -- Not expired, failedConsecutiveEpochs is 20
status = "joined",
operatorStake = gar.getSettings().operators.minStake,
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 20,
},
-- Other gateway properties...
},
-- Other gateway properties...
},
["address3"] = {
startTimestamp = currentTimestamp - 100,
endTimestamp = 0, -- Not expired, but failedConsecutiveEpochs is 30
status = "joined",
operatorStake = gar.getSettings().operators.minStake + 10000,
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 30,
["address3"] = {
startTimestamp = currentTimestamp - 100,
endTimestamp = 0, -- Not expired, but failedConsecutiveEpochs is 30
status = "joined",
operatorStake = gar.getSettings().operators.minStake + 10000, -- will slash 20% of the min operator stake
vaults = {},
delegates = {},
stats = {
failedConsecutiveEpochs = 30,
},
-- Other gateway properties...
},
-- Other gateway properties...
},
}
}

-- Call pruneGateways
gar.pruneGateways(currentTimestamp, msgId)
-- Call pruneGateways
local protocolBalanceBefore = _G.Balances[ao.id] or 0
local status, err = pcall(gar.pruneGateways, currentTimestamp, msgId)
assert.is_true(status)
assert.is_nil(err)

-- Check results
assert.is_nil(GatewayRegistry["address1"])
assert.is_not_nil(GatewayRegistry["address2"])
assert.is_not_nil(GatewayRegistry["address3"])
-- check that gateway 3 was marked as leaving and stake is vaulted
assert.are.equal("leaving", GatewayRegistry["address3"].status)
assert.are.equal(0, GatewayRegistry["address3"].operatorStake)
-- Check that gateway 3's operator stake is vaulted
assert.are.same({
balance = gar.getSettings().operators.minStake,
startTimestamp = currentTimestamp,
endTimestamp = currentTimestamp + gar.getSettings().operators.leaveLengthMs,
}, GatewayRegistry["address3"].vaults["address3"])
assert.are.same({
balance = 10000,
startTimestamp = currentTimestamp,
endTimestamp = currentTimestamp + gar.getSettings().operators.withdrawLengthMs,
}, GatewayRegistry["address3"].vaults[msgId])
end)
local expectedSlashedStake = math.floor(gar.getSettings().operators.minStake * 0.2)
local expectedRemainingStake = math.floor(gar.getSettings().operators.minStake * 0.8) + 10000
assert.is_nil(GatewayRegistry["address1"]) -- removed
assert.is_not_nil(GatewayRegistry["address2"]) -- not removed
assert.is_not_nil(GatewayRegistry["address3"]) -- not removed
-- Check that gateway 3's operator stake is slashed by 20% and the remaining stake is vaulted
assert.are.equal("leaving", GatewayRegistry["address3"].status)
assert.are.equal(0, GatewayRegistry["address3"].operatorStake)
assert.are.same({
balance = expectedRemainingStake,
startTimestamp = currentTimestamp,
endTimestamp = currentTimestamp + gar.getSettings().operators.leaveLengthMs,
}, GatewayRegistry["address3"].vaults["address3"])
assert.are.equal(protocolBalanceBefore + expectedSlashedStake, Balances[ao.id])
end
)

it("should handle empty GatewayRegistry", function()
local currentTimestamp = 1000000
Expand Down
1 change: 1 addition & 0 deletions src/balances.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function balances.reduceBalance(target, qty)
end

function balances.increaseBalance(target, qty)
assert(utils.isInteger(qty), debug.traceback("Quantity must be an integer: " .. qty))
local prevBalance = balances.getBalance(target) or 0
Balances[target] = prevBalance + qty
end
Expand Down
33 changes: 28 additions & 5 deletions src/gar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ GatewayRegistrySettings = GatewayRegistrySettings
maxDelegates = 10000,
leaveLengthMs = 90 * 24 * 60 * 60 * 1000, -- 90 days that balance will be vaulted
failedEpochCountMax = 30, -- number of epochs failed before marked as leaving
failedEpochSlashPercentage = 0.2, -- 20% of stake is returned to protocol balance
},
delegates = {
minStake = 500 * 1000000, -- 500 IO
Expand Down Expand Up @@ -89,17 +90,17 @@ function gar.leaveNetwork(from, currentTimestamp, msgId)

-- Add minimum staked tokens to a vault that unlocks after the gateway completely leaves the network
gateway.vaults[from] = {
balance = gar.getSettings().operators.minStake,
balance = math.min(gar.getSettings().operators.minStake, gateway.operatorStake),
startTimestamp = currentTimestamp,
endTimestamp = gatewayEndTimestamp,
}

gateway.operatorStake = gateway.operatorStake - gar.getSettings().operators.minStake
local remainingStake = gateway.operatorStake - gar.getSettings().operators.minStake

-- Add remainder to another vault
if gateway.operatorStake > 0 then
if remainingStake > 0 then
gateway.vaults[msgId] = {
balance = gateway.operatorStake,
balance = remainingStake,
startTimestamp = currentTimestamp,
endTimestamp = gatewayStakeWithdrawTimestamp,
}
Expand Down Expand Up @@ -628,7 +629,10 @@ function gar.pruneGateways(currentTimestamp, msgId)
and garSettings ~= nil
and gateway.stats.failedConsecutiveEpochs >= garSettings.operators.failedEpochCountMax
then
-- mark as leaving
-- slash 20% of the minimum operator stake and return the rest to the protocol balance, then mark the gateway as leaving
local slashedOperatorStake = math.min(gateway.operatorStake, garSettings.operators.minStake)
local slashAmount = math.floor(slashedOperatorStake * garSettings.operators.failedEpochSlashPercentage)
gar.slashOperatorStake(address, slashAmount)
gar.leaveNetwork(address, currentTimestamp, msgId)
else
if gateway.status == "leaving" and gateway.endTimestamp <= currentTimestamp then
Expand All @@ -640,6 +644,25 @@ function gar.pruneGateways(currentTimestamp, msgId)
end
end

function gar.slashOperatorStake(address, slashAmount)
assert(utils.isInteger(slashAmount), "Slash amount must be an integer")
assert(slashAmount > 0, "Slash amount must be greater than 0")

local gateway = gar.getGateway(address)
if gateway == nil then
error("Gateway does not exist")
end
local garSettings = gar.getSettings()
if garSettings == nil then
error("Gateway Registry settings do not exist")
end

gateway.operatorStake = gateway.operatorStake - slashAmount
balances.increaseBalance(ao.id, slashAmount)
GatewayRegistry[address] = gateway
-- TODO: send slash notice to gateway address
end

function gar.getPaginatedGateways(cursor, limit, sortBy, sortOrder)
local gateways = gar.getGateways()
local gatewaysArray = {}
Expand Down

0 comments on commit 10775bd

Please sign in to comment.