372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import { expect } from 'chai';
|
|
import { BigNumber, BigNumberish, Contract, ContractTransaction } from 'ethers';
|
|
import { ethers } from 'hardhat';
|
|
|
|
import { Baal, BaalSummoner, MultiSend, Poster } from '../../src/types';
|
|
import { encodeMultiAction } from '../../src/util';
|
|
import { moveForwardPeriods } from './evm';
|
|
import { getSaltNonce } from './safe';
|
|
|
|
export type DAOSettings = {
|
|
PROPOSAL_OFFERING: any;
|
|
GRACE_PERIOD_IN_SECONDS: any;
|
|
VOTING_PERIOD_IN_SECONDS: any;
|
|
QUORUM_PERCENT: any;
|
|
SPONSOR_THRESHOLD: any;
|
|
MIN_RETENTION_PERCENT: any;
|
|
MIN_STAKING_PERCENT: any;
|
|
TOKEN_NAME: any;
|
|
TOKEN_SYMBOL: any;
|
|
};
|
|
|
|
export const abiCoder = ethers.utils.defaultAbiCoder;
|
|
|
|
export const revertMessages = {
|
|
molochAlreadyInitialized: "Initializable: contract is already initialized",
|
|
molochConstructorSharesCannotBe0: "shares cannot be 0",
|
|
molochConstructorVotingPeriodCannotBe0: "votingPeriod cannot be 0",
|
|
submitProposalExpired: "expired",
|
|
submitProposalOffering: "Baal requires an offering",
|
|
submitProposalVotingPeriod: "!votingPeriod",
|
|
submitProposalArrays: "!array parity",
|
|
submitProposalArrayMax: "array max",
|
|
submitProposalFlag: "!flag",
|
|
sponsorProposalExpired: "expired",
|
|
sponsorProposalSponsor: "!sponsor",
|
|
sponsorProposalExists: "!exist",
|
|
sponsorProposalSponsored: "sponsored",
|
|
submitVoteNotSponsored: "!sponsored",
|
|
submitVoteTimeEnded: "ended",
|
|
submitVoteVoted: "voted",
|
|
submitVoteMember: "!member",
|
|
submitVoteWithSigTimeEnded: "ended",
|
|
submitVoteWithSigVoted: "voted",
|
|
submitVoteWithSigMember: "!member",
|
|
proposalMisnumbered: "!exist",
|
|
unsetGuildTokensLastToken:
|
|
"reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)",
|
|
sharesTransferPaused: "shares: !transferable",
|
|
sharesInsufficientBalance:
|
|
"ERC20: transfer amount exceeds balance",
|
|
|
|
// -----
|
|
lootAlreadyInitialized: "Initializable: contract is already initialized",
|
|
molochSetupSharesNoShares: "shares != 0",
|
|
proposalNotSponsored: "!sponsor",
|
|
sponsorProposalNotSubmitted: "!submitted",
|
|
submitVoteNotVoting: "!voting",
|
|
processProposalNotReady: "!ready",
|
|
ragequitUnordered: "!order",
|
|
// unsetGuildTokensLastToken: 'reverted with panic code 0x32 (Array accessed at an out-of-bounds or negative index)',
|
|
sharesInsufficientApproval: "ERC20: insufficient allowance", // Error: Transaction reverted without a reason string
|
|
lootTransferPaused: "loot: !transferable",
|
|
lootInsufficientBalance:
|
|
"ERC20: transfer amount exceeds balance",
|
|
// lootInsufficientApproval: 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)',
|
|
lootInsufficientApproval: "ERC20: insufficient allowance", // Error: Transaction reverted without a reason string
|
|
mintSharesArrayParity: "!array parity",
|
|
burnSharesArrayParity: "!array parity",
|
|
burnSharesInsufficientShares: "ERC20: burn amount exceeds balance",
|
|
mintLootArrayParity: "!array parity",
|
|
burnLootArrayParity: "!array parity",
|
|
burnLootInsufficientShares:
|
|
"ERC20: burn amount exceeds balance",
|
|
cancelProposalNotVoting: "!voting",
|
|
cancelProposalNotCancellable: "!cancellable",
|
|
baalOrAdmin: "!baal & !admin",
|
|
baalOrManager: "!baal & !manager",
|
|
baalOrGovernor: "!baal & !governor",
|
|
permitNotAuthorized: "!authorized",
|
|
permitExpired: "expired",
|
|
notEnoughGas: "not enough gas",
|
|
baalGasToHigh: "baalGas to high",
|
|
OwnableCallerIsNotTheOwner: "Ownable: caller is not the owner",
|
|
};
|
|
|
|
export type ProposalType = {
|
|
flag: BigNumberish;
|
|
account?: `0x${string}`;
|
|
data: string;
|
|
details: string;
|
|
expiration: BigNumberish;
|
|
baalGas: BigNumberish;
|
|
};
|
|
|
|
export const PROPOSAL_STATES = {
|
|
UNBORN: 0,
|
|
SUBMITTED: 1,
|
|
VOTING: 2,
|
|
CANCELLED: 3,
|
|
GRACE: 4,
|
|
READY: 5,
|
|
PROCESSED: 6,
|
|
DEEFEATED: 7,
|
|
};
|
|
|
|
export enum SHAMAN_PERMISSIONS {
|
|
NONE,
|
|
ADMIN,
|
|
MANAGER,
|
|
ADMIN_MANAGER,
|
|
GOVERNANCE,
|
|
ADMIN_GOVERNANCE,
|
|
MANAGER_GOVERNANCE,
|
|
ALL,
|
|
};
|
|
|
|
export const defaultDAOSettings: DAOSettings = {
|
|
GRACE_PERIOD_IN_SECONDS: 43200,
|
|
VOTING_PERIOD_IN_SECONDS: 432000,
|
|
PROPOSAL_OFFERING: 0,
|
|
SPONSOR_THRESHOLD: 1,
|
|
MIN_RETENTION_PERCENT: 0,
|
|
MIN_STAKING_PERCENT: 0,
|
|
QUORUM_PERCENT: 0,
|
|
TOKEN_NAME: "wrapped ETH",
|
|
TOKEN_SYMBOL: "WETH",
|
|
};
|
|
|
|
export const defaultMetadataConfig = {
|
|
CONTENT: '{"name":"test"}',
|
|
TAG: "daohaus.summoner.daoProfile",
|
|
};
|
|
|
|
export const defaultProposalSettings = {
|
|
DETAILS: 'all hail baal',
|
|
EXPIRATION: 0,
|
|
BAAL_GAS: 0,
|
|
};
|
|
|
|
export type SummonSetup = {
|
|
loot: number;
|
|
lootPaused: boolean;
|
|
shamanPermissions: SHAMAN_PERMISSIONS;
|
|
shares: number;
|
|
sharesPaused: boolean;
|
|
};
|
|
|
|
export const defaultSummonSetup: SummonSetup = {
|
|
loot: 500,
|
|
lootPaused: false,
|
|
shamanPermissions: SHAMAN_PERMISSIONS.ALL,
|
|
shares: 100,
|
|
sharesPaused: false,
|
|
};
|
|
|
|
export type NewBaalParams = {
|
|
baalSummoner: Contract;
|
|
baalSingleton: Baal;
|
|
poster: Poster;
|
|
config: DAOSettings;
|
|
adminConfig: [boolean, boolean];
|
|
shamans: [string[], number[]];
|
|
shares: [string[], number[]];
|
|
loots: [string[], number[]];
|
|
safeAddress?: `0x${string}`;
|
|
forwarderAddress?: `0x${string}`;
|
|
lootAddress?: `0x${string}`;
|
|
sharesAddress?: `0x${string}`;
|
|
saltNonceOverride?: string;
|
|
}
|
|
|
|
export type NewBaalAddresses = {
|
|
baal: string;
|
|
loot: string;
|
|
shares: string;
|
|
safe: string;
|
|
}
|
|
|
|
export const getNewBaalAddresses = async (tx: ContractTransaction): Promise<NewBaalAddresses> => {
|
|
const receipt = await ethers.provider.getTransactionReceipt(tx.hash);
|
|
// console.log({logs: receipt.logs})
|
|
let baalSummonAbi = [
|
|
"event SummonBaal(address indexed baal, address indexed loot, address indexed shares, address safe, address forwarder, uint256 existingAddrs)",
|
|
];
|
|
let iface = new ethers.utils.Interface(baalSummonAbi);
|
|
let log = iface.parseLog(receipt.logs[receipt.logs.length - 1]);
|
|
const { baal, loot, shares, safe } = log.args;
|
|
return { baal, loot, shares, safe };
|
|
};
|
|
|
|
export const getBaalParams = async function (
|
|
baal: Baal,
|
|
poster: Poster,
|
|
config: DAOSettings,
|
|
adminConfig: [boolean, boolean],
|
|
shamans: [string[], number[]],
|
|
shares: [string[], number[]],
|
|
loots: [string[], number[]],
|
|
safeAddress: string = ethers.constants.AddressZero,
|
|
forwarderAddress: string = ethers.constants.AddressZero,
|
|
lootAddress: string = ethers.constants.AddressZero,
|
|
sharesAddress: string = ethers.constants.AddressZero,
|
|
) {
|
|
const governanceConfig = abiCoder.encode(
|
|
["uint32", "uint32", "uint256", "uint256", "uint256", "uint256"],
|
|
[
|
|
config.VOTING_PERIOD_IN_SECONDS,
|
|
config.GRACE_PERIOD_IN_SECONDS,
|
|
config.PROPOSAL_OFFERING,
|
|
config.QUORUM_PERCENT,
|
|
config.SPONSOR_THRESHOLD,
|
|
config.MIN_RETENTION_PERCENT,
|
|
]
|
|
);
|
|
|
|
const setAdminConfig = await baal.interface.encodeFunctionData(
|
|
"setAdminConfig",
|
|
adminConfig
|
|
);
|
|
const setGovernanceConfig = await baal.interface.encodeFunctionData(
|
|
"setGovernanceConfig",
|
|
[governanceConfig]
|
|
);
|
|
const setShaman = await baal.interface.encodeFunctionData(
|
|
"setShamans",
|
|
shamans
|
|
);
|
|
const mintShares = await baal.interface.encodeFunctionData(
|
|
"mintShares",
|
|
shares
|
|
);
|
|
const mintLoot = await baal.interface.encodeFunctionData("mintLoot", loots);
|
|
const postMetaData = await poster.interface.encodeFunctionData("post", [
|
|
defaultMetadataConfig.CONTENT,
|
|
defaultMetadataConfig.TAG,
|
|
]);
|
|
const posterFromBaal = await baal.interface.encodeFunctionData(
|
|
"executeAsBaal",
|
|
[poster.address, 0, postMetaData]
|
|
);
|
|
|
|
const initalizationActions = [
|
|
setAdminConfig,
|
|
setGovernanceConfig,
|
|
setShaman,
|
|
mintShares,
|
|
mintLoot,
|
|
posterFromBaal,
|
|
];
|
|
|
|
return {
|
|
initParams: abiCoder.encode(
|
|
["string", "string", "address", "address", "address", "address"],
|
|
[
|
|
config.TOKEN_NAME,
|
|
config.TOKEN_SYMBOL,
|
|
safeAddress,
|
|
forwarderAddress,
|
|
lootAddress,
|
|
sharesAddress,
|
|
],
|
|
),
|
|
initalizationActions,
|
|
};
|
|
};
|
|
|
|
// export const setupBaal = async (params: NewBaalParams) => {
|
|
export const setupBaal = async ({
|
|
baalSummoner,
|
|
baalSingleton,
|
|
poster,
|
|
config,
|
|
adminConfig,
|
|
shamans,
|
|
shares,
|
|
loots,
|
|
safeAddress,
|
|
forwarderAddress,
|
|
lootAddress,
|
|
sharesAddress,
|
|
saltNonceOverride
|
|
}: NewBaalParams) => {
|
|
const saltNonce = saltNonceOverride || getSaltNonce();
|
|
const encodedInitParams = await getBaalParams(
|
|
baalSingleton,
|
|
poster,
|
|
config,
|
|
adminConfig,
|
|
shamans,
|
|
shares,
|
|
loots,
|
|
safeAddress,
|
|
forwarderAddress,
|
|
lootAddress,
|
|
sharesAddress,
|
|
);
|
|
const tx = await (baalSummoner as BaalSummoner).summonBaal(
|
|
encodedInitParams.initParams,
|
|
encodedInitParams.initalizationActions,
|
|
saltNonce,
|
|
);
|
|
return await getNewBaalAddresses(tx);
|
|
};
|
|
|
|
export type ProposalParams = {
|
|
baal: Baal;
|
|
encodedAction: string;
|
|
proposal: ProposalType;
|
|
proposalId?: BigNumberish;
|
|
daoSettings?: DAOSettings;
|
|
extraSeconds?: number;
|
|
};
|
|
|
|
export const submitAndProcessProposal = async ({
|
|
baal,
|
|
encodedAction,
|
|
proposal,
|
|
proposalId,
|
|
daoSettings = defaultDAOSettings,
|
|
extraSeconds = 2,
|
|
}: ProposalParams) => {
|
|
await baal.submitProposal(encodedAction, proposal.expiration, proposal.baalGas, ethers.utils.id(proposal.details));
|
|
const id = proposalId ? proposalId : await baal.proposalCount();
|
|
await baal.submitVote(id, true);
|
|
await moveForwardPeriods(daoSettings.VOTING_PERIOD_IN_SECONDS, extraSeconds);
|
|
return await baal.processProposal(id, encodedAction);
|
|
};
|
|
|
|
export const setShamanProposal = async (
|
|
baal: Baal,
|
|
multisend: MultiSend,
|
|
shamanAddress: string,
|
|
permission: BigNumberish,
|
|
daoSettings = defaultDAOSettings,
|
|
extraSeconds = 2,
|
|
) => {
|
|
const setShaman = baal.interface.encodeFunctionData('setShamans', [
|
|
[shamanAddress],
|
|
[permission],
|
|
]);
|
|
const setShamanAction = encodeMultiAction(
|
|
multisend,
|
|
[setShaman],
|
|
[baal.address],
|
|
[BigNumber.from(0)],
|
|
[0]
|
|
);
|
|
|
|
// ----
|
|
await baal.submitProposal(setShamanAction, 0, 0, '');
|
|
const proposalId = await baal.proposalCount();
|
|
await baal.submitVote(proposalId, true);
|
|
await moveForwardPeriods(daoSettings.VOTING_PERIOD_IN_SECONDS, extraSeconds);
|
|
await baal.processProposal(proposalId, setShamanAction);
|
|
return proposalId;
|
|
};
|
|
|
|
export const verifyProposal = (prop1: any, prop2: any, overrides?: any) => {
|
|
for (let key in prop1) {
|
|
if (Number.isInteger(+key)) {
|
|
continue;
|
|
}
|
|
if (overrides && key in overrides) {
|
|
// console.log('override', key)
|
|
expect(prop1[key]).to.equal(overrides[key]);
|
|
} else {
|
|
// console.log('check', key)
|
|
expect(prop1[key]).to.equal(prop2[key]);
|
|
}
|
|
}
|
|
};
|