mirror of
https://github.com/gitgig-io/ragnar.git
synced 2026-05-04 03:01:24 -04:00
1708 lines
66 KiB
TypeScript
1708 lines
66 KiB
TypeScript
import { expect } from "chai";
|
|
import { ethers } from "hardhat";
|
|
import { maintainerClaimSignature, mintSignature } from "./helpers/signatureHelpers";
|
|
import { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers";
|
|
import { time } from "@nomicfoundation/hardhat-network-helpers";
|
|
import { Bounties, Identity, TestERC20 } from "../typechain-types";
|
|
|
|
const BIG_SUPPLY = ethers.toBigInt("1000000000000000000000000000");
|
|
|
|
describe("Bounties", () => {
|
|
async function bountiesFixture() {
|
|
const [owner, custodian, finance, notary, issuer, maintainer, contributor, contributor2, contributor3] = await ethers.getSigners();
|
|
|
|
const TestERC20Factory = await ethers.getContractFactory("TestERC20");
|
|
|
|
const usdc = await TestERC20Factory.deploy("USDC", "USDC", 6, 1_000_000_000_000, issuer.address);
|
|
const usdcAddr = await usdc.getAddress();
|
|
|
|
const arb = await TestERC20Factory.deploy("Arbitrum", "ARB", 18, BIG_SUPPLY, issuer.address);
|
|
const arbAddr = await arb.getAddress();
|
|
|
|
const weth = await TestERC20Factory.deploy("Wrapped ETH", "WETH", 18, BIG_SUPPLY, issuer.address);
|
|
const wethAddr = await weth.getAddress();
|
|
|
|
const IdentityFactory = await ethers.getContractFactory("Identity");
|
|
const identity = await IdentityFactory.deploy(custodian.address, notary.address, "http://localhost:3000");
|
|
|
|
const BountiesFactory = await ethers.getContractFactory("Bounties");
|
|
const bounties = await BountiesFactory.deploy(
|
|
custodian.address,
|
|
finance.address,
|
|
notary.address,
|
|
await identity.getAddress(),
|
|
[usdcAddr, arbAddr, wethAddr]
|
|
);
|
|
|
|
return { owner, custodian, bounties, identity, usdc, arb, weth, finance, notary, issuer, maintainer, contributor, contributor2, contributor3 };
|
|
}
|
|
|
|
async function claimableBountyFixture(contributorIds?: string[]) {
|
|
const fixtures = await bountiesFixture();
|
|
const { bounties, notary, maintainer, contributor, contributor2, contributor3 } = fixtures;
|
|
|
|
const platformId = "1";
|
|
const maintainerUserId = "maintainer1";
|
|
const contributorUserId = "contributor1";
|
|
const repoId = "gitgig-io/ragnar";
|
|
const issueId = "123";
|
|
|
|
const contributorUserIds = contributorIds || [contributorUserId];
|
|
const contributorSigners = [contributor, contributor2, contributor3].slice(0, contributorUserIds.length);
|
|
const claimParams = [maintainerUserId, platformId, repoId, issueId, contributorUserIds];
|
|
const claimSignature = await maintainerClaimSignature(bounties, claimParams, notary);
|
|
const { maintainerClaim } = fixtures.bounties.connect(maintainer);
|
|
const executeMaintainerClaim = async () => await maintainerClaim.apply(maintainerClaim, [...claimParams, claimSignature] as any);
|
|
|
|
return { ...fixtures, platformId, maintainerUserId, contributorUserId, repoId, issueId, claimParams, claimSignature, executeMaintainerClaim, contributorUserIds, contributorSigners };
|
|
}
|
|
|
|
interface LinkIdentityProps {
|
|
identity: Identity;
|
|
platformId: string;
|
|
platformUserId: string;
|
|
platformUsername: string;
|
|
participant: HardhatEthersSigner;
|
|
notary: HardhatEthersSigner;
|
|
nonce?: number;
|
|
}
|
|
|
|
async function linkIdentity({ identity, platformId, platformUserId, platformUsername, participant, notary, nonce = 1 }: LinkIdentityProps) {
|
|
const mintParams = [participant.address, platformId, platformUserId, platformUsername, nonce];
|
|
const mintSig = await mintSignature(identity, mintParams, notary);
|
|
const { mint } = identity.connect(participant);
|
|
await mint.apply(mint, [...mintParams, mintSig] as any);
|
|
}
|
|
|
|
async function claimableLinkedBountyFixture(contributorIds?: string[]) {
|
|
const fixtures = await claimableBountyFixture(contributorIds);
|
|
const { identity, maintainer, notary, platformId, maintainerUserId } = fixtures;
|
|
|
|
// map identity for maintainer
|
|
await linkIdentity({ identity, platformId, platformUserId: maintainerUserId, platformUsername: "coder1", participant: maintainer, notary });
|
|
|
|
return fixtures;
|
|
}
|
|
|
|
async function usdcFixture(issuer: HardhatEthersSigner) {
|
|
const TestERC20Factory = await ethers.getContractFactory("TestERC20");
|
|
const usdc = await TestERC20Factory.deploy("USDC", "USDC", 6, 1_000_000, issuer.address);
|
|
return usdc;
|
|
}
|
|
|
|
interface PostBountyProps {
|
|
amount: number;
|
|
platformId: string;
|
|
repoId: string;
|
|
issueId: string;
|
|
bounties: Bounties;
|
|
issuer: HardhatEthersSigner;
|
|
usdc: TestERC20;
|
|
}
|
|
|
|
async function postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc }: PostBountyProps) {
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
}
|
|
|
|
async function maintainerFee(bounties: Bounties, amount: number) {
|
|
const serviceFee = ethers.toNumber(await bounties.serviceFee());
|
|
const maintainerFee = ethers.toNumber(await bounties.maintainerFee());
|
|
const amountAfterServiceFee = amount - (serviceFee * amount / 100);
|
|
return (maintainerFee * amountAfterServiceFee / 100);
|
|
}
|
|
|
|
async function serviceFee(bounties: Bounties, amount: number) {
|
|
const serviceFee = ethers.toNumber(await bounties.serviceFee());
|
|
return (serviceFee * amount / 100);
|
|
}
|
|
|
|
async function bountyAmountAfterFees(bounties: Bounties, postedAmount: number) {
|
|
const serviceFee = ethers.toNumber(await bounties.serviceFee());
|
|
const amountAfterServiceFee = postedAmount - (serviceFee * postedAmount / 100);
|
|
|
|
const maintainerFee = ethers.toNumber(await bounties.maintainerFee());
|
|
const amountAfterMaintainerFee = amountAfterServiceFee - (maintainerFee * amountAfterServiceFee / 100);
|
|
|
|
return amountAfterMaintainerFee;
|
|
}
|
|
|
|
async function bountyAmountAfterFeesPerContributor(bounties: Bounties, postedAmount: number, numContributors: number) {
|
|
const amountAfterServiceFee = await bountyAmountAfterFees(bounties, postedAmount);
|
|
return amountAfterServiceFee / numContributors;
|
|
}
|
|
|
|
describe("Deployment", () => {
|
|
it("should be able to deploy bounty contract", async () => {
|
|
const { bounties } = await bountiesFixture();
|
|
expect(bounties.getAddress()).to.be.a.string;
|
|
});
|
|
});
|
|
|
|
describe("PostBounty", () => {
|
|
it("should be able to post bounty", async () => {
|
|
const { bounties, issuer, usdc } = await bountiesFixture();
|
|
const amount = 5;
|
|
|
|
// when
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount);
|
|
|
|
// then
|
|
// ensure the smart contract has the tokens now
|
|
expect(await usdc.balanceOf(await bounties.getAddress())).to.be.eq(amount);
|
|
});
|
|
|
|
it("should revert when contract is paused", async () => {
|
|
const { bounties, issuer, usdc, custodian } = await bountiesFixture();
|
|
const amount = 5;
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// when/then
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await expect(bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount))
|
|
.to.be.revertedWithCustomError(bounties, 'EnforcedPause');
|
|
});
|
|
|
|
it("should collect service fees", async () => {
|
|
const { bounties, issuer, usdc } = await bountiesFixture();
|
|
const amount = ethers.toBigInt(5);
|
|
const serviceFee = await bounties.serviceFee();
|
|
const expectedFee = amount * serviceFee / ethers.toBigInt(100);
|
|
|
|
// when
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount);
|
|
|
|
// then
|
|
// ensure the smart contract has the tokens now
|
|
expect(await usdc.balanceOf(await bounties.getAddress())).to.be.eq(amount);
|
|
expect(await bounties.fees(await usdc.getAddress())).to.be.eq(expectedFee);
|
|
});
|
|
|
|
it("should not be able to post bounty with unsupported token", async () => {
|
|
const { bounties, issuer } = await bountiesFixture();
|
|
await expect(bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", issuer.address, 5))
|
|
.to.be.revertedWithCustomError(bounties, "TokenSupportError")
|
|
.withArgs(issuer.address, false);
|
|
});
|
|
|
|
it("should not be able to post bounty on closed issue", async () => {
|
|
// given
|
|
const { bounties, identity, maintainer, notary, issuer, contributor, usdc } = await bountiesFixture();
|
|
const platformId = "1";
|
|
const maintainerUserId = "m1";
|
|
const repoId = "gitgig-io/ragnar";
|
|
const issueId = "123";
|
|
const claimParams = [maintainerUserId, platformId, repoId, issueId, [contributor.address]];
|
|
const claimSignature = await maintainerClaimSignature(bounties, claimParams, notary);
|
|
|
|
// map identity for maintainer
|
|
const mintParams = [maintainer.address, platformId, maintainerUserId, "coder1", 1];
|
|
const mintSig = await mintSignature(identity, mintParams, notary);
|
|
const { mint } = identity.connect(maintainer);
|
|
mint.apply(mint, [...mintParams, mintSig] as any);
|
|
|
|
// when
|
|
const { maintainerClaim } = bounties.connect(maintainer);
|
|
await maintainerClaim.apply(maintainerClaim, [...claimParams, claimSignature] as any);
|
|
|
|
// then
|
|
await expect(bounties.connect(issuer).postBounty(platformId, repoId, issueId, await usdc.getAddress(), 5))
|
|
.to.be.revertedWithCustomError(bounties, "IssueClosed")
|
|
.withArgs(platformId, repoId, issueId);
|
|
});
|
|
|
|
it("should emit a BountyCreate event", async () => {
|
|
const { bounties, issuer, usdc } = await bountiesFixture();
|
|
const amount = 5;
|
|
|
|
const fee = await serviceFee(bounties, amount);
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await expect(bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount))
|
|
.to.emit(bounties, "BountyCreate")
|
|
.withArgs(
|
|
"1",
|
|
"gitgig-io/ragnar",
|
|
"123",
|
|
await issuer.getAddress(),
|
|
await usdc.getAddress(),
|
|
"USDC",
|
|
6,
|
|
amount - fee,
|
|
fee
|
|
)
|
|
});
|
|
|
|
it("should respect custom service fees", async () => {
|
|
const { bounties, custodian, issuer, usdc } = await bountiesFixture();
|
|
await bounties.connect(custodian).setCustomServiceFee(issuer.address, 10);
|
|
const amount = ethers.toBigInt(5);
|
|
const customServiceFee = await bounties.effectiveServiceFee(issuer.address);
|
|
const expectedFee = amount * customServiceFee / ethers.toBigInt(100);
|
|
const serviceFee = await bounties.serviceFee();
|
|
expect(serviceFee).to.not.be.eq(customServiceFee);
|
|
|
|
// when
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount);
|
|
|
|
// then
|
|
// ensure the smart contract has the tokens now
|
|
expect(await usdc.balanceOf(await bounties.getAddress())).to.be.eq(amount);
|
|
expect(await bounties.fees(await usdc.getAddress())).to.be.eq(expectedFee);
|
|
});
|
|
|
|
it("should not respect custom service fees for other users", async () => {
|
|
const { bounties, custodian, notary, issuer, usdc } = await bountiesFixture();
|
|
await bounties.connect(custodian).setCustomServiceFee(notary.address, 10);
|
|
const amount = ethers.toBigInt(5);
|
|
const customServiceFee = await bounties.effectiveServiceFee(issuer.address);
|
|
const expectedFee = amount * customServiceFee / ethers.toBigInt(100);
|
|
const serviceFee = await bounties.serviceFee();
|
|
expect(serviceFee).to.be.eq(customServiceFee);
|
|
|
|
// when
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount);
|
|
|
|
// then
|
|
// ensure the smart contract has the tokens now
|
|
expect(await usdc.balanceOf(await bounties.getAddress())).to.be.eq(amount);
|
|
expect(await bounties.fees(await usdc.getAddress())).to.be.eq(expectedFee);
|
|
});
|
|
|
|
it("should add token to bountyTokens list", async () => {
|
|
const { bounties, issuer, usdc } = await bountiesFixture();
|
|
const amount = 5;
|
|
|
|
// when
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty("1", "gitgig-io/ragnar", "123", await usdc.getAddress(), amount);
|
|
|
|
// then
|
|
// ensure the smart contract has the tokens now
|
|
expect(await bounties.bountyTokens("1", "gitgig-io/ragnar", "123", 0)).to.be.eq(await usdc.getAddress());
|
|
});
|
|
});
|
|
|
|
describe("MaintainerClaim", () => {
|
|
it("should allow maintainer to claim with valid signature", async () => {
|
|
const { executeMaintainerClaim } = await claimableLinkedBountyFixture();
|
|
const txn = await executeMaintainerClaim();
|
|
expect(txn.hash).to.be.a.string;
|
|
});
|
|
|
|
it("should revert when contract is paused", async () => {
|
|
const { executeMaintainerClaim, bounties, custodian } = await claimableLinkedBountyFixture();
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// when/then
|
|
await expect(executeMaintainerClaim()).to.be.revertedWithCustomError(bounties, 'EnforcedPause');
|
|
});
|
|
|
|
it("should transfer tokens to maintainer", async () => {
|
|
const { bounties, maintainer, issuer, platformId, repoId, issueId, usdc, executeMaintainerClaim } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
|
|
// when
|
|
await executeMaintainerClaim();
|
|
|
|
// then
|
|
const expectedAmount = await maintainerFee(bounties, amount);
|
|
expect(await usdc.balanceOf(await maintainer.getAddress())).to.be.eq(expectedAmount);
|
|
});
|
|
|
|
it("should transfer tokens to contributors that have minted identity", async () => {
|
|
// given
|
|
const contributorUserIds = ["contributor1", "contributor2", "contributor3"];
|
|
const autoClaimContributorUserIds = contributorUserIds.slice(1);
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributorSigners } = await claimableLinkedBountyFixture(contributorUserIds);
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const contributorAmount = await bountyAmountAfterFeesPerContributor(bounties, amount, contributorUserIds.length);
|
|
|
|
// contributors link wallet
|
|
for (let i = 0; i < contributorUserIds.length; i++) {
|
|
const contributorId = contributorUserIds[i];
|
|
|
|
if (autoClaimContributorUserIds.includes(contributorId)) {
|
|
const contributor = contributorSigners[i];
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorId,
|
|
platformUsername: contributorId,
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
}
|
|
}
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// when/then
|
|
for (let i = 0; i < contributorUserIds.length; i++) {
|
|
const contributorUserId = contributorUserIds[i];
|
|
const expectedAmount = autoClaimContributorUserIds.includes(contributorUserId) ? contributorAmount : 0;
|
|
const contributor = contributorSigners[i];
|
|
expect(await usdc.balanceOf(contributor.address)).to.be.eq(expectedAmount);
|
|
}
|
|
});
|
|
|
|
|
|
it("should emit BountyClaim event", async () => {
|
|
const { bounties, maintainer, issuer, platformId, repoId, issueId, usdc, executeMaintainerClaim } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const expectedAmount = await maintainerFee(bounties, amount);
|
|
|
|
// when
|
|
await expect(executeMaintainerClaim())
|
|
.to.emit(bounties, "BountyClaim")
|
|
.withArgs(
|
|
platformId,
|
|
repoId,
|
|
issueId,
|
|
await maintainer.getAddress(),
|
|
"maintainer",
|
|
await usdc.getAddress(),
|
|
await usdc.symbol(),
|
|
await usdc.decimals(),
|
|
expectedAmount,
|
|
);
|
|
});
|
|
|
|
it("should revert if issue is already closed", async () => {
|
|
const { bounties, platformId, repoId, issueId, executeMaintainerClaim } = await claimableLinkedBountyFixture();
|
|
await executeMaintainerClaim();
|
|
await expect(executeMaintainerClaim())
|
|
.to.be.revertedWithCustomError(bounties, "IssueClosed")
|
|
.withArgs(platformId, repoId, issueId);
|
|
});
|
|
|
|
it("should emit issue closed event", async () => {
|
|
const { bounties, contributorUserIds, executeMaintainerClaim, platformId, repoId, issueId, maintainer, maintainerUserId } = await claimableLinkedBountyFixture();
|
|
await expect(executeMaintainerClaim())
|
|
.to.emit(bounties, "IssueTransition")
|
|
.withArgs(
|
|
platformId,
|
|
repoId,
|
|
issueId,
|
|
"closed",
|
|
"open",
|
|
maintainerUserId,
|
|
await maintainer.getAddress(),
|
|
contributorUserIds
|
|
);
|
|
});
|
|
|
|
it("should revert if maintainer has not linked identity", async () => {
|
|
const { bounties, platformId, maintainerUserId, executeMaintainerClaim } = await claimableBountyFixture();
|
|
await expect(executeMaintainerClaim())
|
|
.to.be.revertedWithCustomError(bounties, "IdentityNotFound")
|
|
.withArgs(platformId, maintainerUserId);
|
|
});
|
|
|
|
it("should revert with invalid signature", async () => {
|
|
const { bounties, claimParams, maintainer } = await claimableLinkedBountyFixture();
|
|
// signing with maintainer key instead of notary key
|
|
const wrongSignature = await maintainerClaimSignature(bounties, claimParams, maintainer);
|
|
const { maintainerClaim } = bounties.connect(maintainer);
|
|
await expect(maintainerClaim.apply(maintainerClaim, [...claimParams, wrongSignature] as any))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidSignature");
|
|
});
|
|
});
|
|
|
|
describe("ContributorClaim", () => {
|
|
it("should allow resolver to claim bounty", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// when
|
|
const txn = await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
});
|
|
|
|
it("should revert for resolver with same user id on different platform", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
const otherPlatformId = '2';
|
|
expect(otherPlatformId).to.not.equal(platformId);
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId: otherPlatformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// when/then
|
|
await expect(bounties.connect(contributor).contributorClaim(platformId, repoId, issueId))
|
|
.to.be.revertedWithCustomError(bounties, 'InvalidResolver')
|
|
.withArgs(platformId, repoId, issueId, contributor.address);
|
|
});
|
|
|
|
it("should revert when paused", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, custodian, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// pause contract
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// when
|
|
await expect(bounties.connect(contributor).contributorClaim(platformId, repoId, issueId))
|
|
.to.be.revertedWithCustomError(bounties, 'EnforcedPause');
|
|
});
|
|
|
|
it("should claim expected amount", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// when
|
|
await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
|
|
// then
|
|
const expectedAmount = await bountyAmountAfterFees(bounties, amount);
|
|
expect(await usdc.balanceOf(await contributor.getAddress())).to.be.eq(expectedAmount);
|
|
});
|
|
|
|
it("should emit claim event", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// when/then
|
|
const expectedAmount = await bountyAmountAfterFees(bounties, amount);
|
|
await expect(bounties.connect(contributor).contributorClaim(platformId, repoId, issueId))
|
|
.to.emit(bounties, "BountyClaim")
|
|
.withArgs(
|
|
platformId,
|
|
repoId,
|
|
issueId,
|
|
await contributor.getAddress(),
|
|
"contributor",
|
|
await usdc.getAddress(),
|
|
await usdc.symbol(),
|
|
await usdc.decimals(),
|
|
expectedAmount,
|
|
);
|
|
});
|
|
|
|
it("should claim expected amount with two resolvers", async () => {
|
|
// given
|
|
const contributorUserIds = ["contributor1", "contributor2"];
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributorSigners } = await claimableLinkedBountyFixture(contributorUserIds);
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const contributorAmount = await bountyAmountAfterFeesPerContributor(bounties, amount, contributorUserIds.length);
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributors link wallet
|
|
for (let i = 0; i < contributorUserIds.length; i++) {
|
|
const contributorId = contributorUserIds[i];
|
|
const contributor = contributorSigners[i];
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorId,
|
|
platformUsername: contributorId,
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
}
|
|
|
|
// when/then
|
|
for (let i = 0; i < contributorSigners.length; i++) {
|
|
const contributor = contributorSigners[i];
|
|
await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
expect(await usdc.balanceOf(await contributor.getAddress())).to.be.eq(contributorAmount);
|
|
}
|
|
});
|
|
|
|
it("should claim expected amount with three resolvers", async () => {
|
|
// given
|
|
const contributorUserIds = ["contributor1", "contributor2", "contributor3"];
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributorSigners } = await claimableLinkedBountyFixture(contributorUserIds);
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const contributorAmount = await bountyAmountAfterFeesPerContributor(bounties, amount, contributorUserIds.length);
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributors link wallet
|
|
for (let i = 0; i < contributorUserIds.length; i++) {
|
|
const contributorId = contributorUserIds[i];
|
|
const contributor = contributorSigners[i];
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorId,
|
|
platformUsername: contributorId,
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
}
|
|
|
|
// when/then
|
|
for (let i = 0; i < contributorSigners.length; i++) {
|
|
const contributor = contributorSigners[i];
|
|
await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
expect(await usdc.balanceOf(await contributor.getAddress())).to.be.eq(contributorAmount);
|
|
}
|
|
});
|
|
|
|
it("should claim expected amount with three resolvers that link and claim serially", async () => {
|
|
// given
|
|
const contributorUserIds = ["contributor1", "contributor2", "contributor3"];
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributorSigners } = await claimableLinkedBountyFixture(contributorUserIds);
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const contributorAmount = await bountyAmountAfterFeesPerContributor(bounties, amount, contributorUserIds.length);
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributors link wallet
|
|
for (let i = 0; i < contributorUserIds.length; i++) {
|
|
const contributorId = contributorUserIds[i];
|
|
const contributor = contributorSigners[i];
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorId,
|
|
platformUsername: contributorId,
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
expect(await usdc.balanceOf(await contributor.getAddress())).to.be.eq(contributorAmount);
|
|
}
|
|
});
|
|
|
|
it("should revert when non-resolver tries to claim bounty", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor3 } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// link identity
|
|
linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: "non-resolver",
|
|
platformUsername: "non-resolver",
|
|
participant: contributor3,
|
|
notary
|
|
});
|
|
|
|
await expect(bounties.connect(contributor3).contributorClaim(platformId, repoId, issueId))
|
|
.to.be.revertedWithCustomError(bounties, 'InvalidResolver')
|
|
.withArgs(platformId, repoId, issueId, contributor3.address);
|
|
expect(await usdc.balanceOf(await contributor3.getAddress())).to.be.eq(0);
|
|
});
|
|
|
|
it("should revert when resolver tries to claim bounty again", async () => {
|
|
// given
|
|
const { executeMaintainerClaim, identity, usdc, issuer, notary, bounties, platformId, repoId, issueId, contributor, contributorUserId } = await claimableLinkedBountyFixture();
|
|
|
|
// post bounty
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// contributor link wallet
|
|
await linkIdentity({
|
|
identity,
|
|
platformId,
|
|
platformUserId: contributorUserId,
|
|
platformUsername: "coder1",
|
|
participant: contributor,
|
|
notary
|
|
});
|
|
|
|
// when
|
|
await bounties.connect(contributor).contributorClaim(platformId, repoId, issueId);
|
|
|
|
// then
|
|
await expect(bounties.connect(contributor).contributorClaim(platformId, repoId, issueId))
|
|
.to.be.revertedWithCustomError(bounties, "AlreadyClaimed")
|
|
.withArgs(platformId, repoId, issueId, contributor.address);
|
|
|
|
const expectedAmount = await bountyAmountAfterFees(bounties, amount);
|
|
expect(await usdc.balanceOf(await contributor.getAddress())).to.be.eq(expectedAmount);
|
|
});
|
|
});
|
|
|
|
describe("WithdrawFees", () => {
|
|
it('should allow finance team to withdraw', async () => {
|
|
const { bounties, platformId, repoId, issueId, issuer, usdc, finance } = await claimableLinkedBountyFixture();
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// when
|
|
await bounties.connect(finance).withdrawFees(await usdc.getAddress());
|
|
|
|
// then
|
|
const expectedFee = await serviceFee(bounties, amount);
|
|
expect(await usdc.balanceOf(await finance.getAddress())).to.be.eq(expectedFee);
|
|
});
|
|
|
|
it('should zero out fees in contract after withdraw', async () => {
|
|
const { bounties, platformId, repoId, issueId, issuer, usdc, finance } = await claimableLinkedBountyFixture();
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// when
|
|
await bounties.connect(finance).withdrawFees(await usdc.getAddress());
|
|
|
|
// then
|
|
expect(await bounties.fees(await usdc.getAddress())).to.be.eq(0);
|
|
});
|
|
|
|
it('should revert when attempted by non-finance team', async () => {
|
|
const { bounties, platformId, repoId, issueId, issuer, usdc } = await claimableLinkedBountyFixture();
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
|
|
// when/then
|
|
await expect(bounties.connect(issuer).withdrawFees(await usdc.getAddress()))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
it('should emit FeeWithdraw event', async () => {
|
|
const { bounties, platformId, repoId, issueId, issuer, usdc, finance } = await claimableLinkedBountyFixture();
|
|
const amount = 500;
|
|
await postBounty({ amount, platformId, repoId, issueId, bounties, issuer, usdc });
|
|
const expectedFee = await serviceFee(bounties, amount);
|
|
|
|
// when / then
|
|
await expect(bounties.connect(finance).withdrawFees(await usdc.getAddress()))
|
|
.to.emit(bounties, "FeeWithdraw")
|
|
.withArgs(
|
|
await usdc.getAddress(),
|
|
await usdc.symbol(),
|
|
await usdc.decimals(),
|
|
finance.address,
|
|
expectedFee
|
|
);
|
|
});
|
|
|
|
it('should not emit FeeWithdraw event when no fees', async () => {
|
|
const { bounties, finance, usdc } = await claimableLinkedBountyFixture();
|
|
await expect(bounties.connect(finance).withdrawFees(usdc.getAddress()))
|
|
.to.not.emit(bounties, "FeeWithdraw");
|
|
});
|
|
});
|
|
|
|
describe("AccessControl:Custodian", () => {
|
|
it('should allow granting custodian role', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
|
|
// when
|
|
await bounties.connect(custodian).grantRole(await bounties.CUSTODIAN_ROLE(), finance.address);
|
|
|
|
// then
|
|
expect(await bounties.hasRole(await bounties.CUSTODIAN_ROLE(), await finance.getAddress())).to.be.true;
|
|
});
|
|
|
|
it('should allow revoking custodian role', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
await bounties.connect(custodian).grantRole(await bounties.CUSTODIAN_ROLE(), finance.address);
|
|
expect(await bounties.hasRole(await bounties.CUSTODIAN_ROLE(), finance.address)).to.be.true;
|
|
|
|
// when
|
|
await bounties.connect(custodian).revokeRole(await bounties.CUSTODIAN_ROLE(), finance.address);
|
|
|
|
// then
|
|
expect(await bounties.hasRole(await bounties.CUSTODIAN_ROLE(), finance.address)).to.be.false;
|
|
});
|
|
|
|
it('should emit RoleGranted event', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).grantRole(await bounties.CUSTODIAN_ROLE(), finance.address))
|
|
.to.emit(bounties, "RoleGranted")
|
|
.withArgs(
|
|
await bounties.CUSTODIAN_ROLE(),
|
|
await finance.getAddress(),
|
|
await custodian.getAddress(),
|
|
);
|
|
});
|
|
|
|
it('should emit RoleRevoked event', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
await bounties.connect(custodian).grantRole(await bounties.CUSTODIAN_ROLE(), finance.address);
|
|
expect(await bounties.hasRole(await bounties.CUSTODIAN_ROLE(), finance.address)).to.be.true;
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).revokeRole(await bounties.CUSTODIAN_ROLE(), finance.address))
|
|
.to.emit(bounties, "RoleRevoked")
|
|
.withArgs(
|
|
await bounties.CUSTODIAN_ROLE(),
|
|
finance.address,
|
|
custodian.address
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to grant custodian role', async () => {
|
|
const { bounties, finance } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).grantRole(await bounties.CUSTODIAN_ROLE(), finance.address))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("SetNotary", () => {
|
|
it('should update notary', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).setNotary(finance.address);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.notary()).to.be.eq(finance.address);
|
|
});
|
|
|
|
it('should revert with invalid notary address', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setNotary(ethers.ZeroAddress))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidAddress")
|
|
.withArgs(ethers.ZeroAddress);
|
|
});
|
|
|
|
it('should emit ConfigChange event', async () => {
|
|
const { bounties, identity, custodian, finance } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).setNotary(finance.address))
|
|
.to.emit(bounties, "ConfigChange")
|
|
.withArgs(
|
|
await finance.getAddress(),
|
|
await identity.getAddress(),
|
|
await bounties.serviceFee(),
|
|
await bounties.maintainerFee()
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to update notary', async () => {
|
|
const { bounties, finance } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).setNotary(finance.address))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("AccessControl:Finance", () => {
|
|
it('should grant finance role', async () => {
|
|
const { bounties, finance, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(finance).grantRole(await bounties.FINANCE_ROLE(), issuer.address);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.hasRole(await bounties.FINANCE_ROLE(), issuer.address)).to.be.true;
|
|
});
|
|
|
|
it('should emit RoleGranted event', async () => {
|
|
const { bounties, finance, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(finance).grantRole(await bounties.FINANCE_ROLE(), issuer.address))
|
|
.to.emit(bounties, "RoleGranted")
|
|
.withArgs(
|
|
await bounties.FINANCE_ROLE(),
|
|
issuer.address,
|
|
finance.address,
|
|
);
|
|
});
|
|
|
|
it('should not allow non-finance to grant finance role', async () => {
|
|
const { bounties, issuer } = await bountiesFixture();
|
|
|
|
// TODO: should finance be able to grant finance role? probably
|
|
// when/then
|
|
await expect(bounties.connect(issuer).grantRole(await bounties.FINANCE_ROLE(), issuer.address))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("SetIdentity", () => {
|
|
it('should update identity contract', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).setIdentity(issuer.address);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.identityContract()).to.be.eq(issuer.address);
|
|
});
|
|
|
|
it('should revert with invalid identity address', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setIdentity(ethers.ZeroAddress))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidAddress")
|
|
.withArgs(ethers.ZeroAddress);
|
|
});
|
|
|
|
it('should emit ConfigChange event', async () => {
|
|
const { bounties, custodian, notary, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).setIdentity(issuer.address))
|
|
.to.emit(bounties, "ConfigChange")
|
|
.withArgs(
|
|
await notary.getAddress(),
|
|
await issuer.getAddress(),
|
|
await bounties.serviceFee(),
|
|
await bounties.maintainerFee()
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to update identity contract', async () => {
|
|
const { bounties, finance, issuer } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).setIdentity(issuer.address))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("SetServiceFee", () => {
|
|
it('should update service fee', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).setServiceFee(50);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.serviceFee()).to.be.eq(50);
|
|
});
|
|
|
|
it('should emit ConfigChange event', async () => {
|
|
const { bounties, identity, custodian, notary } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).setServiceFee(50))
|
|
.to.emit(bounties, "ConfigChange")
|
|
.withArgs(
|
|
await notary.getAddress(),
|
|
await identity.getAddress(),
|
|
50,
|
|
await bounties.maintainerFee()
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to update service fee', async () => {
|
|
const { bounties, finance } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).setServiceFee(50))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
// TODO: figure out how to check for a TypeError
|
|
it.skip('should not allow service fee below zero', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
expect(() => bounties.connect(custodian).setServiceFee(-1)).to.throw();
|
|
});
|
|
|
|
it('should not allow service fee over 100', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setServiceFee(101))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidFee")
|
|
.withArgs(101);
|
|
});
|
|
});
|
|
|
|
describe("SetCustomServiceFee", () => {
|
|
it('should update service fee', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).setCustomServiceFee(issuer.address, 3);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.effectiveServiceFee(issuer.address)).to.be.eq(3);
|
|
});
|
|
|
|
it('should emit CustomFeeChange event when enabled', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).setCustomServiceFee(issuer.address, 3))
|
|
.to.emit(bounties, "CustomFeeChange")
|
|
.withArgs(
|
|
issuer.address,
|
|
"service",
|
|
3,
|
|
true
|
|
);
|
|
});
|
|
|
|
it('should emit CustomFeeChange event when disabled', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when
|
|
await bounties.connect(custodian).setCustomServiceFee(issuer.address, 3);
|
|
await expect(bounties.connect(custodian).setCustomServiceFee(issuer.address, 20))
|
|
.to.emit(bounties, "CustomFeeChange")
|
|
.withArgs(
|
|
issuer.address,
|
|
"service",
|
|
20,
|
|
false
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to update service fee', async () => {
|
|
const { bounties, finance, issuer } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).setCustomServiceFee(issuer.address, 3))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
// TODO: figure out how to check for a TypeError
|
|
it.skip('should not allow service fee below zero', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when/then
|
|
expect(() => bounties.connect(custodian).setCustomServiceFee(issuer.address, -1)).to.throw();
|
|
});
|
|
|
|
it('should not allow service fee over 100', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setCustomServiceFee(issuer.address, 101))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidFee")
|
|
.withArgs(101);
|
|
});
|
|
});
|
|
|
|
describe("EffectiveServiceFee", () => {
|
|
it('should return the default service fee when no custom fee set', async () => {
|
|
const { bounties, issuer } = await bountiesFixture();
|
|
expect(await bounties.effectiveServiceFee(issuer.address)).to.be.eq(20);
|
|
});
|
|
|
|
it('should return the custom service fee when set', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
await bounties.connect(custodian).setCustomServiceFee(issuer.address, 3);
|
|
expect(await bounties.effectiveServiceFee(issuer.address)).to.be.eq(3);
|
|
});
|
|
|
|
it('should return the default service fee when custom fee set for other wallet', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
await bounties.connect(custodian).setCustomServiceFee(custodian.address, 3);
|
|
expect(await bounties.effectiveServiceFee(issuer.address)).to.be.eq(20);
|
|
});
|
|
});
|
|
|
|
describe("SetMaintainerFee", () => {
|
|
it('should update maintainer fee', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).setMaintainerFee(50);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
expect(await bounties.maintainerFee()).to.be.eq(50);
|
|
});
|
|
|
|
it('should emit ConfigChange event', async () => {
|
|
const { bounties, identity, custodian, notary } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).setMaintainerFee(50))
|
|
.to.emit(bounties, "ConfigChange")
|
|
.withArgs(
|
|
await notary.getAddress(),
|
|
await identity.getAddress(),
|
|
await bounties.serviceFee(),
|
|
50
|
|
);
|
|
});
|
|
|
|
it('should not allow non-custodian to update maintainer fee', async () => {
|
|
const { bounties, finance } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).setMaintainerFee(50))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
it('should not allow maintainer fee over 100', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setMaintainerFee(101))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidFee")
|
|
.withArgs(101);
|
|
});
|
|
|
|
// TODO: figure out how to test for a TypeError INVALID_ARGUMENT
|
|
it.skip('should not allow maintainer fee below zero', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).setMaintainerFee(-1))
|
|
.to.be.revertedWithCustomError(bounties, "InvalidFee")
|
|
.withArgs(-1);
|
|
});
|
|
});
|
|
|
|
describe("AddToken", () => {
|
|
it('should add a supported token', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
const usdc2 = await usdcFixture(issuer);
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).addToken(await usdc2.getAddress());
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
});
|
|
|
|
it('should update supported token map', async () => {
|
|
const { bounties, custodian, issuer, usdc, arb, weth } = await bountiesFixture();
|
|
const usdc2 = await usdcFixture(issuer);
|
|
const usdc2Addr = await usdc2.getAddress();
|
|
|
|
// when
|
|
await bounties.connect(custodian).addToken(usdc2Addr);
|
|
|
|
// then
|
|
expect(await bounties.isSupportedToken(await usdc.getAddress())).to.be.true;
|
|
expect(await bounties.isSupportedToken(await arb.getAddress())).to.be.true;
|
|
expect(await bounties.isSupportedToken(await weth.getAddress())).to.be.true;
|
|
expect(await bounties.isSupportedToken(usdc2Addr)).to.be.true;
|
|
});
|
|
|
|
it('should emit TokenSupportChange event', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
const usdc2 = await usdcFixture(issuer);
|
|
const usdc2Addr = await usdc2.getAddress();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).addToken(usdc2Addr)).to.emit(bounties, "TokenSupportChange").withArgs(
|
|
true,
|
|
usdc2Addr,
|
|
"USDC",
|
|
6
|
|
);
|
|
});
|
|
|
|
it('should revert when called by non-custodian', async () => {
|
|
const { bounties, issuer } = await bountiesFixture();
|
|
const usdc2 = await usdcFixture(issuer);
|
|
const usdc2Addr = await usdc2.getAddress();
|
|
|
|
// when
|
|
await expect(bounties.connect(issuer).addToken(usdc2Addr))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
it('should revert when called with already supported token', async () => {
|
|
const { bounties, custodian, usdc } = await bountiesFixture();
|
|
const usdcAddr = await usdc.getAddress();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).addToken(usdcAddr))
|
|
.to.be.revertedWithCustomError(bounties, "TokenSupportError")
|
|
.withArgs(usdcAddr, true);
|
|
});
|
|
});
|
|
|
|
describe("RemoveToken", () => {
|
|
it('should remove a supported token', async () => {
|
|
const { bounties, custodian, usdc } = await bountiesFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(custodian).removeToken(await usdc.getAddress());
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
});
|
|
|
|
it('should update supported token map', async () => {
|
|
const { bounties, custodian, usdc } = await bountiesFixture();
|
|
|
|
// when
|
|
await bounties.connect(custodian).removeToken(await usdc.getAddress());
|
|
|
|
// then
|
|
expect(await bounties.isSupportedToken(await usdc.getAddress())).to.be.false;
|
|
});
|
|
|
|
it('should emit TokenSupportChange event', async () => {
|
|
const { bounties, custodian, usdc } = await bountiesFixture();
|
|
const usdcAddr = await usdc.getAddress();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(custodian).removeToken(usdcAddr)).to.emit(bounties, "TokenSupportChange").withArgs(
|
|
false,
|
|
usdcAddr,
|
|
"USDC",
|
|
6
|
|
);
|
|
});
|
|
|
|
it('should revert when called by non-custodian', async () => {
|
|
const { bounties, issuer, usdc } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(issuer).removeToken(await usdc.getAddress()))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
it('should revert when called with non-supported token', async () => {
|
|
const { bounties, custodian, issuer } = await bountiesFixture();
|
|
const usdc2 = await usdcFixture(issuer);
|
|
const usdc2Addr = await usdc2.getAddress();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).removeToken(usdc2Addr))
|
|
.to.be.revertedWithCustomError(bounties, "TokenSupportError")
|
|
.withArgs(usdc2Addr, false);
|
|
});
|
|
});
|
|
|
|
describe("Pause", () => {
|
|
it('should pause', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// then
|
|
expect(await bounties.paused()).to.be.true;
|
|
});
|
|
|
|
it('should emit Paused event', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(custodian).pause())
|
|
.to.emit(bounties, "Paused")
|
|
.withArgs(custodian.address);
|
|
});
|
|
|
|
|
|
it('should revert when called by non-custodian', async () => {
|
|
const { bounties, finance } = await bountiesFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(finance).pause())
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("Unpause", () => {
|
|
it('should unpause', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
await bounties.connect(custodian).pause();
|
|
expect(await bounties.paused()).to.be.true;
|
|
|
|
await bounties.connect(custodian).unpause();
|
|
|
|
// then
|
|
expect(await bounties.paused()).to.be.false;
|
|
});
|
|
|
|
it('should emit Unpaused event', async () => {
|
|
const { bounties, custodian } = await bountiesFixture();
|
|
await bounties.connect(custodian).pause();
|
|
expect(await bounties.paused()).to.be.true;
|
|
|
|
await expect(bounties.connect(custodian).unpause())
|
|
.to.emit(bounties, "Unpaused")
|
|
.withArgs(custodian.address);
|
|
});
|
|
|
|
it('should revert when called by non-custodian', async () => {
|
|
const { bounties, custodian, finance } = await bountiesFixture();
|
|
await bounties.connect(custodian).pause();
|
|
expect(await bounties.paused()).to.be.true;
|
|
|
|
// when
|
|
await expect(bounties.connect(finance).unpause()).to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
});
|
|
|
|
describe("SweepBounty", () => {
|
|
async function sweepableBountyFixture() {
|
|
const fixtures = await bountiesFixture();
|
|
const { bounties, issuer, usdc } = fixtures;
|
|
|
|
const platformId = "1";
|
|
const repoId = "gitgig-io/ragnar";
|
|
const issueId = "123";
|
|
const amount = 5;
|
|
|
|
const serviceFee = ethers.toNumber(await bounties.serviceFee()) * amount / 100;
|
|
const bountyAmount = amount - serviceFee;
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
expect(await bounties.bounties(platformId, repoId, issueId, await usdc.getAddress())).to.equal(bountyAmount);
|
|
const supportedTokens = [await usdc.getAddress()];
|
|
|
|
return { ...fixtures, amount, serviceFee, bountyAmount, platformId, repoId, issueId, supportedTokens };
|
|
}
|
|
|
|
const RECLAIM_TIMEFRAME = 60 * 60 * 24 * (365 + 1);
|
|
const SWEEP_TIMEFRAME = 60 * 60 * 24 * (365 + 90);
|
|
|
|
async function sweepableBountyAfterReclaimFixture() {
|
|
const fixtures = await sweepableBountyFixture();
|
|
await time.increase(SWEEP_TIMEFRAME);
|
|
return fixtures;
|
|
}
|
|
|
|
it('should sweep a bounty when required timeframe has passed', async () => {
|
|
const { bounties, finance, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
|
|
// when
|
|
const txn = await bounties.connect(finance).sweepBounty(platformId, repoId, issueId, supportedTokens);
|
|
|
|
// then
|
|
expect(txn.hash).to.be.a.string;
|
|
});
|
|
|
|
it('should revert when before reclaim timeframe', async () => {
|
|
const { bounties, finance, platformId, repoId, issueId, supportedTokens } = await sweepableBountyFixture();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(finance)
|
|
.sweepBounty(platformId, repoId, issueId, supportedTokens)
|
|
).to.be.revertedWithCustomError(bounties, "TimeframeError");
|
|
});
|
|
|
|
it('should revert when during reclaim timeframe', async () => {
|
|
const { bounties, finance, platformId, repoId, issueId, supportedTokens } = await sweepableBountyFixture();
|
|
time.increase(RECLAIM_TIMEFRAME);
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(finance)
|
|
.sweepBounty(platformId, repoId, issueId, supportedTokens)
|
|
).to.be.revertedWithCustomError(bounties, "TimeframeError");
|
|
});
|
|
|
|
it('should revert when paused', async () => {
|
|
const { bounties, custodian, finance, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(finance)
|
|
.sweepBounty(platformId, repoId, issueId, supportedTokens)
|
|
).to.revertedWithCustomError(bounties, 'EnforcedPause');
|
|
});
|
|
|
|
it('should zero out bounty', async () => {
|
|
const { bounties, finance, usdc, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
|
|
// when
|
|
await bounties.connect(finance).sweepBounty(platformId, repoId, issueId, supportedTokens);
|
|
|
|
// then
|
|
expect(await bounties.bounties(platformId, repoId, issueId, await usdc.getAddress())).to.equal(0);
|
|
});
|
|
|
|
it('should transfer bounty tokens to message sender', async () => {
|
|
const { bounties, finance, usdc, bountyAmount, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
|
|
// when
|
|
await bounties.connect(finance).sweepBounty(platformId, repoId, issueId, supportedTokens);
|
|
|
|
// then
|
|
expect(await usdc.balanceOf(await finance.getAddress())).to.equal(bountyAmount);
|
|
});
|
|
|
|
it('should emit BountySweep event', async () => {
|
|
const { bounties, finance, usdc, bountyAmount, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(finance).sweepBounty(platformId, repoId, issueId, supportedTokens))
|
|
.to.emit(bounties, "BountySweep")
|
|
.withArgs(finance.address, "1", "gitgig-io/ragnar", "123", await usdc.getAddress(), "USDC", 6, bountyAmount);
|
|
});
|
|
|
|
|
|
it('should revert if not called by finance', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
|
|
// when/then
|
|
await expect(bounties.connect(issuer).sweepBounty(platformId, repoId, issueId, supportedTokens))
|
|
.to.be.revertedWithCustomError(bounties, "AccessControlUnauthorizedAccount");
|
|
});
|
|
|
|
it('should revert if no bounty to sweep', async () => {
|
|
const { bounties, finance, usdc } = await bountiesFixture();
|
|
const platformId = "1";
|
|
const repoId = "gitgig-io/ragnar";
|
|
const issueId = "123";
|
|
const usdcAddr = await usdc.getAddress();
|
|
await time.increase(SWEEP_TIMEFRAME);
|
|
|
|
await expect(bounties.connect(finance).sweepBounty("1", "gitgig-io/ragnar", "123", [usdcAddr]))
|
|
.to.be.revertedWithCustomError(bounties, "NoBounty")
|
|
.withArgs(platformId, repoId, issueId, [usdcAddr]);
|
|
});
|
|
|
|
it('should remove the token from bountyTokens', async () => {
|
|
const { bounties, finance, platformId, repoId, issueId, supportedTokens } = await sweepableBountyAfterReclaimFixture();
|
|
expect(supportedTokens.length).to.equal(1);
|
|
expect(await bounties.bountyTokens(platformId, repoId, issueId, 0)).to.not.equal(ethers.ZeroAddress);
|
|
|
|
// when
|
|
await bounties.connect(finance).sweepBounty(platformId, repoId, issueId, supportedTokens);
|
|
|
|
// then
|
|
await expect(bounties.bountyTokens(platformId, repoId, issueId, 0)).to.be.reverted;
|
|
});
|
|
});
|
|
|
|
describe('ReclaimBounty', () => {
|
|
async function reclaimableBountyFixture() {
|
|
const fixtures = await bountiesFixture();
|
|
const { bounties, issuer, usdc } = fixtures;
|
|
|
|
const platformId = "1";
|
|
const repoId = "gitgig-io/ragnar";
|
|
const issueId = "123";
|
|
const amount = 100;
|
|
|
|
const serviceFee = ethers.toNumber(await bounties.serviceFee()) * amount / 100;
|
|
const bountyAmount = amount - serviceFee;
|
|
await usdc.connect(issuer).approve(await bounties.getAddress(), amount);
|
|
await bounties.connect(issuer).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
expect(await bounties.bounties(platformId, repoId, issueId, await usdc.getAddress())).to.equal(bountyAmount);
|
|
const issuedToken = await usdc.getAddress();
|
|
|
|
return { ...fixtures, amount, serviceFee, bountyAmount, platformId, repoId, issueId, issuedToken };
|
|
}
|
|
|
|
const RECLAIM_TIMEFRAME = 60 * 60 * 24 * (365 + 1);
|
|
|
|
async function reclaimableBountyAfterReclaimAvailableFixture() {
|
|
const fixtures = await reclaimableBountyFixture();
|
|
await time.increase(RECLAIM_TIMEFRAME);
|
|
return fixtures;
|
|
}
|
|
|
|
it('should revert when reclaiming before required timeframe', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyFixture();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken)
|
|
).to.revertedWithCustomError(bounties, 'TimeframeError');
|
|
});
|
|
|
|
it('should reclaim after required timeframe', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
|
|
// when
|
|
const tx = await bounties.connect(issuer).reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(tx.hash).to.be.a.string;
|
|
});
|
|
|
|
it('should reclaim well after required timeframe', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
await time.increase(RECLAIM_TIMEFRAME * 10);
|
|
|
|
// when
|
|
const tx = await bounties.connect(issuer).reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(tx.hash).to.be.a.string;
|
|
});
|
|
|
|
it('should revert when paused', async () => {
|
|
const { bounties, custodian, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
await bounties.connect(custodian).pause();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken)
|
|
).to.revertedWithCustomError(bounties, 'EnforcedPause');
|
|
});
|
|
|
|
it('should revert when issue is closed', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, usdc, executeMaintainerClaim } = await claimableLinkedBountyFixture();
|
|
|
|
// maintainer claim
|
|
await executeMaintainerClaim();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, await usdc.getAddress())
|
|
).to.revertedWithCustomError(bounties, 'IssueClosed');
|
|
});
|
|
|
|
it('should transfer bounty minus fee to issuer', async () => {
|
|
const { bounties, issuer, bountyAmount, usdc, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
const priorBalance = await usdc.balanceOf(issuer);
|
|
const expectedBalance = priorBalance + ethers.toBigInt(bountyAmount);
|
|
|
|
// when
|
|
await bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(await usdc.balanceOf(issuer)).to.equal(expectedBalance);
|
|
});
|
|
|
|
it('should only transfer bounty that issuer posted', async () => {
|
|
const { bounties, custodian, issuer, bountyAmount, serviceFee: issuerServiceFee, usdc, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
const amount = 100_000_000;
|
|
const bountiesAddr = await bounties.getAddress();
|
|
|
|
// transfer from issuer to custodian
|
|
await usdc.connect(issuer).transfer(custodian.address, amount);
|
|
|
|
const priorBalance = await usdc.balanceOf(issuer);
|
|
const expectedBalance = priorBalance + ethers.toBigInt(bountyAmount);
|
|
|
|
// custodian post bounty
|
|
await usdc.connect(custodian).approve(bountiesAddr, amount);
|
|
await bounties.connect(custodian).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
|
|
// when
|
|
await bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(await usdc.balanceOf(issuer)).to.equal(expectedBalance);
|
|
expect(await usdc.balanceOf(bountiesAddr)).to.equal(amount + issuerServiceFee);
|
|
});
|
|
|
|
it('should allow reclaiming immediately after posting when in reclaim timeframe', async () => {
|
|
const { bounties, custodian, issuer, usdc, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
const bountiesAddr = await bounties.getAddress();
|
|
const amount = 100_000_000;
|
|
|
|
// transfer from issuer to custodian
|
|
await usdc.connect(issuer).transfer(custodian.address, amount);
|
|
|
|
// custodian post bounty
|
|
await usdc.connect(custodian).approve(bountiesAddr, amount);
|
|
await bounties.connect(custodian).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
|
|
// when/then
|
|
await bounties
|
|
.connect(custodian)
|
|
.reclaim(platformId, repoId, issueId, issuedToken);
|
|
});
|
|
|
|
it('should revert when no amount to reclaim', async () => {
|
|
const { bounties, custodian, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(custodian)
|
|
.reclaim(platformId, repoId, issueId, issuedToken)
|
|
).to.be.revertedWithCustomError(bounties, 'NoBounty');
|
|
});
|
|
|
|
it('should revert when already reclaimed', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
await bounties.connect(issuer).reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// when/then
|
|
await expect(bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken)
|
|
).to.be.revertedWithCustomError(bounties, 'NoBounty');
|
|
});
|
|
|
|
it('should reduce the amount of the bounty by the reclaimed amount', async () => {
|
|
const { bounties, custodian, issuer, bountyAmount: issuerBountyAmount, usdc, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
const amount = 100_000_000;
|
|
const fee = await serviceFee(bounties, amount);
|
|
const bountiesAddr = await bounties.getAddress();
|
|
const custodianBountyAmount = amount - fee;
|
|
|
|
// transfer from issuer to custodian
|
|
await usdc.connect(issuer).transfer(custodian.address, amount);
|
|
|
|
// custodian post bounty
|
|
await usdc.connect(custodian).approve(bountiesAddr, amount);
|
|
await bounties.connect(custodian).postBounty(platformId, repoId, issueId, await usdc.getAddress(), amount);
|
|
expect(await bounties.bounties(platformId, repoId, issueId, issuedToken)).to.be.eq(custodianBountyAmount + issuerBountyAmount);
|
|
|
|
// when
|
|
await bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(await bounties.bounties(platformId, repoId, issueId, issuedToken)).to.be.eq(custodianBountyAmount);
|
|
});
|
|
|
|
// TODO: re-add this test if we can get bounties contract size down
|
|
it.skip('should remove token from bountyTokens when that bounty token amount goes to zero', async () => {
|
|
const { bounties, issuer, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
|
|
// when
|
|
await bounties
|
|
.connect(issuer)
|
|
.reclaim(platformId, repoId, issueId, issuedToken);
|
|
|
|
// then
|
|
expect(await bounties.bountyTokens(platformId, repoId, issueId, 0)).to.equal(ethers.ZeroAddress);
|
|
});
|
|
|
|
it('should emit Reclaim event', async () => {
|
|
const { bounties, issuer, usdc, bountyAmount, platformId, repoId, issueId, issuedToken } = await reclaimableBountyAfterReclaimAvailableFixture();
|
|
|
|
// when
|
|
await expect(bounties.connect(issuer).reclaim(platformId, repoId, issueId, issuedToken))
|
|
.to.emit(bounties, 'BountyReclaim')
|
|
.withArgs(
|
|
platformId,
|
|
repoId,
|
|
issueId,
|
|
issuer.address,
|
|
issuedToken,
|
|
await usdc.symbol(),
|
|
await usdc.decimals(),
|
|
bountyAmount
|
|
);
|
|
});
|
|
});
|
|
});
|