From c59a028f391e0d711cde36e09fa354ae827b68d2 Mon Sep 17 00:00:00 2001 From: audsssy Date: Fri, 3 Nov 2023 12:01:44 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20update=20contracts=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/KaliBerger.sol | 88 ++- src/interface/IKaliTokenManager.sol | 2 + src/tokens/PatronCertificate.sol | 5 - test/KaliBerger.t.sol | 919 ++++++++++++++++------------ 4 files changed, 587 insertions(+), 427 deletions(-) diff --git a/src/KaliBerger.sol b/src/KaliBerger.sol index fed16f6..4f74aa1 100644 --- a/src/KaliBerger.sol +++ b/src/KaliBerger.sol @@ -97,6 +97,9 @@ contract KaliBerger is Storage { // Transfer ERC721 back to creator IERC721(token).safeTransferFrom(address(this), msg.sender, tokenId); + + deleteTokenPurchaseStatus(token, tokenId); + // TODO: Consider selfdestruct ImpactDAO } /// ----------------------------------------------------------------------- @@ -109,7 +112,12 @@ contract KaliBerger is Storage { /// @param token ERC721 token address. /// @param tokenId ERC721 tokenId. /// @param sale Confirm or reject use of Harberger Tax for escrowed ERC721. - function approve(address token, uint256 tokenId, bool sale, string calldata detail) external payable onlyOperator { + function approve(address token, uint256 tokenId, bool sale, string calldata detail) + external + payable + initialized + onlyOperator + { if (IERC721(token).ownerOf(tokenId) != address(this)) revert NotAuthorized(); address owner = this.getCreator(token, tokenId); @@ -126,7 +134,7 @@ contract KaliBerger is Storage { setTimeAcquired(token, tokenId, block.timestamp); setOwner(token, tokenId, address(this)); if (sale) setTokenPurchaseStatus(token, tokenId, sale); - if (bytes(detail).length > 0) _setTokenDetail(token, tokenId, detail); + _setTokenDetail(token, tokenId, detail); } } @@ -177,9 +185,9 @@ contract KaliBerger is Storage { bytes[] memory extensionsData = new bytes[](1); extensionsData[0] = "0x0"; - // Provide KaliDAO governance settings + // Provide KaliDAO governance settings. uint32[16] memory govSettings; - govSettings = [uint32(300), 0, 20, 52, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; + govSettings = [uint32(86400), 0, 20, 60, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; // Summon a KaliDAO uint256 count = this.getBergerCount(); @@ -211,6 +219,9 @@ contract KaliBerger is Storage { /// @param dao ImpactDAO summoned for ERC721. function _balance(address token, uint256 tokenId, address dao) private { uint256 count = this.getPatronCount(token, tokenId); + address creator = this.getCreator(token, tokenId); + + // Balance token amount per patron. for (uint256 i = 1; i <= count;) { // Retrieve patron and patron contribution. address _patron = this.getPatron(token, tokenId, i); @@ -219,22 +230,28 @@ contract KaliBerger is Storage { // Retrieve KaliDAO balance data. uint256 _contribution = IERC20(dao).balanceOf(_patron); - // Retrieve creator. - address creator = this.getCreator(token, tokenId); - // Determine to mint or burn. if (contribution > _contribution) { IKaliTokenManager(dao).mintTokens(creator, contribution - _contribution); IKaliTokenManager(dao).mintTokens(_patron, contribution - _contribution); } else if (contribution < _contribution) { - IKaliTokenManager(dao).burnTokens(creator, _contribution - contribution); - IKaliTokenManager(dao).burnTokens(_patron, _contribution - contribution); + IKaliTokenManager(dao).burnTokens(_patron, (_contribution - contribution)); } unchecked { ++i; } } + + uint256 creatorBalance = IERC20(dao).balanceOf(creator); + uint256 totalCollected = this.getTotalCollected(token, tokenId); + + // Balance creator token amount. + if (creatorBalance > totalCollected) { + IKaliTokenManager(dao).burnTokens(creator, creatorBalance - totalCollected); + } else if (totalCollected > creatorBalance) { + IKaliTokenManager(dao).mintTokens(creator, totalCollected - creatorBalance); + } } /// ----------------------------------------------------------------------- @@ -328,29 +345,18 @@ contract KaliBerger is Storage { /// Setter Logic /// ----------------------------------------------------------------------- - function setKaliDaoFactory(address factory) external payable onlyOperator { + function setKaliDaoFactory(address factory) external payable initialized onlyOperator { _setAddress(keccak256(abi.encodePacked("dao.factory")), factory); } - function _setKaliDaoFactory(address factory) internal { - _setAddress(keccak256(abi.encodePacked("dao.factory")), factory); - } - - function setCertificateMinter(address factory) external payable onlyOperator { - _setAddress(keccak256(abi.encodePacked("certificate.minter")), factory); - } - - function _setCertificateMinter(address factory) internal { + function setCertificateMinter(address factory) external payable initialized onlyOperator { _setAddress(keccak256(abi.encodePacked("certificate.minter")), factory); } - function setImpactDao(address token, uint256 tokenId, address impactDao) internal { - _setAddress(keccak256(abi.encode(token, tokenId, ".impactDao")), impactDao); - } - function setTax(address token, uint256 tokenId, uint256 _tax) external payable + initialized onlyOperator collectPatronage(token, tokenId) { @@ -358,16 +364,34 @@ contract KaliBerger is Storage { _setUint(keccak256(abi.encode(token, tokenId, ".tax")), _tax); } - function setCreator(address token, uint256 tokenId, address creator) internal { - _setAddress(keccak256(abi.encode(token, tokenId, ".creator")), creator); + function setTokenDetail(address token, uint256 tokenId, string calldata detail) + external + payable + initialized + onlyOperator + { + if (this.getOwner(token, tokenId) == address(0)) revert NotInitialized(); + _setString(keccak256(abi.encode(token, tokenId, ".detail")), detail); } - function setTokenDetail(address token, uint256 tokenId, string calldata detail) external payable onlyOperator { - _setString(keccak256(abi.encode(token, tokenId, ".detail")), detail); + function _setKaliDaoFactory(address factory) internal { + _setAddress(keccak256(abi.encodePacked("dao.factory")), factory); + } + + function _setCertificateMinter(address factory) internal { + _setAddress(keccak256(abi.encodePacked("certificate.minter")), factory); + } + + function setImpactDao(address token, uint256 tokenId, address impactDao) internal { + _setAddress(keccak256(abi.encode(token, tokenId, ".impactDao")), impactDao); + } + + function setCreator(address token, uint256 tokenId, address creator) internal { + _setAddress(keccak256(abi.encode(token, tokenId, ".creator")), creator); } function _setTokenDetail(address token, uint256 tokenId, string calldata detail) internal { - _setString(keccak256(abi.encode(token, tokenId, ".detail")), detail); + if (bytes(detail).length > 0) _setString(keccak256(abi.encode(token, tokenId, ".detail")), detail); } function setTokenPurchaseStatus(address token, uint256 tokenId, bool _forSale) internal { @@ -537,9 +561,9 @@ contract KaliBerger is Storage { /// Delete Logic /// ----------------------------------------------------------------------- - function deletePrice(address token, uint256 tokenId) internal { - deleteUint(keccak256(abi.encode(token, tokenId, ".price"))); - } + // function deletePrice(address token, uint256 tokenId) internal { + // deleteUint(keccak256(abi.encode(token, tokenId, ".price"))); + // } function deleteDeposit(address token, uint256 tokenId) internal { deleteUint(keccak256(abi.encode(token, tokenId, ".deposit"))); @@ -731,7 +755,7 @@ contract KaliBerger is Storage { if (price != currentPrice || newPrice == 0 || currentPrice > msg.value) revert InvalidAmount(); // Add purchase price to patron contribution. - addPatronContribution(token, tokenId, msg.sender, price); + // addPatronContribution(token, tokenId, msg.sender, price); // Retrieve deposit, if any. uint256 deposit = this.getDeposit(token, tokenId); diff --git a/src/interface/IKaliTokenManager.sol b/src/interface/IKaliTokenManager.sol index 6bb1279..5c995fb 100644 --- a/src/interface/IKaliTokenManager.sol +++ b/src/interface/IKaliTokenManager.sol @@ -8,4 +8,6 @@ interface IKaliTokenManager { function burnTokens(address from, uint256 amount) external; function balanceOf(address account) external view returns (uint256); + + function totalSupply() external view returns (uint256); } diff --git a/src/tokens/PatronCertificate.sol b/src/tokens/PatronCertificate.sol index db1ae9a..719859f 100644 --- a/src/tokens/PatronCertificate.sol +++ b/src/tokens/PatronCertificate.sol @@ -19,15 +19,10 @@ contract PatronCertificate { /// ----------------------------------------------------------------------- error NotMinted(); - error ZeroAddress(); - error Unauthorized(); - error InvalidRecipient(); - error UnsafeRecipient(); - error AlreadyMinted(); /// ----------------------------------------------------------------------- diff --git a/test/KaliBerger.t.sol b/test/KaliBerger.t.sol index edb8d34..4d9049b 100644 --- a/test/KaliBerger.t.sol +++ b/test/KaliBerger.t.sol @@ -47,7 +47,7 @@ contract KaliBergerTest is Test { address[] extensions; bytes[] extensionsData; address[] voters = [address(alfred)]; - uint256[] shares = [10]; + uint256[] tokens = [10]; uint32[16] govSettings = [uint32(300), 0, 20, 52, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; /// ----------------------------------------------------------------------- @@ -60,7 +60,7 @@ contract KaliBergerTest is Test { daoTemplate = new KaliDAO(); factory = new KaliDAOfactory(payable(daoTemplate)); factory.deployKaliDAO( - "Berger Council", "BC", " ", true, extensions, extensionsData, voters, shares, govSettings + "Berger Council", "BC", " ", true, extensions, extensionsData, voters, tokens, govSettings ); // Deploy and initialize KaliBerger contract. @@ -70,10 +70,6 @@ contract KaliBergerTest is Test { // Deploy and initialize PatronCertificate contract. patronCertificate = new PatronCertificate(address(kaliBerger)); - // Initialize. - vm.prank(dao); - kaliBerger.initialize(dao, address(factory), address(patronCertificate)); - // Deploy 3 ERC721 tokens. token_1 = deployErc721(); token_2 = deployErc721(); @@ -88,420 +84,185 @@ contract KaliBergerTest is Test { // Mint Charlie an ERC721. mintErc721(token_3, 1, charlie); - vm.warp(100); - } // 100 + vm.warp(block.timestamp + 1000); + } /// ----------------------------------------------------------------------- - /// Helper Logic + /// Initialization Test /// ----------------------------------------------------------------------- - /// @notice Deploy ERC721. - function deployErc721() internal returns (MockERC721 token) { - token = new MockERC721("TEST", "TEST"); + /// @notice Initialize KaliBerger. + function testInitialized() public payable { + initialize(dao, address(factory), address(patronCertificate)); + assertEq(kaliBerger.getKaliDaoFactory(), address(factory)); + assertEq(kaliBerger.getCertificateMinter(), address(patronCertificate)); } - /// @notice Mint ERC721. - function mintErc721(MockERC721 token, uint256 tokenId, address recipient) internal { - if (address(token) == address(0)) { - token = deployErc721(); - } + /// @notice Update KaliDAO factory. + function testFactory() public payable { + initialize(dao, address(factory), address(patronCertificate)); - // Mint recipient an ERC721 - token.mint(recipient, tokenId); - assertEq(token.balanceOf(recipient), tokenId); + vm.prank(dao); + kaliBerger.setKaliDaoFactory(earn); + assertEq(kaliBerger.getKaliDaoFactory(), address(earn)); } - /// @notice Escrow ERC721. - function escrow(address user, MockERC721 token, uint256 tokenId) internal { - // Approve KaliBerger to transfer ERC721 - vm.prank(user); - token.approve(address(kaliBerger), tokenId); - vm.warp(200); - - // User escrows ERC721 with KaliBerger - vm.prank(user); - kaliBerger.escrow(address(token), tokenId, user); - vm.warp(300); + /// @notice Update KaliDAO factory. + function testFactory_NotInitialized() public payable { + vm.expectRevert(KaliBerger.NotInitialized.selector); + vm.prank(dao); + kaliBerger.setKaliDaoFactory(earn); + } - // Validate - assertEq(token.balanceOf(user), 0); - assertEq(token.balanceOf(address(kaliBerger)), tokenId); - } // 300 + /// @notice Update Patron Certificate. + function testMinter() public payable { + initialize(dao, address(factory), address(patronCertificate)); - /// @notice Approve ERC721 for purchase. - function approve(MockERC721 token, uint256 tokenId, string memory detail) public payable { - // DAO approves ERC721 for sale vm.prank(dao); - kaliBerger.approve(address(token), tokenId, true, detail); - - // Validate - assertEq(kaliBerger.getTokenPurchaseStatus(address(token), tokenId), true); - assertEq(kaliBerger.getTokenDetail(address(token), tokenId), detail); - assertEq(kaliBerger.getOwner(address(token), tokenId), address(kaliBerger)); + kaliBerger.setCertificateMinter(earn); + assertEq(kaliBerger.getCertificateMinter(), address(earn)); } - /// @notice Validate amount of patronage to collect. - function validatePatronageToCollect(MockERC721 token, uint256 tokenId) public payable { - uint256 amount = kaliBerger.getPrice(address(token), tokenId) - * (block.timestamp - kaliBerger.getTimeLastCollected(address(token), tokenId)) - * kaliBerger.getTax(address(token), tokenId) / 365 days / 100; - assertEq(kaliBerger.patronageToCollect(address(token), tokenId), amount); - // emit log_uint(amount); + /// @notice Update Patron Certificate. + function testMinter_NotInitialized() public payable { + vm.expectRevert(KaliBerger.NotInitialized.selector); + vm.prank(dao); + kaliBerger.setCertificateMinter(earn); } - /// @notice Rebalance DAO tokens. - function balanceDao(uint256 timestamp, address token, uint256 tokenId, address creator) public payable { - // Validate - vm.warp(timestamp); - - // Darius balances a DAO for everyone. - vm.prank(darius); - kaliBerger.balanceDao(token, tokenId); - - // Retrieve token balances to validate DAO is in balance. - address impactDao = kaliBerger.getImpactDao(token, tokenId); - uint256 creator_balance = IERC20(impactDao).balanceOf(creator); - uint256 alfred_balance = IERC20(impactDao).balanceOf(alfred); - uint256 bob_balance = IERC20(impactDao).balanceOf(bob); - uint256 charlie_balance = IERC20(impactDao).balanceOf(charlie); - uint256 darius_balance = IERC20(impactDao).balanceOf(darius); - uint256 earn_balance = IERC20(impactDao).balanceOf(earn); + /// @notice With KaliBerger uninitialized, Bob tries to buy tokens and gets an NotInitialized() error. + function testNotInitialized() public payable { + // Deal Bob ether + vm.deal(bob, 10 ether); - // Validate - if (creator == alfred) assertEq(creator_balance, bob_balance + charlie_balance + earn_balance + darius_balance); - if (creator == bob) assertEq(creator_balance, alfred_balance + charlie_balance + earn_balance + darius_balance); - if (creator == charlie) assertEq(creator_balance, alfred_balance + bob_balance + earn_balance + darius_balance); - // emit log_uint(alfred_balance); - // emit log_uint(bob_balance); - // emit log_uint(charlie_balance); - // emit log_uint(darius_balance); - // emit log_uint(earn_balance); - } + // Bob buys + vm.expectRevert(KaliBerger.NotInitialized.selector); + vm.prank(bob); + kaliBerger.buy{value: 0.1 ether}(address(token_3), 1, 1 ether, 0); + } // timestamp: 1500 - /// @notice Buy ERC721. - function primaryBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) - public - payable - { - vm.warp(block.timestamp + 1000); + /// ----------------------------------------------------------------------- + /// Escrow & Approve Test + /// ----------------------------------------------------------------------- - // Deal buyer ether if buyer does not have any ether. - if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + /// @notice The Gang escrows their tokens. + function testEscrow() public payable { + initialize(dao, address(factory), address(patronCertificate)); - // Get berger count before purchase. - uint256 count = kaliBerger.getBergerCount(); + escrow(alfred, token_1, 1); + escrow(bob, token_2, 1); + escrow(charlie, token_3, 1); + } // 300 - // Buyer buys. - vm.prank(buyer); - kaliBerger.buy{value: 0.1 ether}(token, tokenId, newPrice, 0); + /// @notice DAO approves all tokens for purchase and adds custom detail. + function testApprove() public payable { + // Escrow. + testEscrow(); // 300 - // Validate summoning of ImpactDAO. - assertEq(kaliBerger.getBergerCount(), count == 0 ? 1 : ++count); + // DAO approves. + vm.warp(500); + approve(token_1, 1, "Alfred NFT"); // 500x - // Validate ownership of Patron Certificate for token_1, #1. - assertEq( - IPatronCertificate(address(patronCertificate)).ownerOf( - IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) - ), - buyer - ); + // DAO approves. + vm.warp(1000); + approve(token_2, 1, "Bob NFT"); // 1000 - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } + // DAO approves. + vm.warp(2000); + approve(token_3, 1, "Charlie NFT"); // 2000 + } // 500, 1000, 2000 - /// @notice Buy ERC721. - function secondaryBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) - public - payable - { - vm.warp(block.timestamp + 1000); + /// @notice Validate patronage after Approve + function testApprove_PatronageToCollect() public payable { + uint256 timestamp = 10000000; + uint256 tokenId = 1; - // Deal buyer ether if buyer does not have any ether. - if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + testApprove(); + vm.warp(timestamp); - // Retrieve data for validation. - address impactDao = kaliBerger.getImpactDao(address(token), tokenId); - uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); - uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances + // Validate + validatePatronageToCollect(token_1, tokenId); + validatePatronageToCollect(token_2, tokenId); + validatePatronageToCollect(token_3, tokenId); + } // timestamp: 10000000 - // Get current price and add slightly more as deposit. - uint256 currentPrice = kaliBerger.getPrice(token, tokenId); + /// @notice Calculate patronage to collect after Approve + function testApprove_NotForSale() public payable { + // Escrow + testEscrow(); - // Buyer buys. - vm.prank(buyer); - kaliBerger.buy{value: currentPrice + 0.1 ether}(token, tokenId, newPrice, currentPrice); + // DAO disapproves + vm.prank(dao); + kaliBerger.approve(address(token_1), 1, false, "Cool NFT!"); + vm.warp(500); - // Validate number of ImpactDAOs. - assertEq(kaliBerger.getBergerCount(), 1); + // Validate + assertEq(token_1.balanceOf(alfred), 1); + assertEq(token_1.balanceOf(address(kaliBerger)), 0); + assertEq(kaliBerger.getTokenPurchaseStatus(address(token_1), 1), false); + } // timestamp: 500 - // Validate ownership of Patron Certificate for token_1, #1. - assertEq( - IPatronCertificate(address(patronCertificate)).ownerOf( - IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) - ), - buyer - ); + /// ----------------------------------------------------------------------- + /// Token Detail Test + /// ----------------------------------------------------------------------- - // Get unclaimed. - uint256 balanceBeforeClaim = address(kaliBerger).balance; - uint256 unclaimed = kaliBerger.getUnclaimed(address(impactDao)); + /// @notice Update token detail. + function testSetTokenDetail() public payable { + testApprove(); - // Claim any unclaimed. - vm.prank(impactDao); - kaliBerger.claim(); + vm.prank(dao); + kaliBerger.setTokenDetail(address(token_1), 1, "Charlie's NFT"); + assertEq(kaliBerger.getTokenDetail(address(token_1), 1), "Charlie's NFT"); + } - // Validate contract balances. - assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); - assertEq(address(kaliBerger).balance, balanceBeforeClaim - unclaimed); + function testSetTokenDetail_NotInitialized() public payable { + testEscrow(); - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + vm.expectRevert(KaliBerger.NotInitialized.selector); + vm.prank(dao); + kaliBerger.setTokenDetail(address(token_1), 1, "Cool!"); } - /// @notice Buy ERC721 when it is foreclosed. This is different from secondaryBuy() in that we will use 0 for currentPrice to denote foreclosure status. - function secondaryForeclosedBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) - public - payable - { - vm.warp(block.timestamp + 1000); + /// ----------------------------------------------------------------------- + /// Buy Test - Single Token + /// ----------------------------------------------------------------------- - // Deal buyer ether if buyer does not have any ether. - if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + // TODO: Tests for getTimeHeld(), getPatronId(), isPatron() - // Retrieve data for validation. - address impactDao = kaliBerger.getImpactDao(address(token), tokenId); - uint256 oldImpactDaoBalance = address(impactDao).balance; - emit log_uint(oldImpactDaoBalance); + /// @notice Bob buys token_1, tokenId #1 and declares a new price for sale + function testSingleBuy() public payable { + // Escrow & approve + testApprove(); - // Buyer buys. - vm.prank(buyer); - kaliBerger.buy{value: 0.1 ether}(token, tokenId, newPrice, 0); + // Bob buys. + primaryBuy(bob, address(token_1), 1, 1 ether, alfred); + } - // Validate number of ImpactDAOs. - assertEq(kaliBerger.getBergerCount(), 1); + /// @notice Unsatisfied with the first price, Bob sets a new price. + function testSingleBuy_setPrice() public payable { + // Bob buys. + testSingleBuy(); - // Validate ownership of Patron Certificate for token_1, #1. - assertEq( - IPatronCertificate(address(patronCertificate)).ownerOf( - IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) - ), - buyer - ); + // Bob sets new price. + setPrice(bob, address(token_1), 1, 2 ether, alfred); + } // timestamp: 5000 - // Get unclaimed. - uint256 oldKaliBergerBalance = address(kaliBerger).balance; - uint256 unclaimed = kaliBerger.getUnclaimed(address(impactDao)); + /// @notice Bob add deposits to maintain his ownership of token_1, token #1 for a longer period of time. + function testSingleBuy_addDeposit() public payable { + // Bob buys. + testSingleBuy(); - // Claim any unclaimed. - vm.prank(impactDao); - kaliBerger.claim(); + // Bob adds more ether as deposit. + addDeposit(bob, address(token_1), 1, 0.5 ether, alfred); + } - // Validate contract balances. - emit log_uint(address(impactDao).balance); - emit log_uint(address(kaliBerger).balance); - emit log_uint(unclaimed); - emit log_uint(oldKaliBergerBalance); - assertEq(address(impactDao).balance, oldImpactDaoBalance + unclaimed); - assertEq(address(kaliBerger).balance, oldKaliBergerBalance - unclaimed); + /// @notice DAO changes tax. + function testSingleBuy_setTax() public payable { + // Bob buys. + testSingleBuy(); - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } - - /// @notice Set price. - function setPrice(address user, address token, uint256 tokenId, uint256 newPrice, address creator) public payable { - // Retrieve data for validation. - address impactDao = kaliBerger.getImpactDao(address(token), tokenId); - uint256 oldBalance = address(kaliBerger).balance; - uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); - uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances - - // User sets new price. - vm.prank(user); - kaliBerger.setPrice(address(token), tokenId, newPrice); - - // Validate setting of new price. - assertEq(kaliBerger.getPrice(address(token), tokenId), newPrice); - - // Validate balances. - vm.prank(impactDao); - kaliBerger.claim(); - assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); - assertEq(address(kaliBerger).balance, oldBalance - address(impactDao).balance); - - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } - - /// @notice Add deposit. - function addDeposit(address user, address token, uint256 tokenId, uint256 amount, address creator) public payable { - // Retrieve data for validation. - address impactDao = kaliBerger.getImpactDao(address(token), tokenId); - uint256 deposit = kaliBerger.getDeposit(address(token), tokenId); - uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); - uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances - - // User adds deposit. - vm.prank(user); - kaliBerger.addDeposit{value: amount}(address(token), tokenId, amount); - - // Retrieve data for validation. - uint256 balanceBeforeClaim = address(kaliBerger).balance; - uint256 unclaimed = kaliBerger.getUnclaimed(impactDao); - - // Claim any unclaimed. - vm.prank(impactDao); - kaliBerger.claim(); - - // Validate balance and deposit amount. - assertEq(kaliBerger.getDeposit(address(token), tokenId), deposit + amount - patronage); - assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); - assertEq(address(kaliBerger).balance, balanceBeforeClaim - unclaimed); - - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } - - /// @notice Set tax. - function setTax(address token, uint256 tokenId, uint256 tax, address creator) public payable { - // Set new tax rate. - vm.prank(dao); - kaliBerger.setTax(token, tokenId, tax); - - // Validate patron balances. - balanceDao(block.timestamp + 1000, token, tokenId, alfred); - - // Validate tax update. - assertEq(kaliBerger.getTax(token, tokenId), tax); - - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } - - /// @notice Exit. - function exit(address user, address token, uint256 tokenId, uint256 amount, address creator) public payable { - // Retrieve data for validation. - address impactDao = kaliBerger.getImpactDao(address(token), tokenId); - uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); - uint256 deposit = kaliBerger.getDeposit(address(token), tokenId); - uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances - - // User withdraws all of deposit. - vm.prank(user); - kaliBerger.exit(address(token), tokenId, deposit - patronage); - - // Validate patronage amount. - assertEq(kaliBerger.getDeposit(address(token), tokenId), 0); - assertEq(IERC721(token).balanceOf(address(kaliBerger)), tokenId); - - // Validate balances. - vm.prank(impactDao); - kaliBerger.claim(); - assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); - assertEq(address(kaliBerger).balance, 0); - - // Balance DAO. - balanceDao(block.timestamp + 1000, address(token), tokenId, creator); - } - - /// ----------------------------------------------------------------------- - /// Test Escrow & Approve Logic - /// ----------------------------------------------------------------------- - - /// @notice The Gang escrows their tokens. - function testEscrow() public payable { - escrow(alfred, token_1, 1); - escrow(bob, token_2, 1); - escrow(charlie, token_3, 1); - } // 300 - - /// @notice DAO approves all tokens for purchase and adds custom detail. - function testApprove() public payable { - // Escrow. - testEscrow(); // 300 - - // DAO approves. - vm.warp(500); - approve(token_1, 1, "Alfred NFT"); // 500x - - // DAO approves. - vm.warp(1000); - approve(token_2, 1, "Bob NFT"); // 1000 - - // DAO approves. - vm.warp(2000); - approve(token_3, 1, "Charlie NFT"); // 2000 - } // 500, 1000, 2000 - - /// @notice Validate patronage after Approve - function testApprove_PatronageToCollect() public payable { - uint256 timestamp = 10000000; - uint256 tokenId = 1; - - testApprove(); - vm.warp(timestamp); - - // Validate - validatePatronageToCollect(token_1, tokenId); - validatePatronageToCollect(token_2, tokenId); - validatePatronageToCollect(token_3, tokenId); - } // timestamp: 10000000 - - /// @notice Calculate patronage to collect after Approve - function testApprove_NotForSale() public payable { - // Escrow - testEscrow(); - - // DAO disapproves - vm.prank(dao); - kaliBerger.approve(address(token_1), 1, false, "Cool NFT!"); - vm.warp(500); - - // Validate - assertEq(token_1.balanceOf(alfred), 1); - assertEq(token_1.balanceOf(address(kaliBerger)), 0); - assertEq(kaliBerger.getTokenPurchaseStatus(address(token_1), 1), false); - } // timestamp: 500 - - /// ----------------------------------------------------------------------- - /// Test Buy Logic - Single Token - /// ----------------------------------------------------------------------- - - /// @notice Bob buys token_1, tokenId #1 and declares a new price for sale - function testSingleBuy() public payable { - // Escrow & approve - testApprove(); - - // Bob buys. - primaryBuy(bob, address(token_1), 1, 1 ether, alfred); - } - - /// @notice Unsatisfied with the first price, Bob sets a new price. - function testSingleBuy_setPrice() public payable { - // Bob buys. - testSingleBuy(); - - // Bob sets new price. - setPrice(bob, address(token_1), 1, 2 ether, alfred); - } // timestamp: 5000 - - /// @notice Bob add deposits to maintain his ownership of token_1, token #1 for a longer period of time. - function testSingleBuy_addDeposit() public payable { - // Bob buys. - testSingleBuy(); - - // Bob adds more ether as deposit. - addDeposit(bob, address(token_1), 1, 0.5 ether, alfred); - } - - /// @notice DAO changes tax. - function testSingleBuy_setTax() public payable { - // Bob buys. - testSingleBuy(); - - // Bob sets new tax. - setTax(address(token_1), 1, 30, alfred); + // Bob sets new tax. + setTax(address(token_1), 1, 30, alfred); } /// @notice Charlie and Darius add deposits to help Bob maintain ownership of token_1, token #1 for a longer period of time. @@ -569,8 +330,46 @@ contract KaliBergerTest is Test { secondaryBuy(earn, address(token_1), 1, 5 ether, alfred); } + /// @notice Alfred withdraws token_1, tokenId #1. + function testSingleBuy_pull() public payable { + // Continuing from third buy by Earn. + testSingleBuy_thirdBuy(); + vm.warp(2707346409); + + // Alfred pulls ERC721. + pull(alfred, address(token_1), 1); + } + + /// @notice . + function testBalance_DefectedCreator() public payable { + // Bob buys. + testSingleBuy(); + + // Mint KaliDAO tokens. + address payable impactDao = payable(kaliBerger.getImpactDao(address(token_1), 1)); + mintImpactDaoTokens(KaliDAO(impactDao), alfred, alfred, 100 ether); + + // Balance. + emit log_uint(block.timestamp); + balanceDao(block.timestamp + 20000, address(token_1), 1, alfred); + } + + /// @notice . + function testBalance_DefectedPatron() public payable { + // Bob buys. + testSingleBuy(); + + // Mint KaliDAO tokens. + address payable impactDao = payable(kaliBerger.getImpactDao(address(token_1), 1)); + mintImpactDaoTokens(KaliDAO(impactDao), alfred, bob, 100 ether); + + // Balance. + emit log_uint(block.timestamp); + balanceDao(block.timestamp + 20000, address(token_1), 1, alfred); + } + /// ----------------------------------------------------------------------- - /// Test Buy Logic - Multiple Tokens + /// Buy Test - Multiple Tokens /// ----------------------------------------------------------------------- /// @notice Darius buys all tokens and declares new prices for each @@ -585,22 +384,9 @@ contract KaliBergerTest is Test { } /// ----------------------------------------------------------------------- - /// Custom Error Test Logic + /// Custom Error Test /// ----------------------------------------------------------------------- - /// @notice With KaliBerger uninitialized, Bob tries to buy tokens and gets an Uninitialized() error. - function testUninitialized() public payable { - vm.warp(100); - - // Deal Bob ether - vm.deal(bob, 10 ether); - - // Bob buys - vm.expectRevert(KaliBerger.NotInitialized.selector); - vm.prank(bob); - kaliBerger.buy{value: 0.1 ether}(address(token_3), 1, 1 ether, 0); - } // timestamp: 1500 - /// @notice Charlie tries to escrows tokenId #1 of token_1 and triggers NotAuthorized() error. function testEscrow_byOthers() public payable { // Approve KaliBerger to transfer ERC721 @@ -623,7 +409,7 @@ contract KaliBergerTest is Test { vm.expectRevert(KaliBerger.NotAuthorized.selector); vm.prank(charlie); kaliBerger.setPrice(address(token_1), 1, 2 ether); - } // timestamp: 5000 + } /// @notice Bob withdraws too much and triggers InvalidExit() error. function testSingleBuy_exit_invalidExit() public payable { @@ -687,4 +473,357 @@ contract KaliBergerTest is Test { assert(sent); assert(address(kaliBerger).balance == 5 ether); } + + /// ----------------------------------------------------------------------- + /// Helper Logic + /// ----------------------------------------------------------------------- + + /// @notice Deploy ERC721. + function deployErc721() internal returns (MockERC721 token) { + token = new MockERC721("TEST", "TEST"); + } + + /// @notice Mint ERC721. + function mintErc721(MockERC721 token, uint256 tokenId, address recipient) internal { + if (address(token) == address(0)) { + token = deployErc721(); + } + + // Mint recipient an ERC721 + token.mint(recipient, tokenId); + assertEq(token.balanceOf(recipient), tokenId); + } + + /// @notice Initialize KaliBerger. + function initialize(address _dao, address _factory, address minter) internal { + kaliBerger.initialize(_dao, _factory, minter); + } + + /// @notice Escrow ERC721. + function escrow(address user, MockERC721 token, uint256 tokenId) internal { + // Approve KaliBerger to transfer ERC721 + vm.prank(user); + token.approve(address(kaliBerger), tokenId); + vm.warp(200); + + // User escrows ERC721 with KaliBerger + vm.prank(user); + kaliBerger.escrow(address(token), tokenId, user); + vm.warp(300); + + // Validate + assertEq(token.balanceOf(user), 0); + assertEq(token.balanceOf(address(kaliBerger)), tokenId); + } // 300 + + /// @notice Approve ERC721 for purchase. + function approve(MockERC721 token, uint256 tokenId, string memory detail) public payable { + // DAO approves ERC721 for sale + vm.prank(dao); + kaliBerger.approve(address(token), tokenId, true, detail); + + // Validate + assertEq(kaliBerger.getTokenPurchaseStatus(address(token), tokenId), true); + assertEq(kaliBerger.getTokenDetail(address(token), tokenId), detail); + assertEq(kaliBerger.getOwner(address(token), tokenId), address(kaliBerger)); + } + + /// @notice Validate amount of patronage to collect. + function validatePatronageToCollect(MockERC721 token, uint256 tokenId) public payable { + uint256 amount = kaliBerger.getPrice(address(token), tokenId) + * (block.timestamp - kaliBerger.getTimeLastCollected(address(token), tokenId)) + * kaliBerger.getTax(address(token), tokenId) / 365 days / 100; + assertEq(kaliBerger.patronageToCollect(address(token), tokenId), amount); + // emit log_uint(amount); + } + + /// @notice Rebalance DAO tokens. + function balanceDao(uint256 timestamp, address token, uint256 tokenId, address creator) public payable { + // Validate + vm.warp(timestamp); + + // Darius balances a DAO for everyone. + vm.prank(darius); + kaliBerger.balanceDao(token, tokenId); + + vm.warp(timestamp + 1000); + // Retrieve token balances to validate DAO is in balance. + address impactDao = kaliBerger.getImpactDao(token, tokenId); + uint256 creator_balance = IERC20(impactDao).balanceOf(creator); + uint256 alfred_balance = IERC20(impactDao).balanceOf(alfred); + uint256 bob_balance = IERC20(impactDao).balanceOf(bob); + uint256 charlie_balance = IERC20(impactDao).balanceOf(charlie); + uint256 darius_balance = IERC20(impactDao).balanceOf(darius); + uint256 earn_balance = IERC20(impactDao).balanceOf(earn); + + // Validate + if (creator == alfred) assertEq(creator_balance, bob_balance + charlie_balance + earn_balance + darius_balance); + if (creator == bob) assertEq(creator_balance, alfred_balance + charlie_balance + earn_balance + darius_balance); + if (creator == charlie) assertEq(creator_balance, alfred_balance + bob_balance + earn_balance + darius_balance); + emit log_uint(alfred_balance); + emit log_uint(bob_balance); + emit log_uint(charlie_balance); + emit log_uint(darius_balance); + emit log_uint(earn_balance); + } + + /// @notice Buy ERC721. + function primaryBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) + public + payable + { + vm.warp(block.timestamp + 1000); + + // Deal buyer ether if buyer does not have any ether. + if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + + // Get berger count before purchase. + uint256 count = kaliBerger.getBergerCount(); + + // Buyer buys. + vm.prank(buyer); + kaliBerger.buy{value: 0.1 ether}(token, tokenId, newPrice, 0); + + // Validate summoning of ImpactDAO. + assertEq(kaliBerger.getBergerCount(), count == 0 ? 1 : ++count); + + // Validate ownership of Patron Certificate for token_1, #1. + assertEq( + IPatronCertificate(address(patronCertificate)).ownerOf( + IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) + ), + buyer + ); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Buy ERC721. + function secondaryBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) + public + payable + { + vm.warp(block.timestamp + 1000); + + // Deal buyer ether if buyer does not have any ether. + if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + + // Retrieve data for validation. + address impactDao = kaliBerger.getImpactDao(address(token), tokenId); + uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); + uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances + + // Get current price and add slightly more as deposit. + uint256 currentPrice = kaliBerger.getPrice(token, tokenId); + + // Buyer buys. + vm.prank(buyer); + kaliBerger.buy{value: currentPrice + 0.1 ether}(token, tokenId, newPrice, currentPrice); + + // ImpactDAO claims. + claim(impactDao); + + // Validate contract balances. + assertEq(kaliBerger.getBergerCount(), 1); + + // Validate number of ImpactDAOs. + assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); + + // Validate ownership of Patron Certificate for token_1, #1. + assertEq( + IPatronCertificate(address(patronCertificate)).ownerOf( + IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) + ), + buyer + ); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Buy ERC721 when it is foreclosed. This is different from secondaryBuy() in that we will use 0 for currentPrice to denote foreclosure status. + function secondaryForeclosedBuy(address buyer, address token, uint256 tokenId, uint256 newPrice, address creator) + public + payable + { + vm.warp(block.timestamp + 1000); + + // Deal buyer ether if buyer does not have any ether. + if (address(buyer).balance < 0.01 ether) vm.deal(buyer, 10 ether); + + // Buyer buys. + vm.prank(buyer); + kaliBerger.buy{value: 0.1 ether}(token, tokenId, newPrice, 0); + + // Validate number of ImpactDAOs. + assertEq(kaliBerger.getBergerCount(), 1); + + // Validate ownership of Patron Certificate for token_1, #1. + assertEq( + IPatronCertificate(address(patronCertificate)).ownerOf( + IPatronCertificate(address(patronCertificate)).getTokenId(address(token), tokenId) + ), + buyer + ); + + // ImpactDAO claims. + address impactDao = kaliBerger.getImpactDao(address(token), tokenId); + claim(impactDao); + + // emit log_uint(address(impactDao).balance); + // emit log_uint(address(kaliBerger).balance); + // emit log_uint(unclaimed); + // emit log_uint(oldKaliBergerBalance); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Set price. + function setPrice(address user, address token, uint256 tokenId, uint256 newPrice, address creator) public payable { + // Retrieve data for validation. + address impactDao = kaliBerger.getImpactDao(address(token), tokenId); + uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); + uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances + + // User sets new price. + vm.prank(user); + kaliBerger.setPrice(address(token), tokenId, newPrice); + + // ImpactDAO claims. + claim(impactDao); + + // Validate setting of new price. + assertEq(kaliBerger.getPrice(address(token), tokenId), newPrice); + + // Validate impactDAO balance. + assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Add deposit. + function addDeposit(address user, address token, uint256 tokenId, uint256 amount, address creator) public payable { + // Retrieve data for validation. + address impactDao = kaliBerger.getImpactDao(address(token), tokenId); + uint256 deposit = kaliBerger.getDeposit(address(token), tokenId); + uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); + uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances + + // User adds deposit. + vm.prank(user); + kaliBerger.addDeposit{value: amount}(address(token), tokenId, amount); + + // ImpactDAO claims. + claim(impactDao); + + // Validate balance and deposit amount. + assertEq(kaliBerger.getDeposit(address(token), tokenId), deposit + amount - patronage); + assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Set tax. + function setTax(address token, uint256 tokenId, uint256 tax, address creator) public payable { + // Set new tax rate. + vm.prank(dao); + kaliBerger.setTax(token, tokenId, tax); + + // Validate patron balances. + balanceDao(block.timestamp + 1000, token, tokenId, alfred); + + // Validate tax update. + assertEq(kaliBerger.getTax(token, tokenId), tax); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + /// @notice Exit. + function exit(address user, address token, uint256 tokenId, uint256 amount, address creator) public payable { + // Retrieve data for validation. + address impactDao = kaliBerger.getImpactDao(address(token), tokenId); + uint256 depositBeforeExit = kaliBerger.getDeposit(address(token), tokenId); + uint256 patronage = kaliBerger.patronageToCollect(address(token), tokenId); + uint256 oldImpactDaoBalance = address(impactDao).balance + kaliBerger.getUnclaimed(impactDao); // Consider a function to aggregate all impactDao balances + + // User withdraws from deposit. + vm.prank(user); + kaliBerger.exit(address(token), tokenId, amount); + + // Validate patronage amount. + assertEq(kaliBerger.getDeposit(address(token), tokenId), depositBeforeExit - amount - patronage); + assertEq(IERC721(token).balanceOf(address(kaliBerger)), 1); + + // ImpactDAO claims. + claim(impactDao); + assertEq(address(impactDao).balance, oldImpactDaoBalance + patronage); + + // Balance DAO. + balanceDao(block.timestamp + 1000, address(token), tokenId, creator); + } + + function pull(address user, address token, uint256 tokenId) public payable { + // User pulls ERC721 out of KaliBerger contract. + vm.prank(user); + kaliBerger.pull(token, tokenId); + + // Validate. + assertEq(MockERC721(token).balanceOf(user), 1); + assertEq(MockERC721(token).balanceOf(address(kaliBerger)), 0); + assertEq(kaliBerger.getPrice(address(token), tokenId), 0); + assertEq(kaliBerger.getDeposit(address(token), tokenId), 0); + assertEq(kaliBerger.getTokenPurchaseStatus(address(token), tokenId), false); + } + + function claim(address user) public payable { + // Retrieve data for validation. + uint256 bergerBalanceBeforeClaim = address(kaliBerger).balance; + uint256 userBalanceBeforeClaim = address(user).balance; + uint256 unclaimed = kaliBerger.getUnclaimed(user); + + // Claim any unclaimed. + vm.prank(user); + kaliBerger.claim(); + + // Validate balance and deposit amount. + assertEq(address(user).balance, userBalanceBeforeClaim + unclaimed); + assertEq(address(kaliBerger).balance, bergerBalanceBeforeClaim - unclaimed); + } + + function mintImpactDaoTokens(KaliDAO impactDao, address proposer, address recipient, uint256 amount) + public + payable + { + // Set up voting params. + address[] memory recipients = new address[](1); + recipients[0] = recipient; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + bytes[] memory payloads = new bytes[](1); + payloads[0] = ""; + + // Retrieve data for validation. + uint256 userBalanceBeforeMint = KaliDAO(impactDao).balanceOf(recipient); + + vm.prank(proposer); + impactDao.propose(KaliDAO.ProposalType.MINT, "", recipients, amounts, payloads); + + vm.warp(block.timestamp + 1000); + uint256 proposalId = impactDao.proposalCount(); + + vm.prank(proposer); + impactDao.vote(proposalId, true); + + vm.warp(block.timestamp + 100000); + + vm.prank(proposer); + impactDao.processProposal(proposalId); + + assertEq(KaliDAO(impactDao).balanceOf(recipient), userBalanceBeforeMint + amount); + } }