diff --git a/contract/spec/arns_spec.lua b/contract/spec/arns_spec.lua index 07a127fe..71f64389 100644 --- a/contract/spec/arns_spec.lua +++ b/contract/spec/arns_spec.lua @@ -10,6 +10,7 @@ describe("arns", function() before_each(function() arns.records = {} arns.reserved = {} + arns.fees = constants.genesisFees token.balances = { Bob = 5000000, } @@ -42,6 +43,33 @@ describe("arns", function() }, token.balances) end) + it('defaults to 1 year and lease when not provided', function () + local status, result = pcall(arns.buyRecord, "test-name", nil, nil, "Bob", false, timestamp, testProcessId) + assert.is_true(status) + assert.are.same({ + purchasePrice = 1500, + type = "lease", + undernameCount = 10, + processId = testProcessId, + startTimestamp = 0, + endTimestamp = timestamp + constants.MS_IN_A_YEAR, + }, result) + assert.are.same({ + ["test-name"] = { + purchasePrice = 1500, + type = "lease", + undernameCount = 10, + processId = testProcessId, + startTimestamp = 0, + endTimestamp = timestamp + constants.MS_IN_A_YEAR, + }, + }, arns.records) + assert.are.same({ + ["Bob"] = 4998500, + [_G.ao.id] = 1500, + }, token.balances) + end) + it( "should validate a permabuy request and add the record to global state and deduct balance from caller", function() @@ -170,5 +198,37 @@ describe("arns", function() assert.is_false(status) assert.match("Name is not registered", error) end) + + it('should allow extension for existing lease up to 5 years', function() + arns.records["test-name"] = { + -- 1 year lease + endTimestamp = timestamp + constants.MS_IN_A_YEAR, + processId = testProcessId, + purchasePrice = 1500, + startTimestamp = 0, + type = "lease", + undernameCount = 10, + } + local status, result = pcall(arns.extendLease, "Bob", "test-name", 4, timestamp) + assert.is_true(status) + assert.are.same({ + endTimestamp = timestamp + constants.MS_IN_A_YEAR * 5, + processId = testProcessId, + purchasePrice = 1500, + startTimestamp = 0, + type = "lease", + undernameCount = 10, + }, result) + assert.are.same({ + ["test-name"] = { + endTimestamp = timestamp + constants.MS_IN_A_YEAR * 5, + processId = testProcessId, + purchasePrice = 1500, + startTimestamp = 0, + type = "lease", + undernameCount = 10, + }, + }, arns.records) + end) end) end) diff --git a/contract/src/arns.lua b/contract/src/arns.lua index f26fdcd1..5376d2fc 100644 --- a/contract/src/arns.lua +++ b/contract/src/arns.lua @@ -2,10 +2,12 @@ local utils = require("utils") local constants = require("constants") local token = Token or require("token") +local demand = Demand or require("demand") local arns = { reserved = {}, records = {}, auctions = {}, + fees = constants.genesisFees, } function arns.buyRecord(name, purchaseType, years, from, auction, timestamp, processId) -- don't catch, let the caller handle the error @@ -19,7 +21,9 @@ function arns.buyRecord(name, purchaseType, years, from, auction, timestamp, pro years = 1 -- set to 1 year by default end - local totalRegistrationFee = utils.calculateRegistrationFee(purchaseType, name, years) + local baseRegistrionFee = arns.fees[#name] + + local totalRegistrationFee = utils.calculateRegistrationFee(purchaseType, baseRegistrionFee, years, demand.getDemandFactor()) if token.getBalance(from) < totalRegistrationFee then error("Insufficient funds") @@ -66,13 +70,20 @@ function arns.extendLease(from, name, years, timestamp) local record = arns.getRecord(name) -- throw error if invalid utils.assertValidExtendLease(record, timestamp, years) - - local totalExtensionFee = utils.calculateExtensionFee(name, years, record.type) + local baseRegistrionFee = arns.fees[#name] + local totalExtensionFee = utils.calculateExtensionFee(baseRegistrionFee, years, demand.getDemandFactor()) -- Transfer tokens to the protocol balance token.transfer(ao.id, from, totalExtensionFee) arns.records[name].endTimestamp = record.endTimestamp + constants.MS_IN_A_YEAR * years - return Records[name] + return arns.records[name] +end + +function arns.calculateExtensionFee(name, years, purchaseType) + local record = arns.getRecord(name) + local yearsRemaining = utils.calculateYearsBetweenTimestamps(record.endTimestamp, timestamp) + local extensionFee = utils.calculateUndernameCost(name, years, purchaseType, yearsRemaining) + return extensionFee end function arns.increaseUndernameCount(from, name, qty, timestamp) @@ -93,7 +104,8 @@ function arns.increaseUndernameCount(from, name, qty, timestamp) end local existingUndernames = record.undernameCount - local additionalUndernameCost = utils.calculateUndernameCost(name, qty, record.type, yearsRemaining) + local baseRegistrionFee = arns.fees[#name] + local additionalUndernameCost = utils.calculateUndernameCost(baseRegistrionFee, qty, record.type, yearsRemaining, demand.getDemandFactor()) -- Transfer tokens to the protocol balance token.transfer(ao.id, from, additionalUndernameCost) diff --git a/contract/src/constants.lua b/contract/src/constants.lua index c0ee2d5f..8e056682 100644 --- a/contract/src/constants.lua +++ b/contract/src/constants.lua @@ -39,7 +39,8 @@ constants.ARNS_MAX_UNDERNAME_MESSAGE = "Name has reached undername limit of 1000 constants.MAX_ALLOWED_UNDERNAMES = 10000 constants.UNDERNAME_LEASE_FEE_PERCENTAGE = 0.001 constants.UNDERNAME_PERMABUY_FEE_PERCENTAGE = 0.005 -constants.MS_IN_GRACE_PERIOD = 3 * 7 * 24 * 60 * 60 * 1000 +constants.gracePeriodMs = 3 * 7 * 24 * 60 * 60 * 1000 +constants.maxLeaseLengthYears = 5 -- DEMAND constants.DEMAND_SETTINGS = { diff --git a/contract/src/utils.lua b/contract/src/utils.lua index 79836caa..6dd14344 100644 --- a/contract/src/utils.lua +++ b/contract/src/utils.lua @@ -185,54 +185,32 @@ function utils.walletHasSufficientBalance(wallet, quantity) return Balances[wallet] ~= nil and Balances[wallet] >= quantity end -function utils.calculateLeaseFee(name, years) - -- Initial cost to register a name - -- TODO: Harden the types here to make fees[name.length] an error - local initialNamePurchaseFee = constants.genesisFees[string.len(name)] - - -- total cost to purchase name (no demand factor) - return (initialNamePurchaseFee + utils.calculateAnnualRenewalFee(name, years)) +function utils.calculateLeaseFee(baseFee, years, demandFactor) + local annualRegistrionFee = utils.calculateAnnualRenewalFee(baseFee, years) + local totalLeaseCost = baseFee + annualRegistrionFee + return demandFactor * totalLeaseCost end -function utils.calculateAnnualRenewalFee(name, years) - -- Determine annual registration price of name - local initialNamePurchaseFee = constants.genesisFees[string.len(name)] - - -- Annual fee is specific % of initial purchase cost - local nameAnnualRegistrationFee = initialNamePurchaseFee * constants.ANNUAL_PERCENTAGE_FEE - +function utils.calculateAnnualRenewalFee(baseFee, years) + local nameAnnualRegistrationFee = baseFee * constants.ANNUAL_PERCENTAGE_FEE local totalAnnualRenewalCost = nameAnnualRegistrationFee * years - return totalAnnualRenewalCost end -function utils.calculatePermabuyFee(name) - -- genesis price - local initialNamePurchaseFee = constants.genesisFees[string.len(name)] - - -- calculate the annual fee for the name for default of 10 years - local permabuyPrice = - -- No demand factor - initialNamePurchaseFee -- total renewal cost pegged to 10 years to purchase name - + utils.calculateAnnualRenewalFee(name, constants.PERMABUY_LEASE_FEE_LENGTH) - return permabuyPrice +function utils.calculatePermabuyFee(baseFee, demandFactor) + local permabuyPrice = baseFee + utils.calculateAnnualRenewalFee(baseFee, constants.PERMABUY_LEASE_FEE_LENGTH) + return demandFactor * permabuyPrice end -function utils.calculateRegistrationFee(purchaseType, name, years) +function utils.calculateRegistrationFee(purchaseType, baseFee, years, demandFactor) if purchaseType == "lease" then - return utils.calculateLeaseFee(name, years) + return utils.calculateLeaseFee(baseFee, years, demandFactor) elseif purchaseType == "permabuy" then - return utils.calculatePermabuyFee(name) + return utils.calculatePermabuyFee(baseFee, demandFactor) end end -function utils.calculateUndernameCost(name, increaseQty, registrationType, years) - local initialNameFee = constants.genesisFees[string.len(name)] -- Get the fee based on the length of the name - if initialNameFee == nil then - -- Handle the case where there is no fee for the given name length - return 0 - end - +function utils.calculateUndernameCost(baseFee, increaseQty, registrationType, years, demandFactor) local undernamePercentageFee = 0 if registrationType == "lease" then undernamePercentageFee = constants.UNDERNAME_LEASE_FEE_PERCENTAGE @@ -240,8 +218,13 @@ function utils.calculateUndernameCost(name, increaseQty, registrationType, years undernamePercentageFee = constants.UNDERNAME_PERMABUY_FEE_PERCENTAGE end - local totalFeeForQtyAndYears = initialNameFee * undernamePercentageFee * increaseQty * years - return totalFeeForQtyAndYears + local totalFeeForQtyAndYears = baseFee * undernamePercentageFee * increaseQty * years + return demandFactor * totalFeeForQtyAndYears +end + +function utils.calculateExtensionFee(baseFee, years, demandFactor) + local extensionFee = utils.calculateAnnualRenewalFee(baseFee, years) + return demandFactor * extensionFee end function utils.isLeaseRecord(record) @@ -265,7 +248,7 @@ function utils.isNameInGracePeriod(record, currentTimestamp) if not record or not record.endTimestamp then return false end -- if it has no timestamp, it is a permabuy - if (utils.ensureMilliseconds(record.endTimestamp) + constants.MS_IN_GRACE_PERIOD) < currentTimestamp then + if (utils.ensureMilliseconds(record.endTimestamp) + constants.gracePeriodMs) < currentTimestamp then return false end return true @@ -348,8 +331,8 @@ end function utils.assertValidExtendLease(record, currentTimestamp, years) utils.assertAllowedNameModification(record, currentTimestamp) - - if years > utils.getMaxAllowedYearsExtensionForRecord(record, currentTimestamp) then + local maxAllowedYears = utils.getMaxAllowedYearsExtensionForRecord(record, currentTimestamp) + if years > maxAllowedYears then error("Invalid number of years for record extension") end end @@ -360,12 +343,12 @@ function utils.getMaxAllowedYearsExtensionForRecord(record, currentTimestamp) end -- if expired return 0 because it cannot be extended and must be re-bought - if currentTimestamp > record.endTimestamp + constants.SECONDS_IN_GRACE_PERIOD then + if currentTimestamp > (record.endTimestamp + constants.gracePeriodMs) then return 0 end if utils.isNameInGracePeriod(record, currentTimestamp) then - return constants.ARNS_LEASE_LENGTH_MAX_YEARS + return constants.maxLeaseLengthYears end -- TODO: should we put this as the ceiling? or should we allow people to extend as soon as it is purchased