Skip to content

Commit

Permalink
feat: Add a protocolFee to the unstake action (#96)
Browse files Browse the repository at this point in the history
  • Loading branch information
Whytecrowe authored Jan 29, 2024
2 parents 213334f + dc90e07 commit 53df710
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 162 deletions.
4 changes: 3 additions & 1 deletion contracts/registrar/ZNSRootRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,9 @@ contract ZNSRootRegistrar is
bool stakeRefunded = false;
// send the stake back if it exists
if (stakedAmount > 0) {
treasury.unstakeForDomain(domainHash, owner);
uint256 protocolFee = rootPricer.getFeeForPrice(0x0, stakedAmount);

treasury.unstakeForDomain(domainHash, owner, protocolFee);
stakeRefunded = true;
}

Expand Down
6 changes: 5 additions & 1 deletion contracts/treasury/IZNSTreasury.sol
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ interface IZNSTreasury {
uint256 protocolFee
) external;

function unstakeForDomain(bytes32 domainHash, address owner) external;
function unstakeForDomain(
bytes32 domainHash,
address owner,
uint256 protocolFee
) external;

function processDirectPayment(
bytes32 parentHash,
Expand Down
12 changes: 11 additions & 1 deletion contracts/treasury/ZNSTreasury.sol
Original file line number Diff line number Diff line change
Expand Up @@ -156,14 +156,24 @@ contract ZNSTreasury is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNS
* Since we are clearing storage, gas refund from this operation makes Revoke transactions cheaper.
* @param domainHash The hash of the domain for which the stake is being withdrawn.
* @param owner The address of the user who is withdrawing the stake.
* @param protocolFee The protocol fee paid by the user to Zero.
*/
function unstakeForDomain(
bytes32 domainHash,
address owner
address owner,
uint256 protocolFee
) external override onlyRegistrar {
Stake memory stakeData = stakedForDomain[domainHash];
delete stakedForDomain[domainHash];

if (protocolFee > 0) {
stakeData.token.safeTransferFrom(
owner,
paymentConfigs[0x0].beneficiary,
protocolFee
);
}

stakeData.token.safeTransfer(owner, stakeData.amount);

emit StakeWithdrawn(
Expand Down
43 changes: 39 additions & 4 deletions test/ZNSRootRegistrar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { IDistributionConfig } from "./helpers/types";
import * as ethers from "ethers";
import { defaultRootRegistration } from "./helpers/register-setup";
import { checkBalance } from "./helpers/balances";
import { getPriceObject } from "./helpers/pricing";
import { getPriceObject, getStakingOrProtocolFee } from "./helpers/pricing";
import { getDomainHashFromEvent } from "./helpers/events";
import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types";
import { ADMIN_ROLE, GOVERNOR_ROLE } from "../src/deploy/constants";
Expand Down Expand Up @@ -92,7 +92,7 @@ describe("ZNSRootRegistrar", () => {
ethers.MaxUint256
);

userBalanceInitial = ethers.parseEther("100000000000");
userBalanceInitial = ethers.parseEther("1000000000000000000");
// Give funds to user
await zns.meowToken.connect(user).approve(await zns.treasury.getAddress(), ethers.MaxUint256);
await zns.meowToken.mint(user.address, userBalanceInitial);
Expand Down Expand Up @@ -984,14 +984,46 @@ describe("ZNSRootRegistrar", () => {
expect(finalstaked).to.equal(BigInt("0"));
expect(finalToken).to.equal(ethers.ZeroAddress);

const protocolFee = getStakingOrProtocolFee(staked);

// Verify final balances
const computedFinalBalance = balance + staked;
const computedFinalBalance = balance + staked - protocolFee;
const finalBalance = await zns.meowToken.balanceOf(user.address);
expect(computedFinalBalance).to.equal(finalBalance);
});
});

describe("Revoking Domains", () => {
it("Charges a protocol fee to the owner as part of the revoke flow", async () => {
await defaultRootRegistration({
user,
zns,
domainName: defaultDomain,
distrConfig: {
pricerContract: await zns.curvePricer.getAddress(),
paymentType: PaymentType.STAKE,
accessType: AccessType.OPEN,
},
});

const domainHash = await getDomainHashFromEvent({
zns,
user,
});

const price = await zns.curvePricer.getPrice(ethers.ZeroHash, defaultDomain, false);
const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price);

const balanceBefore = await zns.meowToken.balanceOf(user.address);

// is revoke meant to be free if owner of parent? register subdomain is
await zns.rootRegistrar.connect(user).revokeDomain(domainHash);

const balanceAfter = await zns.meowToken.balanceOf(user.address);

expect(balanceAfter).to.eq(balanceBefore + price - protocolFee);
});

it("Revokes a Top level Domain, locks distribution and removes mintlist", async () => {
// Register Top level
await defaultRootRegistration({
Expand Down Expand Up @@ -1095,17 +1127,20 @@ describe("ZNSRootRegistrar", () => {
// Revoke the domain
await zns.rootRegistrar.connect(user).revokeDomain(domainHash);


// Validated funds are unstaked
const { amount: finalstaked, token: finalToken } = await zns.treasury.stakedForDomain(domainHash);
expect(finalstaked).to.equal(BigInt("0"));
expect(finalToken).to.equal(ethers.ZeroAddress);

const protocolFee = getStakingOrProtocolFee(staked);

// Verify final balances
const computedBalanceAfterStaking = balanceAfterStaking + staked;
const balanceMinusFee = balance - expectedStakeFee;
expect(computedBalanceAfterStaking).to.equal(balanceMinusFee);
const finalBalance = await zns.meowToken.balanceOf(user.address);
expect(computedBalanceAfterStaking).to.equal(finalBalance);
expect(computedBalanceAfterStaking - protocolFee).to.equal(finalBalance);
});

it("Cannot revoke if Name is owned by another user", async () => {
Expand Down
80 changes: 62 additions & 18 deletions test/ZNSSubRegistrar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,11 @@ describe("ZNSSubRegistrar", () => {
const parentBalBefore = await zns.meowToken.balanceOf(lvl4SubOwner.address);
const paymentContractBalBefore = await zns.meowToken.balanceOf(await zns.treasury.getAddress());

const stake = await zns.treasury.stakedForDomain(domainHash);
const protocolFee = getStakingOrProtocolFee(stake.amount);

await zns.meowToken.connect(lvl5SubOwner).approve(await zns.treasury.getAddress(), protocolFee);

await zns.rootRegistrar.connect(lvl5SubOwner).revokeDomain(domainHash);

const userBalAfter = await zns.meowToken.balanceOf(lvl5SubOwner.address);
Expand All @@ -977,7 +982,7 @@ describe("ZNSSubRegistrar", () => {
expect(
userBalAfter - userBalanceBefore
).to.eq(
expectedPrice
expectedPrice - protocolFee
);
expect(
parentBalBefore - parentBalAfter
Expand Down Expand Up @@ -1080,6 +1085,11 @@ describe("ZNSSubRegistrar", () => {
const childExists = await zns.registry.exists(lvl3Hash);
assert.ok(childExists);

const stake = await zns.treasury.stakedForDomain(lvl2Hash);
const protocolFee = getStakingOrProtocolFee(stake.amount);

await zns.meowToken.connect(lvl2SubOwner).approve(await zns.treasury.getAddress(), protocolFee);

// revoke parent
await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain(
lvl2Hash,
Expand Down Expand Up @@ -1126,14 +1136,19 @@ describe("ZNSSubRegistrar", () => {

const userBalBefore = await zns.meowToken.balanceOf(lvl3SubOwner.address);

const subStake = await zns.treasury.stakedForDomain(lvl3Hash);
const subProtocolFee = getStakingOrProtocolFee(subStake.amount);

await zns.meowToken.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), subProtocolFee);

// revoke child
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
lvl3Hash,
);

const userBalAfter = await zns.meowToken.balanceOf(lvl3SubOwner.address);

expect(userBalAfter - userBalBefore).to.eq(expectedPrice);
expect(userBalAfter - userBalBefore).to.eq(expectedPrice - subProtocolFee);

const childExistsAfter = await zns.registry.exists(lvl3Hash);
assert.ok(!childExistsAfter);
Expand Down Expand Up @@ -1479,7 +1494,7 @@ describe("ZNSSubRegistrar", () => {
);

// send future child some tokens
await token5.connect(deployer).transfer(lvl3SubOwner.address, expectedPrice + stakeFee + protocolFee);
await token5.connect(deployer).transfer(lvl3SubOwner.address, expectedPrice + stakeFee + (protocolFee * 2n));

const contractBalBefore = await token5.balanceOf(await zns.treasury.getAddress());
const parentBalBefore = await token5.balanceOf(lvl2SubOwner.address);
Expand All @@ -1504,6 +1519,11 @@ describe("ZNSSubRegistrar", () => {
expect(contractBalAfter - contractBalBefore).to.eq(expectedPrice);
expect(zeroVaultBalanceAfter - zeroVaultBalanceBefore).to.eq(protocolFee);

const stake = await zns.treasury.stakedForDomain(childHash);
const protocolFeeOut = getStakingOrProtocolFee(stake.amount);

await token5.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256);

// revoke
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
childHash,
Expand All @@ -1516,9 +1536,25 @@ describe("ZNSSubRegistrar", () => {
const zeroVaultBalanceAfterRevoke = await token5.balanceOf(zeroVault.address);

expect(contractBalAfter - contractBalAfterRevoke).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice - protocolFeeOut);
expect(parentBalAfterRevoke - parentBalAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter - protocolFeeOut).to.eq(0);
});

it("Does not charge the owner of a parent domain when they revoke a subdomain", async () => {
const subdomainHash = await registrationWithSetup({
zns,
user: rootOwner,
parentHash: rootHash,
domainLabel: "subdomain",
});

const balanceBefore = await zns.meowToken.balanceOf(rootOwner.address);

await zns.rootRegistrar.connect(rootOwner).revokeDomain(subdomainHash);

const balanceAfter = await zns.meowToken.balanceOf(rootOwner.address);
expect(balanceBefore).to.eq(balanceAfter);
});

it("FixedPricer - StakePayment - no fee - 18 decimals", async () => {
Expand Down Expand Up @@ -1554,7 +1590,7 @@ describe("ZNSSubRegistrar", () => {
// send future child some tokens
await token18.connect(deployer).transfer(
lvl3SubOwner.address,
expectedPrice + protocolFee
expectedPrice + (protocolFee * 2n)
);

const contractBalBefore = await token18.balanceOf(await zns.treasury.getAddress());
Expand All @@ -1579,6 +1615,8 @@ describe("ZNSSubRegistrar", () => {
expect(contractBalAfter - contractBalBefore).to.eq(expectedPrice);
expect(zeroVaultBalanceAfter - zeroVaultBalanceBefore).to.eq(protocolFee);

await token18.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), protocolFee);

// revoke
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
childHash,
Expand All @@ -1591,9 +1629,9 @@ describe("ZNSSubRegistrar", () => {
const zeroVaultBalanceAfterRevoke = await token18.balanceOf(zeroVault.address);

expect(contractBalAfter - contractBalAfterRevoke).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice - protocolFee);
expect(parentBalAfterRevoke - parentBalAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter - protocolFee).to.eq(0);
});

it("FixedPricer - DirectPayment - no fee - 8 decimals", async () => {
Expand Down Expand Up @@ -1718,7 +1756,7 @@ describe("ZNSSubRegistrar", () => {
// send future child some tokens
await token13.connect(deployer).transfer(
lvl3SubOwner.address,
expectedPrice + stakeFee + protocolFee
expectedPrice + stakeFee + (protocolFee * 2n)
);

const contractBalBefore = await token13.balanceOf(await zns.treasury.getAddress());
Expand All @@ -1744,10 +1782,11 @@ describe("ZNSSubRegistrar", () => {
expect(contractBalAfter - contractBalBefore).to.eq(expectedPrice);
expect(zeroVaultBalanceAfter - zeroVaultBalanceBefore).to.eq(protocolFee);

const protocolFeeOut = getStakingOrProtocolFee(expectedPrice);
await token13.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), protocolFeeOut);

// revoke
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
childHash,
);
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(childHash);

// should offer refund !
const contractBalAfterRevoke = await token13.balanceOf(await zns.treasury.getAddress());
Expand All @@ -1756,9 +1795,9 @@ describe("ZNSSubRegistrar", () => {
const zeroVaultBalanceAfterRevoke = await token13.balanceOf(zeroVault.address);

expect(contractBalAfter - contractBalAfterRevoke).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice - protocolFeeOut);
expect(parentBalAfterRevoke - parentBalAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter - protocolFeeOut).to.eq(0);
});

it("CurvePricer - StakePayment - no fee - 2 decimals", async () => {
Expand Down Expand Up @@ -1799,7 +1838,7 @@ describe("ZNSSubRegistrar", () => {
// send future child some tokens
await token2.connect(deployer).transfer(
lvl3SubOwner.address,
expectedPrice + protocolFee
expectedPrice + (protocolFee * 2n)
);

const contractBalBefore = await token2.balanceOf(await zns.treasury.getAddress());
Expand All @@ -1824,6 +1863,8 @@ describe("ZNSSubRegistrar", () => {
expect(contractBalAfter - contractBalBefore).to.eq(expectedPrice);
expect(zeroVaultBalanceAfter - zeroVaultBalanceBefore).to.eq(protocolFee);

await token2.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), protocolFee);

// revoke
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
childHash,
Expand All @@ -1836,9 +1877,9 @@ describe("ZNSSubRegistrar", () => {
const zeroVaultBalanceAfterRevoke = await token2.balanceOf(zeroVault.address);

expect(contractBalAfter - contractBalAfterRevoke).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice);
expect(childBalAfterRevoke - childBalAfter).to.eq(expectedPrice - protocolFee);
expect(parentBalAfterRevoke - parentBalAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter).to.eq(0);
expect(zeroVaultBalanceAfterRevoke - zeroVaultBalanceAfter - protocolFee).to.eq(0);
});

it("CurvePricer - DirectPayment - no fee - 18 decimals", async () => {
Expand Down Expand Up @@ -3281,14 +3322,17 @@ describe("ZNSSubRegistrar", () => {

const userBalbefore = await zns.meowToken.balanceOf(lvl3SubOwner.address);

const protocolFee = getStakingOrProtocolFee(stakedAfter);

await zns.meowToken.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), protocolFee);
// try revoking
await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain(
regResults[1].domainHash,
);

// verify that refund has been acquired by the new owner
const userBalAfter = await zns.meowToken.balanceOf(lvl3SubOwner.address);
expect(userBalAfter - userBalbefore).to.eq(fixedPrice);
expect(userBalAfter - userBalbefore).to.eq(fixedPrice - protocolFee);
});
});

Expand Down
13 changes: 9 additions & 4 deletions test/ZNSTreasury.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
NOT_AUTHORIZED_REG_WIRED_ERR,
INITIALIZED_ERR,
DEFAULT_PRICE_CONFIG,
validateUpgrade, NOT_AUTHORIZED_TREASURY_ERR,
validateUpgrade,
NOT_AUTHORIZED_TREASURY_ERR,
getStakingOrProtocolFee,
} from "./helpers";
import { DeployZNSParams, IZNSContracts } from "./helpers/types";
import * as ethers from "ethers";
Expand Down Expand Up @@ -247,23 +249,26 @@ describe("ZNSTreasury", () => {
const balanceBeforeUnstake = await zns.meowToken.balanceOf(user.address);
const { token, amount: stake } = await zns.treasury.stakedForDomain(domainHash);

await zns.treasury.connect(mockRegistrar).unstakeForDomain(domainHash, user.address);
await zns.treasury.connect(mockRegistrar).unstakeForDomain(domainHash, user.address, protocolFee);

await checkBalance({
token: zns.meowToken,
balanceBefore: balanceBeforeUnstake,
userAddress: user.address,
target: stake,
target: stake - protocolFee,
shouldDecrease: false,
});
expect(token).to.eq(await zns.meowToken.getAddress());
});

it("Should revert if called from an address without REGISTRAR_ROLE", async () => {
const { amount } = await zns.treasury.stakedForDomain(domainHash);
const protocolFee = getStakingOrProtocolFee(amount);
await expect(
zns.treasury.connect(user).unstakeForDomain(
domainHash,
user.address
user.address,
protocolFee
)
).to.be.revertedWith(
getAccessRevertMsg(user.address, REGISTRAR_ROLE)
Expand Down
Loading

0 comments on commit 53df710

Please sign in to comment.