diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f79587f..19a204db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,5 +37,11 @@ "editor.formatOnPaste": true, "editor.formatOnSaveMode": "file" }, - "cSpell.words": ["ARIO", "hashchain", "redelegate"] + "cSpell.words": [ + "ARIO", + "hashchain", + "redelegate", + "redelegation", + "redelegations" + ] } diff --git a/spec/gar_spec.lua b/spec/gar_spec.lua index 3a10fa5b..087bfc9f 100644 --- a/spec/gar_spec.lua +++ b/spec/gar_spec.lua @@ -1129,9 +1129,11 @@ describe("gar", function() weights = testGateway.weights, }, result) end) + end) - it("should decrease delegated stake if the remaining stake is greater than the minimum stake", function() - local totalDelegatedStake = 750000000 + describe("decreaseDelegateStake", function() + it("should decrease delegated stake if the remaining stake is at least the minimum stake", function() + local totalDelegatedStake = minDelegatedStake + 100000000 local decreaseAmount = 100000000 _G.GatewayRegistry[stubGatewayAddress] = testGateway _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake = totalDelegatedStake @@ -1141,13 +1143,13 @@ describe("gar", function() vaults = {}, } - local expectation = { + local expectedGateway = { operatorStake = testGateway.operatorStake, - totalDelegatedStake = totalDelegatedStake - decreaseAmount, + totalDelegatedStake = minDelegatedStake, vaults = {}, delegates = { [stubRandomAddress] = { - delegatedStake = totalDelegatedStake - decreaseAmount, + delegatedStake = minDelegatedStake, startTimestamp = 0, vaults = { [stubMessageId] = { @@ -1166,6 +1168,25 @@ describe("gar", function() observerAddress = testGateway.observerAddress, weights = testGateway.weights, } + + local expectation = { + amountWithdrawn = 0, + delegatePruned = false, + expeditedWithdrawalFee = 0, + gatewayTotalDelegatedStake = minDelegatedStake, + penaltyRate = 0, + updatedDelegate = { + delegatedStake = minDelegatedStake, + startTimestamp = 0, + vaults = { + [stubMessageId] = { + balance = decreaseAmount, + startTimestamp = startTimestamp, + endTimestamp = startTimestamp + (90 * 24 * 60 * 60 * 1000), -- 90 days + }, + }, + }, + } local status, result = pcall( gar.decreaseDelegateStake, stubGatewayAddress, @@ -1175,29 +1196,126 @@ describe("gar", function() stubMessageId ) assert.is_true(status) - assert.are.same(expectation, result.gateway) - assert.are.same(expectation, _G.GatewayRegistry[stubGatewayAddress]) + assert.are.same(expectation, result) + assert.are.same(expectedGateway, _G.GatewayRegistry[stubGatewayAddress]) end) - it("should decrease delegated stake with instant withdrawal and apply penalty and remove delegate", function() - _G.Balances[ao.id] = 0 - local expeditedWithdrawalFee = 1000 * 0.50 - local withdrawalAmount = 1000 - expeditedWithdrawalFee - _G.GatewayRegistry[stubGatewayAddress] = { - operatorStake = minOperatorStake, - totalDelegatedStake = minDelegatedStake + 1000, + it( + "should decrease delegated stake with instant withdrawal if the remaining stake is at least the minimum stake", + function() + local totalDelegatedStake = minDelegatedStake + 100000000 + local decreaseAmount = 100000000 + local expeditedWithdrawalFee = decreaseAmount * 0.50 + local withdrawalAmount = decreaseAmount - expeditedWithdrawalFee + _G.GatewayRegistry[stubGatewayAddress] = testGateway + _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake = totalDelegatedStake + _G.GatewayRegistry[stubGatewayAddress].delegates[stubRandomAddress] = { + delegatedStake = totalDelegatedStake, + startTimestamp = 0, + vaults = {}, + } + + local expectedGateway = { + operatorStake = testGateway.operatorStake, + totalDelegatedStake = minDelegatedStake, + vaults = {}, + delegates = { + [stubRandomAddress] = { + delegatedStake = minDelegatedStake, + startTimestamp = 0, + vaults = {}, + }, + }, + startTimestamp = testGateway.startTimestamp, + stats = testGateway.stats, + services = testGateway.services, + settings = testGateway.settings, + status = testGateway.status, + observerAddress = testGateway.observerAddress, + weights = testGateway.weights, + } + + local expectation = { + amountWithdrawn = withdrawalAmount, + delegatePruned = false, + expeditedWithdrawalFee = expeditedWithdrawalFee, + gatewayTotalDelegatedStake = minDelegatedStake, + penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE, + updatedDelegate = { + delegatedStake = minDelegatedStake, + startTimestamp = 0, + vaults = {}, + }, + } + local status, result = pcall( + gar.decreaseDelegateStake, + stubGatewayAddress, + stubRandomAddress, + decreaseAmount, + startTimestamp, + stubMessageId, + true -- Instant withdrawal flag + ) + assert.is_true(status) + assert.are.same(expectation, result) + assert.are.same(expectedGateway, _G.GatewayRegistry[stubGatewayAddress]) + assert.are.equal(withdrawalAmount, _G.Balances[stubRandomAddress]) + assert.are.equal(expeditedWithdrawalFee, _G.Balances[ao.id]) + end + ) + + it("should allow decreasing entire delegated stake", function() + local totalDelegatedStake = minDelegatedStake + local decreaseAmount = minDelegatedStake + _G.GatewayRegistry[stubGatewayAddress] = testGateway + _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake = totalDelegatedStake + _G.GatewayRegistry[stubGatewayAddress].delegates[stubRandomAddress] = { + delegatedStake = totalDelegatedStake, + startTimestamp = 0, vaults = {}, - startTimestamp = startTimestamp, + } + + local expectedGateway = { + operatorStake = testGateway.operatorStake, + totalDelegatedStake = 0, + vaults = {}, + delegates = { + [stubRandomAddress] = { + delegatedStake = 0, + startTimestamp = 0, + vaults = { + [stubMessageId] = { + balance = decreaseAmount, + startTimestamp = startTimestamp, + endTimestamp = startTimestamp + (90 * 24 * 60 * 60 * 1000), -- 90 days + }, + }, + }, + }, + startTimestamp = testGateway.startTimestamp, stats = testGateway.stats, services = testGateway.services, settings = testGateway.settings, status = testGateway.status, observerAddress = testGateway.observerAddress, - delegates = { - [stubRandomAddress] = { - delegatedStake = minDelegatedStake + 1000, - startTimestamp = 0, - vaults = {}, + weights = testGateway.weights, + } + + local expectation = { + amountWithdrawn = 0, + delegatePruned = false, + expeditedWithdrawalFee = 0, + gatewayTotalDelegatedStake = 0, + penaltyRate = 0, + updatedDelegate = { + delegatedStake = 0, + startTimestamp = 0, + vaults = { + [stubMessageId] = { + balance = decreaseAmount, + startTimestamp = startTimestamp, + endTimestamp = startTimestamp + (90 * 24 * 60 * 60 * 1000), -- 90 days + }, }, }, } @@ -1205,28 +1323,76 @@ describe("gar", function() gar.decreaseDelegateStake, stubGatewayAddress, stubRandomAddress, - 1000, + decreaseAmount, startTimestamp, - stubMessageId, - true -- instant withdrawal + stubMessageId ) + assert.is_true(status) + assert.are.same(expectation, result) + assert.are.same(expectedGateway, _G.GatewayRegistry[stubGatewayAddress]) + end) + it("should allow decreasing entire delegated stake with instant withdrawal and prune the delegate", function() + local totalDelegatedStake = minDelegatedStake + local decreaseAmount = minDelegatedStake + local expeditedWithdrawalFee = decreaseAmount * 0.50 + local withdrawalAmount = decreaseAmount - expeditedWithdrawalFee + _G.GatewayRegistry[stubGatewayAddress] = testGateway + _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake = totalDelegatedStake + _G.GatewayRegistry[stubGatewayAddress].delegates[stubRandomAddress] = { + delegatedStake = totalDelegatedStake, + startTimestamp = 0, + vaults = {}, + } + + local expectedGateway = { + operatorStake = testGateway.operatorStake, + totalDelegatedStake = 0, + vaults = {}, + delegates = {}, + startTimestamp = testGateway.startTimestamp, + stats = testGateway.stats, + services = testGateway.services, + settings = utils.deepCopy(testGateway.settings), + status = testGateway.status, + observerAddress = testGateway.observerAddress, + weights = testGateway.weights, + } + expectedGateway.settings.allowedDelegatesLookup["test-this-is-valid-arweave-wallet-address-3"] = true -- Add pruned delegate back to the allowlist + + local expectation = { + amountWithdrawn = withdrawalAmount, + delegatePruned = true, + expeditedWithdrawalFee = expeditedWithdrawalFee, + gatewayTotalDelegatedStake = 0, + penaltyRate = constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE, + updatedDelegate = { + delegatedStake = 0, + startTimestamp = 0, + vaults = {}, + }, + } + local status, result = pcall( + gar.decreaseDelegateStake, + stubGatewayAddress, + stubRandomAddress, + decreaseAmount, + startTimestamp, + stubMessageId, + true -- Instant withdrawal flag + ) assert.is_true(status) - assert.are.same(result.gateway.delegates[stubRandomAddress].delegatedStake, minDelegatedStake) - assert.are.equal(result.gateway.totalDelegatedStake, minDelegatedStake) - assert.are.equal(withdrawalAmount, result.amountWithdrawn) + assert.are.same(expectation, result) + assert.are.same(expectedGateway, _G.GatewayRegistry[stubGatewayAddress]) assert.are.equal(withdrawalAmount, _G.Balances[stubRandomAddress]) - assert.are.equal(expeditedWithdrawalFee, result.expeditedWithdrawalFee) assert.are.equal(expeditedWithdrawalFee, _G.Balances[ao.id]) - assert.are.equal(constants.MAX_EXPEDITED_WITHDRAWAL_PENALTY_RATE, result.penaltyRate) - assert.are.equal(minDelegatedStake, _G.GatewayRegistry[stubGatewayAddress].totalDelegatedStake) end) - it("should error if the remaining delegate stake is less than the minimum stake", function() + it("should error if the remaining delegate stake is less than the minimum stake and greater than 0", function() local delegatedStake = minDelegatedStake _G.GatewayRegistry[stubGatewayAddress] = { operatorStake = minOperatorStake, - totalDelegatedStake = minDelegatedStake - 1, + totalDelegatedStake = minDelegatedStake, vaults = {}, delegates = { [stubRandomAddress] = { diff --git a/src/gar.lua b/src/gar.lua index 68269090..c56133f0 100644 --- a/src/gar.lua +++ b/src/gar.lua @@ -494,6 +494,7 @@ end --- @param gateway Gateway --- @param quantity mARIO --- @param ban boolean|nil do not add the delegate back to the gateway allowlist if their delegation is over +--- @return Delegate, boolean # a copy of the updated delegate and whether or not it was pruned function decreaseDelegateStakeAtGateway(delegateAddress, gateway, quantity, ban) local delegate = gateway.delegates[delegateAddress] assert(delegate, "Delegate is required") @@ -505,10 +506,11 @@ function decreaseDelegateStakeAtGateway(delegateAddress, gateway, quantity, ban) assert(gateway, "Gateway is required") delegate.delegatedStake = delegate.delegatedStake - quantity gateway.totalDelegatedStake = gateway.totalDelegatedStake - quantity - gar.pruneDelegateFromGatewayIfNecessary(delegateAddress, gateway) + local pruned = gar.pruneDelegateFromGatewayIfNecessary(delegateAddress, gateway) if ban and gateway.settings.allowedDelegatesLookup then gateway.settings.allowedDelegatesLookup[delegateAddress] = nil end + return utils.deepCopy(delegate), pruned end --- Creates a delegate at a gateway, managing allowlisting accounting if necessary @@ -615,6 +617,21 @@ function gar.getSettingsUnsafe() return GatewayRegistrySettings end +--- @class DecreaseDelegateStakeReturn +--- @field gatewayTotalDelegatedStake mARIO The updated amount of total delegated stake at the gateway +--- @field updatedDelegate Delegate The updated delegate object +--- @field delegatePruned boolean Whether or not the delegate was pruned from the gateway +--- @field penaltyRate number The penalty rate for the expedited withdrawal, if applicable +--- @field expeditedWithdrawalFee number The fee deducted from the stake for the expedited withdrawal, if applicable +--- @field amountWithdrawn number The amount of stake withdrawn after any penalty fee is deducted + +--- @param gatewayAddress WalletAddress The address of the gateway from which to decrease delegated stake +--- @param delegator WalletAddress The address of the delegator for which to decrease delegated stake +--- @param qty mARIO The amount of delegated stake to decrease +--- @param currentTimestamp Timestamp The current timestamp +--- @param messageId MessageId The message ID of the current action +--- @param instantWithdraw boolean Whether to withdraw the stake instantly; otherwise allow it to be vaulted +--- @return DecreaseDelegateStakeReturn # Details about the outcome of the operation function gar.decreaseDelegateStake(gatewayAddress, delegator, qty, currentTimestamp, messageId, instantWithdraw) assert(type(qty) == "number", "Quantity is required and must be a number") assert(qty > 0, "Quantity must be greater than 0") @@ -624,9 +641,10 @@ function gar.decreaseDelegateStake(gatewayAddress, delegator, qty, currentTimest assert(gateway, "Gateway not found") assert(gateway.status ~= "leaving", "Gateway is leaving the network and cannot withdraw more stake.") - assert(gateway.delegates[delegator], "This delegate is not staked at this gateway.") + local delegate = gateway.delegates[delegator] + assert(delegate, "This delegate is not staked at this gateway.") - local existingStake = gateway.delegates[delegator].delegatedStake + local existingStake = delegate.delegatedStake local requiredMinimumStake = gateway.settings.minDelegatedStake local maxAllowedToWithdraw = existingStake - requiredMinimumStake assert( @@ -645,12 +663,14 @@ function gar.decreaseDelegateStake(gatewayAddress, delegator, qty, currentTimest else createDelegateWithdrawVault(gateway, delegator, messageId, qty, currentTimestamp) end - decreaseDelegateStakeAtGateway(delegator, gateway, qty) + local updatedDelegate, pruned = decreaseDelegateStakeAtGateway(delegator, gateway, qty) -- update the gateway GatewayRegistry[gatewayAddress] = gateway return { - gateway = gateway, + gatewayTotalDelegatedStake = gateway.totalDelegatedStake, + updatedDelegate = updatedDelegate, + delegatePruned = pruned, penaltyRate = penaltyRate, expeditedWithdrawalFee = expeditedWithdrawalFee, amountWithdrawn = amountToWithdraw, @@ -1219,16 +1239,20 @@ end --- Preserves delegate's position in allow list upon removal from gateway --- @param delegateAddress string The address of the delegator --- @param gateway table The gateway from which the delegate is being removed +--- @return boolean # Whether or not the delegate was pruned function gar.pruneDelegateFromGatewayIfNecessary(delegateAddress, gateway) + local pruned = false local delegate = gateway.delegates[delegateAddress] if delegate.delegatedStake == 0 and utils.lengthOfTable(delegate.vaults) == 0 then gateway.delegates[delegateAddress] = nil + pruned = true -- replace the delegate in the allowedDelegatesLookup table if necessary if gateway.settings.allowedDelegatesLookup then gateway.settings.allowedDelegatesLookup[delegateAddress] = true end end + return pruned end --- Add delegate addresses to the allowedDelegatesLookup table in the gateway's settings @@ -1777,10 +1801,10 @@ end --- @field vaultId string | nil # The vault id to redelegate from (optional) --- @class RedelegateStakeResult ---- @field sourceGateway table # The updated gateway object that the stake was moved from ---- @field targetGateway table # The updated gateway object that the stake was moved to +--- @field sourceAddress WalletAddress # The address of the gateway that the stake was moved from +--- @field targetAddress table # The address of the gateway that the stake was moved to --- @field redelegationFee number # The fee charged for the redelegation ---- @field feeResetTimestamp number # The timestamp when the reldelegation fee will be reset +--- @field feeResetTimestamp number # The timestamp when the redelegation fee will be reset --- @field redelegationsSinceFeeReset number # The number of redelegations the user has made since the last fee reset --- Take stake from a delegate and stake it to a new delegate. diff --git a/src/main.lua b/src/main.lua index d1923c1b..ff8717b8 100644 --- a/src/main.lua +++ b/src/main.lua @@ -1386,7 +1386,6 @@ addEventingHandler( local result = gar.decreaseDelegateStake(target, msg.From, quantity, msg.Timestamp, msg.Id, instantWithdraw) local decreaseDelegateStakeResult = { - gateway = result and result.gateway or {}, penaltyRate = result and result.penaltyRate or 0, expeditedWithdrawalFee = result and result.expeditedWithdrawalFee or 0, amountWithdrawn = result and result.amountWithdrawn or 0, @@ -1394,13 +1393,11 @@ addEventingHandler( msg.ioEvent:addField("Sender-New-Balance", Balances[msg.From]) -- should be unchanged - local delegateResult = {} - if result ~= nil and result.gateway ~= nil then - local gateway = result.gateway - local newStake = gateway.delegates[msg.From].delegatedStake + if result ~= nil then + local newStake = result.updatedDelegate.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("Gateway-Total-Delegated-Stake", result.gatewayTotalDelegatedStake) if instantWithdraw then msg.ioEvent:addField("Instant-Withdrawal", instantWithdraw) @@ -1409,8 +1406,7 @@ addEventingHandler( msg.ioEvent:addField("Penalty-Rate", result.penaltyRate) end - delegateResult = gateway.delegates[msg.From] - local newDelegateVaults = delegateResult.vaults + local newDelegateVaults = result.updatedDelegate.vaults if newDelegateVaults ~= nil then msg.ioEvent:addField("Vaults-Count", utils.lengthOfTable(newDelegateVaults)) local newDelegateVault = newDelegateVaults[msg.Id] @@ -1440,7 +1436,7 @@ addEventingHandler( ["Expedited-Withdrawal-Fee"] = tostring(decreaseDelegateStakeResult.expeditedWithdrawalFee), ["Amount-Withdrawn"] = tostring(decreaseDelegateStakeResult.amountWithdrawn), }, - Data = json.encode(delegateResult), + Data = json.encode(result and result.updatedDelegate or {}), }) end ) @@ -2282,24 +2278,29 @@ addEventingHandler(ActionMap.RedelegateStake, utils.hasMatchingTag("Action", Act local isStakeMovingFromDelegateToOperator = delegateAddress == targetAddress local isStakeMovingFromOperatorToDelegate = delegateAddress == sourceAddress local isStakeMovingFromWithdrawal = vaultId ~= nil + if isStakeMovingFromWithdrawal then + LastKnownWithdrawSupply = LastKnownWithdrawSupply - quantity + end if isStakeMovingFromDelegateToOperator then - if isStakeMovingFromWithdrawal then - LastKnownWithdrawSupply = LastKnownWithdrawSupply - stakeMoved - else - LastKnownDelegatedSupply = LastKnownDelegatedSupply - stakeMoved + if not isStakeMovingFromWithdrawal then + LastKnownDelegatedSupply = LastKnownDelegatedSupply - quantity end LastKnownStakedSupply = LastKnownStakedSupply + stakeMoved elseif isStakeMovingFromOperatorToDelegate then - if isStakeMovingFromWithdrawal then - LastKnownWithdrawSupply = LastKnownWithdrawSupply + stakeMoved - else - LastKnownStakedSupply = LastKnownStakedSupply - stakeMoved + if not isStakeMovingFromWithdrawal then + LastKnownStakedSupply = LastKnownStakedSupply - quantity end LastKnownDelegatedSupply = LastKnownDelegatedSupply + stakeMoved + elseif isStakeMovingFromWithdrawal then + LastKnownStakedSupply = LastKnownStakedSupply + stakeMoved + -- else + -- Stake is simply moving from one delegation to another end - LastKnownCirculatingSupply = LastKnownCirculatingSupply - redelegationResult.redelegationFee + if redelegationFee > 0 then + msg.ioEvent:addField("Redelegation-Fee", redelegationFee) + end addSupplyData(msg.ioEvent) Send(msg, { diff --git a/tests/gar.test.mjs b/tests/gar.test.mjs index 380b687d..d440cafc 100644 --- a/tests/gar.test.mjs +++ b/tests/gar.test.mjs @@ -922,10 +922,12 @@ describe('GatewayRegistry', async () => { }); describe('Decrease-Delegate-Stake', () => { - it('should allow withdrawing a delegated stake from a gateway', async () => { + async function decreaseDelegateStakeTest({ + stakeQty, + decreaseQty, + instant = false, + }) { const decreaseStakeTimestamp = STUB_TIMESTAMP + 1000 * 60 * 15; // 15 minutes after stubbedTimestamp - const stakeQty = 10000000000; - const decreaseQty = stakeQty / 2; const decreaseStakeMsgId = 'decrease-stake-message-id-'.padEnd(43, 'x'); const { memory: delegatedStakeMemory } = await delegateStake({ delegatorAddress, @@ -939,14 +941,81 @@ describe('GatewayRegistry', async () => { memory: delegatedStakeMemory, timestamp: STUB_TIMESTAMP, }); - const { memory: decreaseStakeMemory } = await decreaseDelegateStake({ + const { + result: decreaseDelegateStakeResult, + memory: decreaseStakeMemory, + } = await decreaseDelegateStake({ memory: delegatedStakeMemory, delegatorAddress, decreaseQty, timestamp: decreaseStakeTimestamp, gatewayAddress: STUB_ADDRESS, messageId: decreaseStakeMsgId, + instant, }); + + if (instant) { + assert.equal(decreaseDelegateStakeResult.Messages.length, 1); + decreaseDelegateStakeResult.Messages[0].Tags.sort((a, b) => + a.name.localeCompare(b.name), + ); + assert.deepStrictEqual(decreaseDelegateStakeResult.Messages[0], { + Data: `{"vaults":[],"startTimestamp":21600000,"delegatedStake":${stakeQty - decreaseQty}}`, + Target: delegatorAddress, + Anchor: '00000000000000000000000000000008', + Tags: [ + { + name: 'Action', + value: 'Decrease-Delegate-Stake-Notice', + }, + { + name: 'Address', + value: STUB_ADDRESS, + }, + { + name: 'Amount-Withdrawn', + value: `${decreaseQty / 2}`, + }, + { + name: 'Data-Protocol', + value: 'ao', + }, + { + name: 'Expedited-Withdrawal-Fee', + value: `${decreaseQty / 2}`, + }, + { + name: 'From-Module', + value: '', + }, + { + name: 'From-Process', + value: PROCESS_ID, + }, + { + name: 'Penalty-Rate', + value: '0.5', + }, + { + name: 'Quantity', + value: decreaseQty, + }, + { + name: 'Ref_', + value: '8', + }, + { + name: 'Type', + value: 'Message', + }, + { + name: 'Variant', + value: 'ao.TN.1', + }, + ], + }); + } + // get the gateway record const gatewayAfter = await getGateway({ address: STUB_ADDRESS, @@ -963,6 +1032,34 @@ describe('GatewayRegistry', async () => { timestamp: decreaseStakeTimestamp, }); + const delegationsForDelegator = await getDelegations({ + memory: decreaseStakeMemory, + address: delegatorAddress, + timestamp: decreaseStakeTimestamp, + }); + + return { + gatewayAfter, + delegateItems, + delegationsForDelegator, + decreaseStakeMemory, + decreaseStakeTimestamp, + }; + } + + it('should allow decreasing a delegated stake from a gateway', async () => { + const stakeQty = 10000000000; + const decreaseQty = stakeQty / 2; + const { + delegateItems, + delegationsForDelegator, + decreaseStakeMemory, + decreaseStakeTimestamp, + } = await decreaseDelegateStakeTest({ + stakeQty, + decreaseQty, + }); + assert.deepStrictEqual(delegateItems, [ { startTimestamp: STUB_TIMESTAMP, @@ -974,11 +1071,6 @@ describe('GatewayRegistry', async () => { const expectedEndTimestamp = 90 * 24 * 60 * 60 * 1000 + decreaseStakeTimestamp; // check the vault was created and delegation still exists - const delegationsForDelegator = await getDelegations({ - memory: decreaseStakeMemory, - address: delegatorAddress, - timestamp: decreaseStakeTimestamp, - }); assert.deepStrictEqual(delegationsForDelegator.items, [ { balance: decreaseQty, @@ -987,7 +1079,7 @@ describe('GatewayRegistry', async () => { endTimestamp: expectedEndTimestamp, delegationId: expectedDelegateId, type: 'vault', - vaultId: decreaseStakeMsgId, + vaultId: 'decrease-stake-message-id-'.padEnd(43, 'x'), }, { balance: stakeQty - decreaseQty, @@ -1001,6 +1093,62 @@ describe('GatewayRegistry', async () => { lastTimestamp = decreaseStakeTimestamp; }); + it('should allow partially withdrawing a delegated stake from a gateway', async () => { + const stakeQty = 10000000000; + const decreaseQty = stakeQty / 2; + const { + delegateItems, + delegationsForDelegator, + decreaseStakeMemory, + decreaseStakeTimestamp, + } = await decreaseDelegateStakeTest({ + stakeQty, + decreaseQty, + instant: true, + }); + + assert.deepStrictEqual(delegateItems, [ + { + startTimestamp: STUB_TIMESTAMP, + delegatedStake: stakeQty - decreaseQty, + address: delegatorAddress, + }, + ]); + // check that no vault was created and delegation still exists + assert.deepStrictEqual(delegationsForDelegator.items, [ + { + balance: stakeQty - decreaseQty, + gatewayAddress: STUB_ADDRESS, + startTimestamp: STUB_TIMESTAMP, + delegationId: `${STUB_ADDRESS}_${STUB_TIMESTAMP}`, + type: 'stake', + }, + ]); + sharedMemory = decreaseStakeMemory; + lastTimestamp = decreaseStakeTimestamp; + }); + + it('should allow fully withdrawing a delegated stake from a gateway', async () => { + const stakeQty = 10000000000; + const decreaseQty = stakeQty; + const { + delegateItems, + delegationsForDelegator, + decreaseStakeMemory, + decreaseStakeTimestamp, + } = await decreaseDelegateStakeTest({ + stakeQty, + decreaseQty, + instant: true, + }); + + // Ensure delegation no longer exists + assert.deepStrictEqual(delegateItems, []); + assert.deepStrictEqual(delegationsForDelegator.items, []); + sharedMemory = decreaseStakeMemory; + lastTimestamp = decreaseStakeTimestamp; + }); + it('should fail to withdraw a delegated stake if below the minimum withdrawal limitation', async () => { const decreaseStakeTimestamp = STUB_TIMESTAMP + 1000 * 60 * 15; // 15 minutes after stubbedTimestamp const stakeQty = 10000000000; @@ -1597,6 +1745,7 @@ describe('GatewayRegistry', async () => { }, memory, }); + assertNoResultError(result); return { result, memory: result.Memory, @@ -1625,13 +1774,15 @@ describe('GatewayRegistry', async () => { memory: joinTargetMemory, }); - const { memory: delegatedStakeMemory } = await delegateStake({ - delegatorAddress, - quantity: stakeQty, - gatewayAddress: sourceAddress, - timestamp: STUB_TIMESTAMP, - memory: transferMemory, - }); + const { result: delegateStakeResult, memory: delegatedStakeMemory } = + await delegateStake({ + delegatorAddress, + quantity: stakeQty, + gatewayAddress: sourceAddress, + timestamp: STUB_TIMESTAMP, + memory: transferMemory, + }); + assertNoResultError(delegateStakeResult); const sourceGatewayBefore = await getGateway({ address: sourceAddress, @@ -1780,14 +1931,16 @@ describe('GatewayRegistry', async () => { ); const decreaseStakeMsgId = 'decrease-stake-message-id-'.padEnd(43, 'x'); - const { memory: decreaseStakeMemory } = await decreaseDelegateStake({ - memory: delegatedStakeMemory, - timestamp: STUB_TIMESTAMP + 1, - delegatorAddress, - decreaseQty: stakeQty, - gatewayAddress: sourceAddress, - messageId: decreaseStakeMsgId, - }); + const { result: decreaseStakeResult, memory: decreaseStakeMemory } = + await decreaseDelegateStake({ + memory: delegatedStakeMemory, + timestamp: STUB_TIMESTAMP + 1, + delegatorAddress, + decreaseQty: stakeQty, + gatewayAddress: sourceAddress, + messageId: decreaseStakeMsgId, + }); + assertNoResultError(decreaseStakeResult); const { memory: redelegateStakeMemory } = await redelegateStake({ memory: decreaseStakeMemory,