diff --git a/src/contracts/Dao.sol b/src/contracts/Dao.sol index d7055d7..2770c72 100644 --- a/src/contracts/Dao.sol +++ b/src/contracts/Dao.sol @@ -6,52 +6,153 @@ import {DaoTokenInterface} from "./interface/DaoTokenInterface.sol"; import {DonationInterface} from "./interface/DonationInterface.sol"; import {Initializable} from "./common/upgradeable/Initializable.sol"; -contract Dao { +contract Dao is DaoInterface, Initializable { ///////////// @notice 아래에 변수 추가 //////////// /// @notice Admin 주소 - + address public admin; /// @notice DAO 토큰 컨트랙트 주소 - + DaoTokenInterface public daoToken; /// @notice 기부 컨트랙트 주소 - + DonationInterface public donation; /// @notice DAO 가입시 필요한 DAO 토큰 수량 - + uint256 public daoMembershipAmount; /// @notice DAO 멤버 리스트 - + address[] public daoMemberList; /// @notice 멤버십 신청자 목록 - + address[] public membershipRequests; ///////////// @notice 아래에 매핑 추가 //////////// /// @notice 주소 -> DAO 멤버 여부 - + mapping(address => bool) public isDaoMember; /// @notice 신청자 주소 -> DAO 멤버십 신청 승인 여부 - + mapping(address => MembershipRequestStatusCode) public membershipRequestStatus; /// @notice 투표 아이디 -> 찬성 투표 수 - + mapping(uint256 => uint256) public voteCountYes; /// @notice 투표 아이디 -> 반대 투표 수 - + mapping(uint256 => uint256) public voteCountNo; /// @notice 투표 아이디 -> 투표 진행 여부 - + mapping(uint256 => bool) public voteInProgress; /// @notice 투표 아이디 -> 투표자 주소 -> 투표 여부 + mapping(uint256 => mapping(address => bool)) public hasVoted; + uint256[50] private __gap; ///////////// @notice 아래에 modifier 추가 //////////// /// @notice DAO 멤버만 접근 가능하도록 설정 + modifier onlyDaoMember() { + require(isDaoMember[msg.sender], "Only Dao members can perform this action"); + _; + } /// @notice 관리자만 접근 가능하도록 설정 + modifier onlyAdmin() { + require(msg.sender == admin, "Only admin can mint tokens"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(DonationInterface _donation, DaoTokenInterface _daoToken) public initializer { + admin = msg.sender; + donation = _donation; + daoToken = _daoToken; + } + + function startVote(uint256 _campaignId) external { + uint256 goalAmount = donation.getCampaignGoal(_campaignId); + uint256 totalAmount = donation.getCampaignTotalAmount(_campaignId); + + voteCountYes[_campaignId] = 0; + voteCountNo[_campaignId] = 0; + voteInProgress[_campaignId] = true; + + emit VoteStarted(_campaignId, goalAmount, totalAmount); + } + + function vote(uint256 _campaignId, bool _agree) public onlyDaoMember { + require(voteInProgress[_campaignId], "No vote in progress for this campaign"); + require(!hasVoted[_campaignId][msg.sender], "You have already voted"); + + hasVoted[_campaignId][msg.sender] = true; + _agree ? voteCountYes[_campaignId] += 1 : voteCountNo[_campaignId] += 1; + + if (voteCountYes[_campaignId] + voteCountNo[_campaignId] == daoMemberList.length) { + voteEnd(_campaignId); + } + + emit Voted(_campaignId, msg.sender, _agree); + } + + function voteEnd(uint256 _campaignId) internal { + uint256 DECIMAL_PRECISION = 1e18; + uint256 agreePercentage = (100 * DECIMAL_PRECISION * voteCountYes[_campaignId]) / daoMemberList.length; + string memory approveMessage = "The campaign has been approved for claim."; + string memory rejectMessage = "he campaign was declined for claim."; + + voteInProgress[_campaignId] = false; + + uint256 threshold = 70 * DECIMAL_PRECISION; + + if (agreePercentage >= threshold) { + donation.claim(_campaignId); + emit VoteEnded(_campaignId, true, agreePercentage, approveMessage); + } else { + emit VoteEnded(_campaignId, false, agreePercentage, rejectMessage); + } + } + + function requestDaoMembership() external { + require(!isDaoMember[msg.sender], "User is already a DAO member"); + require(daoToken.balanceOf(msg.sender) >= daoMembershipAmount, "Insufficient DAO tokens"); + + membershipRequests.push(msg.sender); + membershipRequestStatus[msg.sender] = MembershipRequestStatusCode.PENDING; + + emit DaoMembershipRequested(msg.sender, "User has requested DAO membership"); + } + + function handleDaoMembership(address _user, bool _approve) external onlyAdmin { + if (_approve) { + membershipRequestStatus[_user] = MembershipRequestStatusCode.APPROVED; + daoMemberList.push(_user); + isDaoMember[_user] = true; + + emit DaoMembershipApproved(_user, "User has been approved as a DAO member"); + } else { + membershipRequestStatus[_user] = MembershipRequestStatusCode.REJECTED; + + emit DaoMembershipRejected(_user, "User has been rejected as a DAO member"); + } + } + + function removeDaoMembership(address _user) external { + require(isDaoMember[_user], "User is not a DAO member"); + isDaoMember[_user] = false; + + for (uint256 i = 0; i < daoMemberList.length; i++) { + if (daoMemberList[i] == _user) { + if (i < daoMemberList.length - 1) { + daoMemberList[i] = daoMemberList[daoMemberList.length - 1]; + } + daoMemberList.pop(); + break; + } + } + + emit DaoMembershipRemoved(_user, "User has been removed from DAO membership"); + } - function startVote(uint256 _campaignId) external {} - - function vote(uint256 _campaignId, bool agree) public {} - - function voteEnd(uint256 _campaignId) internal {} - - function requestDaoMembership() external {} - - function handleDaoMembership(address _user, bool _approve) external {} + ///////////// @notice 아래에 set함수 & get함수 추가 //////////// - function removeDaoMembership(address _user) external {} + function getMembershipRequests() external view onlyAdmin returns (address[] memory) { + return membershipRequests; + } - ///////////// @notice 아래에 set함수 & get함수 추가 //////////// + function getDaoList() external view onlyAdmin returns (address[] memory) { + return daoMemberList; + } } diff --git a/src/contracts/Donation.sol b/src/contracts/Donation.sol index bf26135..1942f07 100644 --- a/src/contracts/Donation.sol +++ b/src/contracts/Donation.sol @@ -5,24 +5,33 @@ import "./interface/DaoTokenInterface.sol"; import "./interface/DaoInterface.sol"; import "./interface/DonationInterface.sol"; -contract Donation { +contract Donation is DonationInterface { ///////////// @notice 아래에 변수 추가 //////////// /// @notice Admin 주소 + address public admin; /// @notice 캠페인 아이디 카운트 + uint256 public count; /// @notice DAO 토큰 컨트랙트 주소 + DaoTokenInterface public daoToken; ///////////// @notice 아래에 매핑 추가 //////////// /// @notice 캠페인 아이디 -> 캠페인 구조체 + mapping(uint256 => Campaign) public campaigns; /// @notice 캠페인 아이디 -> 사용자 주소 -> 기부 금액 + mapping(uint256 => mapping(address => uint256)) public pledgedUserToAmount; ///////////// @notice 아래에 생성자 및 컨트랙트 주소 설정 //////////// /// @notice 관리자 및 DAO Token 컨트랙트 주소 설정 + constructor(address daoTokenAddr) { + admin = msg.sender; + daoToken = DaoTokenInterface(daoTokenAddr); + } ///////////// @notice 아래에 modifier 추가 //////////// @@ -37,39 +46,103 @@ contract Donation { uint256 _goal, uint32 _startAt, uint32 _endAt - ) external {} - - function cancel(uint256 _campaignId) external {} - - function pledge(uint256 _campaignId, uint256 _amount) external {} - - function unpledge(uint256 _campaignId, uint256 _amount) external {} - - //2. onlyDao modifier 추가 - function claim(uint256 _campaignId) external {} - - function refund(uint256 _campaignId) external {} + ) external { + require(_startAt >= block.timestamp, "start at < now"); + require(_endAt >= _startAt, "end at < start at"); + require(_endAt <= block.timestamp + 90 days, "end at > max duration"); + + count += 1; + campaigns[count] = Campaign({ + creator: msg.sender, + target: _target, + title: _title, + description: _description, + goal: _goal, + pledged: 0, + startAt: _startAt, + endAt: _endAt, + claimed: false + }); + + emit Launch(count, campaigns[count]); + } + + function cancel(uint256 _campaignId) external { + Campaign memory campaign = campaigns[_campaignId]; + require(msg.sender == campaign.creator, "not creator"); + require(block.timestamp < campaign.startAt, "started"); + + delete campaigns[_campaignId]; + emit Cancel(_campaignId); + } + + function pledge(uint256 _campaignId, uint256 _amount) external { + Campaign storage campaign = campaigns[_campaignId]; + require(block.timestamp >= campaign.startAt, "not started"); + require(!getIsEnded(_campaignId), "Campaign ended"); + require(_amount > 0, "Amount must be greater than zero"); + + campaign.pledged += _amount; + pledgedUserToAmount[_campaignId][msg.sender] += _amount; + daoToken.transferFrom(msg.sender, address(this), _amount); + + emit Pledge(_campaignId, msg.sender, _amount, campaign.pledged); + } + + function unpledge(uint256 _campaignId, uint256 _amount) external { + Campaign storage campaign = campaigns[_campaignId]; + require(_amount > 0, "Amount must be greater than zero"); + require(!getIsEnded(_campaignId), "Campaign ended"); + + campaign.pledged -= _amount; + pledgedUserToAmount[_campaignId][msg.sender] -= _amount; + daoToken.transfer(msg.sender, _amount); + + emit Unpledge(_campaignId, msg.sender, _amount, campaign.pledged); + } + + function claim(uint256 _campaignId) external { + require(getIsEnded(_campaignId), "Campaign not ended"); + + Campaign storage campaign = campaigns[_campaignId]; + require(!campaign.claimed, "claimed"); + + daoToken.transfer(campaign.target, campaign.pledged); + campaign.claimed = true; + + emit Claim(_campaignId, campaign.claimed, campaign.pledged); + } + + function refund(uint256 _campaignId) external { + require(getIsEnded(_campaignId), "Campaign not ended"); + + uint256 bal = pledgedUserToAmount[_campaignId][msg.sender]; + pledgedUserToAmount[_campaignId][msg.sender] = 0; + daoToken.transfer(msg.sender, bal); + + emit Refund(_campaignId, msg.sender, bal); + } ///////////// @notice 아래에 get함수는 필요한 경우 주석을 해제해 사용해주세요 //////////// - // function getIsEnded(uint256 _campaignId) public view returns (bool) { - // Campaign memory campaign = campaigns[_campaignId]; - // return block.timestamp >= campaign.endAt || campaign.pledged >= campaign.goal; - // } + function getIsEnded(uint256 _campaignId) public view returns (bool) { + Campaign memory campaign = campaigns[_campaignId]; + return block.timestamp >= campaign.endAt || campaign.pledged >= campaign.goal; + } - // function getCampaign(uint256 _campaignId) external view returns (Campaign memory) { - // return campaigns[_campaignId]; - // } + function getCampaign(uint256 _campaignId) external view returns (Campaign memory) { + return campaigns[_campaignId]; + } - // function getCampaignCreator(uint256 _campaignId) external view returns (address) { - // return campaigns[_campaignId].creator; - // } + function getCampaignCreator(uint256 _campaignId) external view returns (address) { + return campaigns[_campaignId].creator; + } - // function getCampaignGoal(uint256 _campaignId) external view returns (uint256) { - // return campaigns[_campaignId].goal; - // } + function getCampaignGoal(uint256 _campaignId) external view returns (uint256) { + return campaigns[_campaignId].goal; + } - // function getCampaignTotalAmount(uint256 _campaignId) external view returns (uint256) { - // return campaigns[_campaignId].pledged; - // } + function getCampaignTotalAmount(uint256 _campaignId) external view returns (uint256) { + return campaigns[_campaignId].pledged; + } } diff --git a/src/contracts/interface/DaoInterface.sol b/src/contracts/interface/DaoInterface.sol index 94e44a8..d94f0ac 100644 --- a/src/contracts/interface/DaoInterface.sol +++ b/src/contracts/interface/DaoInterface.sol @@ -23,6 +23,8 @@ interface DaoInterface { function getDaoList() external view returns (address[] memory); + function isDaoMember(address _user) external view returns (bool); + event VoteStarted(uint256 indexed campaignId, uint256 goalAmount, uint256 totalAmount); event Voted(uint256 indexed campaignId, address voter, bool agree); diff --git a/src/scripts/deploy/hardhat/todo/001_deploy_contracts.ts b/src/scripts/deploy/hardhat/todo/001_deploy_contracts.ts index 34807e1..b44fa9e 100644 --- a/src/scripts/deploy/hardhat/todo/001_deploy_contracts.ts +++ b/src/scripts/deploy/hardhat/todo/001_deploy_contracts.ts @@ -16,31 +16,40 @@ const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { autoMine: true, }); - // const DonationContract = await deploy("Donation", { - // from: developer.address, - // contract: "Donation", - // args: [DaoTokenContract.address], - // log: true, - // autoMine: true, - // }); + const DonationContract = await deploy("Donation", { + from: developer.address, + contract: "Donation", + args: [DaoTokenContract.address], + log: true, + autoMine: true, + }); - // const DaoContract = await deploy("Dao", { + //이곳에 코드를 추가할 예정입니다. + const initializeParams = [DaoTokenContract.address, DonationContract.address]; + + await deploy("Dao", { + from: developer.address, + contract: "Dao", + proxy: { + execute: { + init: { + methodName: "initialize", // initializer modifier가 붙은 함수의 이름 + args: initializeParams, // initialize 실행 시 필요한 파라미터, 배열로 전달 + }, + }, + }, + log: true, + autoMine: true, + }); + + // 이후 업그레이드 시 사용 + // await deploy("Dao", { // from: developer.address, // contract: "Dao", - // proxy: { - // execute: { - // init: { - // methodName: "initialize", - // args: [DaoTokenContract.address, DonationContract.address], - // }, - // }, - // }, + // proxy: true, // log: true, // autoMine: true, // }); - - // const donation = await ethers.getContractAt("Donation", DonationContract.address); - // await donation.connect(developer).setDaoAddress(DaoContract.address); }; export default func; diff --git a/src/test/dao.test.ts b/src/test/dao.test.ts index 1b929e5..c05e196 100644 --- a/src/test/dao.test.ts +++ b/src/test/dao.test.ts @@ -9,7 +9,7 @@ // import { HardhatUtil } from "./lib/hardhat_utils"; // import { GAS_PER_TRANSACTION } from "./mock/mock"; -// describe("Dao Token 테스트", () => { +// describe("Dao 테스트", () => { // /* Signer */ // let admin: SignerWithAddress; // let users: SignerWithAddress[]; @@ -43,7 +43,219 @@ // it("Hardhat 환경 배포 테스트", () => { // expect(daoToken.address).to.not.be.undefined; -// expect(dao.address).to.not.be.undefined; // expect(donation.address).to.not.be.undefined; +// expect(dao.address).to.not.be.undefined; +// }); + +// describe("DaoToken 초기화 테스트", () => { +// it("DaoToken의 관리자가 정상적으로 설정되어 있는지 확인", async () => { +// expect(await daoToken.admin()).to.equal(admin.address); +// }); + +// it("DaoToken의 초기 값이 정상적으로 설정되어 있는지 확인", async () => { +// const { daoTokenName, daoTokenSymbol, exchangeRate } = hardhatInfo; +// await Promise.all([ +// expect(await daoToken.name()).to.equal(daoTokenName), +// expect(await daoToken.symbol()).to.equal(daoTokenSymbol), +// expect(await daoToken.exchangeRate()).to.equal(exchangeRate), +// ]); +// }); + +// it("DaoToken의 초기 공급량이 정상적으로 설정되어 있는지 확인", async () => { +// const initialSupply = await daoToken.totalSupply(); +// expect(initialSupply).to.equal(hardhatInfo.initialSupply); + +// const adminBalance = await daoToken.balanceOf(admin.address); +// expect(adminBalance).to.equal(initialSupply); +// }); +// }); + +// describe("Mint 함수 테스트", () => { +// const amount = ethers.utils.parseEther(faker.datatype.number({ min: 1, max: 100 }).toString()); + +// it("DaoToken의 mint 함수가 정상적으로 동작하는지 확인", async () => { +// await daoToken.connect(admin).mint(users[0].address, amount); +// const balance = await daoToken.balanceOf(users[0].address); +// expect(balance).to.equal(amount); +// }); + +// it("DaoToken의 mint 함수가 관리자만 사용 가능한지 확인", async () => { +// await expect(daoToken.connect(users[0]).mint(users[0].address, amount)).to.be.revertedWith( +// "Only admin can mint tokens", +// ); +// }); +// }); + +// describe("BuyToken 함수 테스트", () => { +// const amount = ethers.utils.parseEther(faker.datatype.number({ min: 1, max: 100 }).toString()); + +// it("DaoToken의 buyToken 함수에서 잔고가 부족한 경우 실패하는지 확인", async () => { +// await expect(daoToken.connect(users[0]).buyTokens()).to.be.revertedWith("You must send ETH to buy tokens"); +// }); + +// it("DaoToken의 buyToken 함수 실행 시 가치 교환이 정상적으로 이루어지는지 확인", async () => { +// await daoToken.connect(users[0]).buyTokens({ value: amount }); + +// /* 사용자의 토큰 잔고 확인 */ +// const balance = await daoToken.balanceOf(users[0].address); +// const expectedTokenBalance = HardhatUtil.divExp(amount.mul(hardhatInfo.exchangeRate)); +// expect(balance).to.equal(expectedTokenBalance); + +// /* 컨트랙트의 ETH 잔고 확인 */ +// const contractBalance = await daoToken.getContractBalance(); +// expect(contractBalance).to.equal(amount); +// }); + +// it("DaoToken의 buyToken 함수 실행 시 이벤트가 정상적으로 발생하는지 확인", async () => { +// const expectedTokenBalance = HardhatUtil.divExp(amount.mul(hardhatInfo.exchangeRate)); + +// await expect(daoToken.connect(users[0]).buyTokens({ value: amount })) +// .to.emit(daoToken, "TokensPurchased") +// .withArgs(users[0].address, amount, expectedTokenBalance); // 1 ETH => 100,000 DAO +// }); +// }); + +// describe("SellToken 함수 테스트", () => { +// let tokenBalance: BigNumber; +// beforeEach(async () => { +// /* 토큰 구매 */ +// const amount = ethers.utils.parseEther(faker.datatype.number({ min: 1, max: 100 }).toString()); +// await daoToken.connect(users[0]).buyTokens({ value: amount }); +// tokenBalance = await daoToken.balanceOf(users[0].address); +// }); + +// it("DaoToken의 sellToken 함수 실행 시 이벤트가 정상적으로 발생하는지 확인", async () => { +// const expectedETHAmount = HardhatUtil.mulExp(tokenBalance).div(hardhatInfo.exchangeRate); + +// await expect(daoToken.connect(users[0]).sellTokens(tokenBalance)) +// .to.emit(daoToken, "TokensWithdrawn") +// .withArgs(users[0].address, expectedETHAmount); +// }); + +// it("sellToken 함수 실행 시 정상적으로 가치 교환이 이루어지는지 확인", async () => { +// const expectedETHAmount = HardhatUtil.mulExp(tokenBalance).div(hardhatInfo.exchangeRate); +// const balanceBefore = await ethers.provider.getBalance(users[0].address); + +// await daoToken.connect(users[0]).sellTokens(tokenBalance); + +// /* 사용자의 토큰 잔고 확인 */ +// const balanceAfter = await ethers.provider.getBalance(users[0].address); +// expect(balanceAfter).to.closeTo(balanceBefore.add(expectedETHAmount), GAS_PER_TRANSACTION); + +// /* 컨트랙트의 ETH 잔고 확인 */ +// const contractBalance = await daoToken.getContractBalance(); +// expect(contractBalance).to.equal(0); +// }); + +// it("DaoToken의 sellToken 함수에서 잔고가 부족한 경우 실패하는지 확인", async () => { +// await daoToken.connect(users[0]).sellTokens(tokenBalance); +// await expect(daoToken.connect(users[0]).sellTokens(tokenBalance)).to.be.revertedWith("Insufficient balance"); +// }); +// }); + +// it("ExchangeRate 변경이 정상적으로 이루어지는지 확인", async () => { +// const newExchangeRate = ethers.utils.parseEther(faker.datatype.float({ min: 0.01, max: 1 }).toString()); +// await daoToken.connect(admin).setExchangeRate(newExchangeRate); + +// expect(await daoToken.exchangeRate()).to.equal(newExchangeRate); +// }); + +// describe("DAO 멤버십 테스트", () => { +// const membershipAmount = ethers.utils.parseEther("100"); + +// it("DAO 멤버십 요청이 정상적으로 동작하는지 확인", async () => { +// const user = users[0]; +// await daoToken.connect(admin).transfer(user.address, membershipAmount); +// await dao.connect(user).requestDaoMembership(); + +// const requestStatus = await dao.membershipRequestStatus(user.address); +// expect(requestStatus).to.equal(0); // PENDING 상태 +// }); + +// it("DAO 멤버십 승인 테스트", async () => { +// const user = users[0]; +// await daoToken.connect(admin).transfer(user.address, membershipAmount); +// await dao.connect(user).requestDaoMembership(); +// await dao.connect(admin).handleDaoMembership(user.address, true); + +// const isMember = await dao.isDaoMember(user.address); +// expect(isMember).to.be.true; + +// const daoMemberList = await dao.getDaoList(); +// expect(daoMemberList).to.include(user.address); +// }); + +// it("DAO 멤버십 거절 테스트", async () => { +// const user = users[1]; +// await daoToken.connect(admin).transfer(user.address, membershipAmount); +// await dao.connect(user).requestDaoMembership(); +// await dao.connect(admin).handleDaoMembership(user.address, false); + +// const isMember = await dao.isDaoMember(user.address); +// expect(isMember).to.be.false; + +// const requestStatus = await dao.membershipRequestStatus(user.address); +// expect(requestStatus).to.equal(2); // REJECTED 상태 +// }); + +// describe("투표 기능 테스트", () => { +// const goalAmount = ethers.utils.parseEther("1000"); +// const totalAmount = ethers.utils.parseEther("500"); +// let campaignId: number; + +// beforeEach(async () => { +// // 새로운 캠페인을 생성하고 기부를 수행합니다. +// await donation.connect(admin).launch( +// users[0].address, // 타겟 +// "테스트 캠페인", // 제목 +// "테스트용 캠페인 설명", // 설명 +// goalAmount, // 목표 금액 +// Math.floor(new Date().getTime() / 1000) + 100, // 시작 시간 +// Math.floor(new Date().getTime() / 1000) + 200, // 종료 시간 +// ); +// campaignId = 1; // 캠페인 ID는 1로 설정했다고 가정합니다. + +// await donation.connect(admin).pledge(campaignId, totalAmount); +// }); + +// it("투표 시작 테스트", async () => { +// await dao.connect(admin).startVote(campaignId); + +// const inProgress = await dao.voteInProgress(campaignId); +// expect(inProgress).to.be.true; +// }); + +// it("투표 진행 중에 다시 시작하려고 하면 실패하는지 확인", async () => { +// await dao.connect(admin).startVote(campaignId); +// await expect(dao.connect(admin).startVote(campaignId)).to.be.revertedWith("Vote is already in progress"); +// }); + +// it("투표 종료 테스트", async () => { +// const user = users[0]; +// await daoToken.connect(admin).transfer(user.address, membershipAmount); +// await dao.connect(user).requestDaoMembership(); +// await dao.connect(admin).handleDaoMembership(user.address, true); + +// await dao.connect(admin).startVote(campaignId); +// await dao.connect(user).vote(campaignId, true); + +// const inProgress = await dao.voteInProgress(campaignId); +// expect(inProgress).to.be.false; + +// const voteCountYes = await dao.voteCountYes(campaignId); +// expect(voteCountYes).to.equal(1); +// }); + +// it("중복 투표가 불가능한지 확인", async () => { +// const user = users[0]; +// await daoToken.connect(admin).transfer(user.address, membershipAmount); +// await dao.connect(user).requestDaoMembership(); +// await dao.connect(admin).handleDaoMembership(user.address, true); + +// await dao.connect(admin).startVote(campaignId); +// await dao.connect(user).vote(campaignId, true); +// await expect(dao.connect(user).vote(campaignId, true)).to.be.revertedWith("User has already voted"); +// }); +// }); // }); // }); diff --git a/src/test/donation.test.ts b/src/test/donation.test.ts index 1b929e5..3fc1de8 100644 --- a/src/test/donation.test.ts +++ b/src/test/donation.test.ts @@ -1,49 +1,319 @@ -// import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; -// import { setup } from "./setup"; -// import { DaoToken, Dao, Donation } from "@typechains"; -// import { expect } from "chai"; -// import { ethers, network } from "hardhat"; -// import { hardhatInfo } from "@constants"; -// import { faker } from "@faker-js/faker"; -// import { BigNumber } from "ethers"; -// import { HardhatUtil } from "./lib/hardhat_utils"; -// import { GAS_PER_TRANSACTION } from "./mock/mock"; - -// describe("Dao Token 테스트", () => { -// /* Signer */ -// let admin: SignerWithAddress; -// let users: SignerWithAddress[]; - -// /* 컨트랙트 객체 */ -// let daoToken: DaoToken; -// let dao: Dao; -// let donation: Donation; - -// /* 테스트 스냅샷 */ -// let initialSnapshotId: number; -// let snapshotId: number; - -// before(async () => { -// /* 테스트에 필요한 컨트랙트 및 Signer 정보를 불러오는 함수 */ -// ({ admin, users, daoToken, dao, donation } = await setup()); -// initialSnapshotId = await network.provider.send("evm_snapshot"); -// }); - -// beforeEach(async () => { -// snapshotId = await network.provider.send("evm_snapshot"); -// }); - -// afterEach(async () => { -// await network.provider.send("evm_revert", [snapshotId]); -// }); - -// after(async () => { -// await network.provider.send("evm_revert", [initialSnapshotId]); -// }); - -// it("Hardhat 환경 배포 테스트", () => { -// expect(daoToken.address).to.not.be.undefined; -// expect(dao.address).to.not.be.undefined; -// expect(donation.address).to.not.be.undefined; -// }); -// }); +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signers"; +import { setup } from "./setup"; +import { DaoToken, Dao, Donation } from "@typechains"; +import { expect } from "chai"; +import { ethers, network } from "hardhat"; +import { HardhatUtil } from "./lib/hardhat_utils"; +import { mockCampaign } from "./mock/mock"; + +describe("Donation 테스트", () => { + /* Signer */ + let admin: SignerWithAddress; + let users: SignerWithAddress[]; + + /* 컨트랙트 객체 */ + let daoToken: DaoToken; + let donation: Donation; + + /* 테스트 스냅샷 */ + let initialSnapshotId: number; + let snapshotId: number; + + before(async () => { + /* 테스트에 필요한 컨트랙트 및 Signer 정보를 불러오는 함수 */ + ({ admin, users, daoToken, donation } = await setup()); + initialSnapshotId = await network.provider.send("evm_snapshot"); + }); + + beforeEach(async () => { + snapshotId = await network.provider.send("evm_snapshot"); + }); + + afterEach(async () => { + await network.provider.send("evm_revert", [snapshotId]); + }); + + after(async () => { + await network.provider.send("evm_revert", [initialSnapshotId]); + }); + + it("Hardhat 환경 배포 테스트", () => { + expect(daoToken.address).to.not.be.undefined; + expect(donation.address).to.not.be.undefined; + }); + + describe("캠페인 생성(Launch) 테스트", () => { + it("launch 함수가 시작 시간이 현재 시간보다 이전인 경우 실패하는지 확인", async () => { + const campaignData = mockCampaign({ startAt: Math.floor(Date.now() / 1000) - 1000 }); + const { target, title, description, goal, startAt, endAt } = campaignData; + + await expect( + donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt), + ).to.be.revertedWith("start at < now"); + }); + + it("launch 함수가 종료 시간이 시작 시간보다 이전인 경우 실패하는지 확인", async () => { + const campaignData = mockCampaign({ + startAt: Math.floor(Date.now() / 1000) + 1000, + endAt: Math.floor(Date.now() / 1000) - 1000, + }); + const { target, title, description, goal, startAt, endAt } = campaignData; + + await expect( + donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt), + ).to.be.revertedWith("end at < start at"); + }); + + it("launch 함수가 종료 시간이 90일을 초과하는 경우 실패하는지 확인", async () => { + const campaignData = mockCampaign({ endAt: Math.floor(Date.now() / 1000) + 100 * 24 * 60 * 60 }); + const { target, title, description, goal, startAt, endAt } = campaignData; + + await expect( + donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt), + ).to.be.revertedWith("end at > max duration"); + }); + + it("launch 함수 실행 후 캠페인 정보가 정상적으로 등록되는지 확인", async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + + const campaign = await donation.campaigns(1); + expect(campaign.creator).to.equal(users[0].address); + expect(campaign.target).to.equal(target); + expect(campaign.title).to.equal(title); + expect(campaign.description).to.equal(description); + expect(campaign.goal).to.equal(goal); + expect(campaign.startAt).to.equal(startAt); + expect(campaign.endAt).to.equal(endAt); + expect(campaign.pledged).to.equal(0); + expect(campaign.claimed).to.equal(false); + + expect(await donation.count()).to.equal(1); + }); + + it("launch 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + + await expect(donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt)) + .to.emit(donation, "Launch") + .withArgs(1, [users[0].address, target, title, description, goal, 0, startAt, endAt, false]); + }); + }); + + describe("캠페인 취소(Cancel) 테스트", () => { + beforeEach(async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + }); + + it("cancel 함수가 캠페인 생성자에 의해 호출되지 않는 경우 실패하는지 확인", async () => { + await expect(donation.connect(users[1]).cancel(1)).to.be.revertedWith("not creator"); + }); + + it("cancel 함수가 캠페인이 시작된 후 호출되는 경우 실패하는지 확인", async () => { + const campaign = await donation.campaigns(1); + const currentTime = await HardhatUtil.blockTimeStamp(); + const startInFuture = campaign.startAt - currentTime + 10; + + await HardhatUtil.passNSeconds(startInFuture); + + await expect(donation.connect(users[0]).cancel(1)).to.be.revertedWith("started"); + }); + + it("cancel 함수가 정상적으로 실행되는지 확인", async () => { + const newCampaignData = mockCampaign({ startAt: Math.floor(Date.now() / 1000) + 5000 }); + const { target, title, description, goal, startAt, endAt } = newCampaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + + await donation.connect(users[0]).cancel(2); + const campaign = await donation.campaigns(2); + + expect(campaign.creator).to.equal(ethers.constants.AddressZero); + }); + + it("cancel 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + const newCampaignData = mockCampaign({ startAt: Math.floor(Date.now() / 1000) + 5000 }); + const { target, title, description, goal, startAt, endAt } = newCampaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + + await expect(donation.connect(users[0]).cancel(2)).to.emit(donation, "Cancel").withArgs(2); + }); + }); + + describe("기부(Pledge) 테스트", () => { + beforeEach(async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + await HardhatUtil.setNextBlockTimestamp(startAt); + await HardhatUtil.mineNBlocks(1); + }); + + it("pledge 함수가 캠페인이 종료된 경우 실패하는지 확인", async () => { + const amount = HardhatUtil.ToETH(1); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + + await expect(donation.connect(users[1]).pledge(1, amount)).to.be.revertedWith("Campaign ended"); + }); + + it("pledge 함수가 0 이상의 금액을 기부하는지 확인", async () => { + const amount = HardhatUtil.ToETH(0); + + await expect(donation.connect(users[1]).pledge(1, amount)).to.be.revertedWith("Amount must be greater than zero"); + }); + + it("pledge 함수 실행 후 캠페인에 기부금이 정상적으로 반영되는지 확인", async () => { + const amount = HardhatUtil.ToETH(1); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + + await donation.connect(users[1]).pledge(1, amount); + const campaign = await donation.campaigns(1); + + expect(campaign.pledged).to.equal(amount); + }); + + it("pledge 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + const amount = HardhatUtil.ToETH(1); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + + await expect(donation.connect(users[1]).pledge(1, amount)) + .to.emit(donation, "Pledge") + .withArgs(1, users[1].address, amount, amount); + }); + }); + + describe("기부 취소(Unpledge) 테스트", () => { + beforeEach(async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + await HardhatUtil.setNextBlockTimestamp(startAt); + await HardhatUtil.mineNBlocks(1); + + const amount = HardhatUtil.ToETH(1); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + await donation.connect(users[1]).pledge(1, amount); + }); + + it("unpledge 함수가 0 이상의 금액을 기부 취소하는지 확인", async () => { + const amount = HardhatUtil.ToETH(0); + + await expect(donation.connect(users[1]).unpledge(1, amount)).to.be.revertedWith( + "Amount must be greater than zero", + ); + }); + + it("unpledge 함수가 캠페인이 종료된 경우 실패하는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + + await expect(donation.connect(users[1]).unpledge(1, HardhatUtil.ToETH(1))).to.be.revertedWith("Campaign ended"); + }); + + it("unpledge 함수 실행 후 캠페인에 기부 취소 금액이 정상적으로 반영되는지 확인", async () => { + const amount = HardhatUtil.ToETH(1); + + await donation.connect(users[1]).unpledge(1, amount); + const campaign = await donation.campaigns(1); + + expect(campaign.pledged).to.equal(HardhatUtil.ToETH(0)); + }); + + it("unpledge 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + const amount = HardhatUtil.ToETH(1); + + await expect(donation.connect(users[1]).unpledge(1, amount)) + .to.emit(donation, "Unpledge") + .withArgs(1, users[1].address, amount, HardhatUtil.ToETH(0)); + }); + }); + + describe("기부금 수령(Claim) 테스트", () => { + beforeEach(async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + await HardhatUtil.setNextBlockTimestamp(startAt); + await HardhatUtil.mineNBlocks(1); + + const amount = HardhatUtil.ToETH(10); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + await donation.connect(users[1]).pledge(1, amount); + }); + + it("claim 함수가 캠페인이 종료되지 않은 경우 실패하는지 확인", async () => { + await expect(donation.connect(users[0]).claim(1)).to.be.revertedWith("Campaign not ended"); + }); + + it("claim 함수가 이미 수령된 캠페인에서 호출된 경우 실패하는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + + await donation.connect(users[0]).claim(1); + + await expect(donation.connect(users[0]).claim(1)).to.be.revertedWith("claimed"); + }); + + it("claim 함수 실행 후 기부금이 정상적으로 수령되는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + await donation.connect(users[0]).claim(1); + + const campaign = await donation.campaigns(1); + expect(campaign.claimed).to.be.true; + }); + + it("claim 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + await expect(donation.connect(users[0]).claim(1)) + .to.emit(donation, "Claim") + .withArgs(1, true, HardhatUtil.ToETH(10)); + }); + }); + + describe("환불(Refund) 테스트", () => { + beforeEach(async () => { + const campaignData = mockCampaign(); + const { target, title, description, goal, startAt, endAt } = campaignData; + await donation.connect(users[0]).launch(target, title, description, goal, startAt, endAt); + await HardhatUtil.setNextBlockTimestamp(startAt); + await HardhatUtil.mineNBlocks(1); + + const amount = HardhatUtil.ToETH(1); + await daoToken.transfer(users[1].address, amount); + await daoToken.connect(users[1]).approve(donation.address, amount); + await donation.connect(users[1]).pledge(1, amount); + }); + + it("refund 함수가 캠페인이 종료되지 않은 경우 실패하는지 확인", async () => { + await expect(donation.connect(users[1]).refund(1)).to.be.revertedWith("Campaign not ended"); + }); + + it("refund 함수 실행 후 기부자가 환불받는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + + const initialBalance = await daoToken.balanceOf(users[1].address); + + await donation.connect(users[1]).refund(1); + const finalBalance = await daoToken.balanceOf(users[1].address); + + expect(finalBalance).to.equal(initialBalance.add(HardhatUtil.ToETH(1))); + }); + + it("refund 함수 실행 후 이벤트가 정상적으로 발생하는지 확인", async () => { + await HardhatUtil.setNextBlockTimestamp((await donation.campaigns(1)).endAt); + + await expect(donation.connect(users[1]).refund(1)) + .to.emit(donation, "Refund") + .withArgs(1, users[1].address, HardhatUtil.ToETH(1)); + }); + }); +}); diff --git a/src/test/setup.ts b/src/test/setup.ts index 5bdeddb..cfe508c 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -9,8 +9,8 @@ export const setup = async () => { await deployments.fixture(["DaoToken", "Donation", "Dao"]); const contracts = { daoToken: await ethers.getContract("DaoToken"), - // donation: await ethers.getContract("Donation"), - // dao: await ethers.getContract("Dao"), + donation: await ethers.getContract("Donation"), + dao: await ethers.getContract("Dao"), }; return {