Skip to content

Commit

Permalink
PE-6976: instant withdraw event data (#129)
Browse files Browse the repository at this point in the history
- Corrected spelling of `expendited` -> `expedited` everywhere necessary
- Updated constant names from `FEE` to `RATE` where sensible
- Modified redundant unit test for instant delegate withdrawals to the
max duration (non-invariant boundary case)
- Collected penalty rate and fee information for event data on instant
withdrawals
  • Loading branch information
vilenarios authored Oct 25, 2024
2 parents f1f6be1 + c3b41e5 commit ff06e0a
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 56 deletions.
86 changes: 59 additions & 27 deletions spec/gar_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1075,8 +1075,8 @@ describe("gar", function()

it("should decrease delegated stake with instant withdrawal and apply penalty and remove delegate", function()
Balances[ao.id] = 0
local expenditedWithdrawalFee = 1000 * 0.80
local withdrawalAmount = 1000 - expenditedWithdrawalFee
local expeditedWithdrawalFee = 1000 * 0.80
local withdrawalAmount = 1000 - expeditedWithdrawalFee
GatewayRegistry[stubGatewayAddress] = {
operatorStake = gar.getSettings().operators.minStake,
totalDelegatedStake = gar.getSettings().delegates.minStake + 1000,
Expand Down Expand Up @@ -1117,7 +1117,7 @@ describe("gar", function()
assert.are.same(result.delegates[stubRandomAddress].delegatedStake, gar.getSettings().delegates.minStake)
assert.are.equal(result.totalDelegatedStake, gar.getSettings().delegates.minStake)
assert.are.equal(withdrawalAmount, Balances[stubRandomAddress])
assert.are.equal(expenditedWithdrawalFee, Balances[ao.id])
assert.are.equal(expeditedWithdrawalFee, Balances[ao.id])
assert.are.equal(
gar.getSettings().delegates.minStake,
_G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake
Expand Down Expand Up @@ -1178,9 +1178,9 @@ describe("gar", function()
local currentTimestamp = 1000000
local startTimestamp = 1000000
local vaultBalance = 1000
local expectedPenaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_FEE
local expectedexpenditedWithdrawalFee = vaultBalance * expectedPenaltyRate
local expectedWithdrawalAmount = vaultBalance - expectedexpenditedWithdrawalFee
local expectedPenaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
local expectedexpeditedWithdrawalFee = vaultBalance * expectedPenaltyRate
local expectedWithdrawalAmount = vaultBalance - expectedexpeditedWithdrawalFee

Balances[ao.id] = 0

Expand All @@ -1196,6 +1196,7 @@ describe("gar", function()
[vaultId] = {
balance = vaultBalance,
startTimestamp = startTimestamp,
endTimestamp = startTimestamp + gar.getSettings().delegates.withdrawLengthMs,
},
},
},
Expand Down Expand Up @@ -1224,10 +1225,16 @@ describe("gar", function()
)

assert.is_true(status)
assert.are.equal(nil, result.delegate) -- Delegate should be removed after full withdrawal
assert.are.equal(0, result.totalDelegatedStake)
assert.are.same({
delegate = nil, -- Delegate should be removed after full withdrawal
elapsedTime = 0,
remainingTime = gar.getSettings().delegates.withdrawLengthMs,
penaltyRate = 0.8,
expeditedWithdrawalFee = 800,
amountWithdrawn = 200,
}, result)
assert.are.equal(expectedWithdrawalAmount, Balances[stubRandomAddress])
assert.are.equal(expectedexpenditedWithdrawalFee, Balances[ao.id])
assert.are.equal(expectedexpeditedWithdrawalFee, Balances[ao.id])
assert.are.equal(0, _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake)
end
)
Expand All @@ -1242,13 +1249,15 @@ describe("gar", function()
local elapsedTime = 15 * 24 * 60 * 60 * 1000 -- Half of 30 days in milliseconds
local currentTimestamp = startTimestamp + elapsedTime
local vaultBalance = 1000
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_FEE
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
- (
(constants.MAX_EXPEDITED_WITHDRAWAL_FEE - constants.MIN_EXPEDITED_WITHDRAWAL_FEE)
* (elapsedTime / gar.getSettings().delegates.withdrawLengthMs)
(
constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
- constants.MIN_EXPEDITED_WITHDRAWAL_PENALTY_RATE
) * (elapsedTime / gar.getSettings().delegates.withdrawLengthMs)
)
local expectedexpenditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local expectedWithdrawalAmount = vaultBalance - expectedexpenditedWithdrawalFee
local expectedexpeditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local expectedWithdrawalAmount = vaultBalance - expectedexpeditedWithdrawalFee
Balances[ao.id] = 0

_G.GatewayRegistry[stubGatewayAddress] = {
Expand All @@ -1263,6 +1272,7 @@ describe("gar", function()
[vaultId] = {
balance = vaultBalance,
startTimestamp = startTimestamp,
endTimestamp = startTimestamp + gar.getSettings().delegates.withdrawLengthMs,
},
},
},
Expand Down Expand Up @@ -1291,10 +1301,21 @@ describe("gar", function()
)

assert.is_true(status)
assert.are.equal(nil, next(result.delegate.vaults)) -- Delegate should have no vaults remaining
assert.are.equal(remainingDelegateStakeBalance, result.totalDelegatedStake)
result.penaltyRate = string.format("%.3f", result.penaltyRate) -- stave off floating point errors
assert.are.same({
delegate = {
delegatedStake = 1000,
startTimestamp = 500000,
vaults = {}, -- Delegate should have no vaults remaining
},
elapsedTime = 1296000000,
remainingTime = 1296000000,
penaltyRate = "0.425",
expeditedWithdrawalFee = 425,
amountWithdrawn = 575,
}, result)
assert.are.equal(expectedWithdrawalAmount, Balances[stubRandomAddress])
assert.are.equal(expectedexpenditedWithdrawalFee, Balances[ao.id])
assert.are.equal(expectedexpeditedWithdrawalFee, Balances[ao.id])
assert.are.equal(
remainingDelegateStakeBalance,
_G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake
Expand All @@ -1303,21 +1324,23 @@ describe("gar", function()
)

it(
"should withdraw delegate stake and apply reduced penalty based on more elapsed time and remove delegate",
"should withdraw delegate stake and apply near minimum penalty based nearly all required elapsed time and remove delegate",
function()
-- Setup a valid gateway with a delegate vault
local vaultId = "vault_id_1"
local vaultBalance = 1000
local startTimestamp = 500000
local elapsedTime = 29 * 24 * 60 * 60 * 1000 -- Half of 30 days in milliseconds
local elapsedTime = 30 * 24 * 60 * 60 * 1000 - 1 -- 1ms less than 30 days in milliseconds
local currentTimestamp = startTimestamp + elapsedTime
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_FEE
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
- (
(constants.MAX_EXPEDITED_WITHDRAWAL_FEE - constants.MIN_EXPEDITED_WITHDRAWAL_FEE)
* (elapsedTime / gar.getSettings().delegates.withdrawLengthMs)
(
constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
- constants.MIN_EXPEDITED_WITHDRAWAL_PENALTY_RATE
) * (elapsedTime / gar.getSettings().delegates.withdrawLengthMs)
)
local expectedexpenditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local expectedWithdrawalAmount = vaultBalance - expectedexpenditedWithdrawalFee
local expectedexpeditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local expectedWithdrawalAmount = vaultBalance - expectedexpeditedWithdrawalFee
Balances[ao.id] = 0

_G.GatewayRegistry[stubGatewayAddress] = {
Expand All @@ -1332,6 +1355,7 @@ describe("gar", function()
[vaultId] = {
balance = vaultBalance,
startTimestamp = startTimestamp,
endTimestamp = startTimestamp + gar.getSettings().delegates.withdrawLengthMs,
},
},
},
Expand Down Expand Up @@ -1360,10 +1384,17 @@ describe("gar", function()
)

assert.is_true(status)
assert.are.equal(nil, result.delegate) -- Delegate should be removed after full withdrawal
assert.are.equal(0, result.totalDelegatedStake)
result.penaltyRate = string.format("%.3f", result.penaltyRate) -- stave off floating point errors
assert.are.same({
delegate = nil, -- Delegate should be removed after full withdrawal
elapsedTime = 2591999999,
remainingTime = 1,
penaltyRate = "0.050",
expeditedWithdrawalFee = 50,
amountWithdrawn = 950,
}, result)
assert.are.equal(expectedWithdrawalAmount, Balances[stubRandomAddress])
assert.are.equal(expectedexpenditedWithdrawalFee, Balances[ao.id])
assert.are.equal(expectedexpeditedWithdrawalFee, Balances[ao.id])
assert.are.equal(0, _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake)
end
)
Expand All @@ -1386,6 +1417,7 @@ describe("gar", function()
[vaultId] = {
balance = vaultBalance,
startTimestamp = startTimestamp,
endTimestamp = startTimestamp + gar.getSettings().delegates.withdrawLengthMs,
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions src/constants.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ constants.oneYearSeconds = 60 * 60 * 24 * 365
constants.thirtyDaysSeconds = 60 * 60 * 24 * 30
constants.defaultundernameLimit = 10
constants.totalTokenSupply = 1000000000 * 1000000 -- 1 billion tokens
constants.MIN_EXPEDITED_WITHDRAWAL_FEE = 0.05
constants.MAX_EXPEDITED_WITHDRAWAL_FEE = 0.80
constants.MIN_EXPEDITED_WITHDRAWAL_PENALTY_RATE = 0.05
constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE = 0.80

-- ARNS
constants.DEFAULT_UNDERNAME_COUNT = 10
Expand Down
36 changes: 20 additions & 16 deletions src/gar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -380,11 +380,11 @@ function gar.decreaseDelegateStake(gatewayAddress, delegator, qty, currentTimest
gateway.totalDelegatedStake = gateway.totalDelegatedStake - qty

-- Calculate the penalty amount
local expenditedWithdrawalFee = qty * constants.MAX_EXPEDITED_WITHDRAWAL_FEE
local amountToWithdraw = qty - expenditedWithdrawalFee
local expeditedWithdrawalFee = qty * constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
local amountToWithdraw = qty - expeditedWithdrawalFee

-- Add penalty to AR.IO protocol balance
balances.increaseBalance(ao.id, expenditedWithdrawalFee)
balances.increaseBalance(ao.id, expeditedWithdrawalFee)

-- Withdraw the remaining tokens to the delegate
balances.increaseBalance(delegator, amountToWithdraw)
Expand Down Expand Up @@ -809,30 +809,30 @@ function gar.instantDelegateWithdrawal(from, gatewayAddress, vaultId, currentTim

-- Calculate elapsed time since the withdrawal started
local elapsedTime = currentTimestamp - vault.startTimestamp
local totalWithdrawalTime = gar.getSettings().delegates.withdrawLengthMs
local totalWithdrawalTime = vault.endTimestamp - vault.startTimestamp

-- Ensure the elapsed time is not negative
if elapsedTime < 0 then
error("Invalid elapsed time")
end

-- Calculate the penalty rate based on elapsed time
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_FEE
- (
(constants.MAX_EXPEDITED_WITHDRAWAL_FEE - constants.MIN_EXPEDITED_WITHDRAWAL_FEE)
* (elapsedTime / totalWithdrawalTime)
)
penaltyRate =
math.max(constants.MIN_EXPEDITED_WITHDRAWAL_FEE, math.min(constants.MAX_EXPEDITED_WITHDRAWAL_FEE, penaltyRate)) -- Ensure penalty is within bounds
local maxPenaltyRateReduction = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
- constants.MIN_EXPEDITED_WITHDRAWAL_PENALTY_RATE
local earnedPenaltyRateReduction = maxPenaltyRateReduction * (elapsedTime / totalWithdrawalTime)
local penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE - earnedPenaltyRateReduction
penaltyRate = math.max(
constants.MIN_EXPEDITED_WITHDRAWAL_PENALTY_RATE,
math.min(constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE, penaltyRate)
) -- Ensure penalty is within bounds

-- Calculate the penalty amount and the amount to withdraw
local vaultBalance = vault.balance

local expenditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local amountToWithdraw = vaultBalance - expenditedWithdrawalFee
local expeditedWithdrawalFee = math.floor(vaultBalance * penaltyRate)
local amountToWithdraw = vaultBalance - expeditedWithdrawalFee

-- Add penalty to AR.IO protocol balance
balances.increaseBalance(ao.id, expenditedWithdrawalFee)
balances.increaseBalance(ao.id, expeditedWithdrawalFee)
balances.increaseBalance(from, amountToWithdraw)

-- Remove the vault after withdrawal
Expand All @@ -847,7 +847,11 @@ function gar.instantDelegateWithdrawal(from, gatewayAddress, vaultId, currentTim
GatewayRegistry[gatewayAddress] = gateway
return {
delegate = gar.getGateway(gatewayAddress).delegates[from],
totalDelegatedStake = gateway.totalDelegatedStake,
elapsedTime = elapsedTime,
remainingTime = totalWithdrawalTime - elapsedTime,
penaltyRate = penaltyRate,
expeditedWithdrawalFee = expeditedWithdrawalFee,
amountWithdrawn = amountToWithdraw,
}
end

Expand Down
41 changes: 30 additions & 11 deletions src/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1317,17 +1317,24 @@ addEventingHandler(
return
end

local delegateResult = {}
local delegateResult = result and result.delegate or {
delegatedStake = 0,
vaults = {},
}
msg.ioEvent:addField("Remaining-Delegate-Stake", delegateResult.delegatedStake)
if result ~= nil then
if result.delegate ~= nil then
delegateResult = result.delegate
local newStake = delegateResult.delegatedStake
msg.ioEvent:addField("PreviousStake", newStake - delegateResult.vaults[vaultId].balance)
msg.ioEvent:addField("NewStake", newStake)
msg.ioEvent:addField("GatewayTotalDelegatedStake", result.totalDelegatedStake)
end
msg.ioEvent:addField("Vault-Elapsed-Time", result.elapsedTime)
msg.ioEvent:addField("Vault-Remaining-Time", result.remainingTime)
msg.ioEvent:addField("Penalty-Rate", result.penaltyRate)
msg.ioEvent:addField("Instant-Withdrawal-Fee", result.expeditedWithdrawalFee)
msg.ioEvent:addField("Amount-Withdrawn", result.amountWithdrawn)
msg.ioEvent:addField("Previous-Vault-Balance", result.amountWithdrawn + result.expeditedWithdrawalFee)
lastKnownCirculatingSupply = lastKnownCirculatingSupply + result.amountWithdrawn
lastKnownWithdrawSupply = lastKnownWithdrawSupply - result.amountWithdrawn - result.expeditedWithdrawalFee
end

addSupplyData(msg.ioEvent)

ao.send({
Target = msg.From,
Tags = {
Expand Down Expand Up @@ -1390,13 +1397,22 @@ addEventingHandler(
return
end

local instantWithdrawalFee = 0
local amountWithdrawn = 0
if instantWithdraw then
instantWithdrawalFee = quantity * constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE
amountWithdrawn = quantity - instantWithdrawalFee
msg.ioEvent:addField("Instant-Withdrawal", instantWithdraw)
msg.ioEvent:addField("Instant-Withdrawal-Fee", instantWithdrawalFee)
msg.ioEvent:addField("Amount-Withdrawn", amountWithdrawn)
end

local delegateResult = {}
if gateway ~= nil then
local newStake = gateway.delegates[from].delegatedStake
msg.ioEvent:addField("Previous-Stake", newStake + quantity)
msg.ioEvent:addField("New-Stake", newStake)
msg.ioEvent:addField("Gateway-Total-Delegated-Stake", gateway.totalDelegatedStake)
msg.ioEvent:addField("Instant-Withdrawal", instantWithdraw)
delegateResult = gateway.delegates[from]
local newDelegateVaults = delegateResult.vaults
if newDelegateVaults ~= nil then
Expand All @@ -1411,8 +1427,11 @@ addEventingHandler(
end
end

lastKnownStakedSupply = lastKnownStakedSupply - quantity
lastKnownWithdrawSupply = lastKnownWithdrawSupply + quantity
lastKnownDelegatedSupply = lastKnownDelegatedSupply - quantity
if not instantWithdraw then
lastKnownWithdrawSupply = lastKnownWithdrawSupply + quantity
end
lastKnownCirculatingSupply = lastKnownCirculatingSupply + amountWithdrawn
addSupplyData(msg.ioEvent)

ao.send({
Expand Down

0 comments on commit ff06e0a

Please sign in to comment.