From 99afbe02324e4dcc2ece38c9ffce38141d4e17df Mon Sep 17 00:00:00 2001 From: Iain Nash Date: Sat, 17 Dec 2022 01:24:20 -0500 Subject: [PATCH] Update founders forking test (#90) * wip * add new tests and forking for founders update * github run action file update * update env name * fix tests --- .github/workflows/test.yml | 3 +- src/token/Token.sol | 24 ++++---- test/Token.t.sol | 89 +++++++++++++++++++++++------ test/forking/TestUpdateOwners.t.sol | 82 ++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 test/forking/TestUpdateOwners.t.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b550ab..f78d9da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,7 @@ jobs: build: name: Test runs-on: ubuntu-latest + environment: Test steps: - uses: actions/checkout@v2 with: @@ -15,4 +16,4 @@ jobs: - uses: onbjerg/foundry-toolchain@v1 with: version: nightly - - run: npm run test + - run: ETH_RPC_MAINNET=${{secrets.ETH_RPC_MAINNET}} npm run test diff --git a/src/token/Token.sol b/src/token/Token.sol index 61f3360..833240d 100644 --- a/src/token/Token.sol +++ b/src/token/Token.sol @@ -6,7 +6,6 @@ import { ReentrancyGuard } from "../lib/utils/ReentrancyGuard.sol"; import { ERC721Votes } from "../lib/token/ERC721Votes.sol"; import { ERC721 } from "../lib/token/ERC721.sol"; import { Ownable } from "../lib/utils/Ownable.sol"; - import { TokenStorageV1 } from "./storage/TokenStorageV1.sol"; import { IBaseMetadata } from "./metadata/interfaces/IBaseMetadata.sol"; import { IManager } from "../manager/IManager.sol"; @@ -92,15 +91,14 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC /// @dev We do this by reserving an mapping of [0-100] token indices, such that if a new token mint ID % 100 is reserved, it's sent to the appropriate founder. /// @param _founders The list of DAO founders function _addFounders(IManager.FounderParams[] calldata _founders) internal { - // Cache the number of founders - uint256 numFounders = _founders.length; - // Used to store the total percent ownership among the founders uint256 totalOwnership; + uint8 numFoundersAdded = 0; + unchecked { // For each founder: - for (uint256 i; i < numFounders; ++i) { + for (uint256 i; i < _founders.length; ++i) { // Cache the percent ownership uint256 founderPct = _founders[i].ownershipPct; @@ -118,7 +116,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC } // Compute the founder's id - uint256 founderId = settings.numFounders++; + uint256 founderId = numFoundersAdded++; // Get the pointer to store the founder Founder storage newFounder = founder[founderId]; @@ -152,7 +150,7 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // Store the founders' details settings.totalOwnership = uint8(totalOwnership); - settings.numFounders = uint8(numFounders); + settings.numFounders = numFoundersAdded; } } @@ -349,6 +347,15 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // copy the founder into memory Founder memory cachedFounder = cachedFounders[i]; + // Delete the founder from the stored mapping + delete founder[i]; + + // Some DAOs were initialized with 0 percentage ownership. + // This skips them to avoid a division by zero error. + if (cachedFounder.ownershipPct == 0) { + continue; + } + // using the ownership percentage, get reserved token percentages uint256 schedule = 100 / cachedFounder.ownershipPct; @@ -369,9 +376,6 @@ contract Token is IToken, VersionedContract, UUPS, Ownable, ReentrancyGuard, ERC // Update the base token id baseTokenId = (baseTokenId + schedule) % 100; } - - // Delete the founder from the stored mapping - delete founder[i]; } } diff --git a/test/Token.t.sol b/test/Token.t.sol index 12a5544..2c5696b 100644 --- a/test/Token.t.sol +++ b/test/Token.t.sol @@ -147,15 +147,16 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { deployWithCustomFounders(wallets, percents, vestExpirys); - assertEq(token.totalFounders(), 100); + // Last founder is omitted so total number of founders is 99 + assertEq(token.totalFounders(), 99); assertEq(token.totalFounderOwnership(), 99); - Founder memory founder; + Founder memory thisFounder; for (uint256 i; i < 99; ++i) { - founder = token.getScheduledRecipient(i); + thisFounder = token.getScheduledRecipient(i); - assertEq(founder.wallet, otherUsers[i]); + assertEq(thisFounder.wallet, otherUsers[i]); } } @@ -183,16 +184,16 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalFounders(), 50); assertEq(token.totalFounderOwnership(), 99); - Founder memory founder; + Founder memory thisFounder; for (uint256 i; i < 49; ++i) { - founder = token.getScheduledRecipient(i); + thisFounder = token.getScheduledRecipient(i); - assertEq(founder.wallet, otherUsers[i]); + assertEq(thisFounder.wallet, otherUsers[i]); - founder = token.getScheduledRecipient(i + 50); + thisFounder = token.getScheduledRecipient(i + 50); - assertEq(founder.wallet, otherUsers[i]); + assertEq(thisFounder.wallet, otherUsers[i]); } } @@ -219,20 +220,20 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalFounders(), 2); assertEq(token.totalFounderOwnership(), 98); - Founder memory founder; + Founder memory thisFounder; unchecked { for (uint256 i; i < 500; ++i) { - founder = token.getScheduledRecipient(i); + thisFounder = token.getScheduledRecipient(i); if (i % 100 >= 98) { continue; } if (i % 2 == 0) { - assertEq(founder.wallet, otherUsers[0]); + assertEq(thisFounder.wallet, otherUsers[0]); } else { - assertEq(founder.wallet, otherUsers[1]); + assertEq(thisFounder.wallet, otherUsers[1]); } } } @@ -423,16 +424,49 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { assertEq(token.totalFounders(), 2); assertEq(token.totalFounderOwnership(), 99); - Founder memory founder; + Founder memory thisFounder; unchecked { for (uint256 i; i < 99; ++i) { - founder = token.getScheduledRecipient(i); + thisFounder = token.getScheduledRecipient(i); if (i % 2 == 0) { - assertEq(founder.wallet, otherUsers[0]); + assertEq(thisFounder.wallet, otherUsers[0]); } else { - assertEq(founder.wallet, otherUsers[1]); + assertEq(thisFounder.wallet, otherUsers[1]); + } + } + } + + vm.prank(otherUsers[0]); + auction.unpause(); + } + + function testFoundersCreateZeroOwnershipOmitted() public { + createUsers(2, 1 ether); + + address[] memory wallets = new address[](2); + uint256[] memory percents = new uint256[](2); + uint256[] memory vestExpirys = new uint256[](2); + + uint256 end = 4 weeks; + wallets[0] = otherUsers[0]; + vestExpirys[0] = end; + wallets[1] = otherUsers[1]; + vestExpirys[1] = end; + percents[0] = 0; + percents[1] = 50; + + deployWithCustomFounders(wallets, percents, vestExpirys); + + assertEq(token.totalFounders(), 1); + assertEq(token.totalFounderOwnership(), 50); + + unchecked { + for (uint256 i; i < 99; ++i) { + if (i % 2 == 0) { + Founder memory thisFounder = token.getScheduledRecipient(i); + assertEq(thisFounder.wallet, otherUsers[1]); } } } @@ -472,6 +506,27 @@ contract TokenTest is NounsBuilderTest, TokenTypesV1 { token.updateFounders(foundersArr); } + function test_UpdateFoundersZeroOwnership() public { + deployMock(); + + IManager.FounderParams[] memory newFoundersArr = new IManager.FounderParams[](2); + newFoundersArr[0] = IManager.FounderParams({ + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 0, + vestExpiry: 2556057600 + }); + newFoundersArr[1] = IManager.FounderParams({ + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 10, + vestExpiry: 2556057600 + }); + + vm.prank(address(founder)); + token.updateFounders(newFoundersArr); + + assertEq(token.getFounders().length, 1); + } + function test_UpdateFounderShareAllocationFuzz( uint256 f1Percentage, uint256 f2Percentage, diff --git a/test/forking/TestUpdateOwners.t.sol b/test/forking/TestUpdateOwners.t.sol new file mode 100644 index 0000000..cf58d5d --- /dev/null +++ b/test/forking/TestUpdateOwners.t.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.16; + +import { Test } from "forge-std/Test.sol"; +import { Treasury } from "../../src/governance/treasury/Treasury.sol"; +import { Auction } from "../../src/auction/Auction.sol"; +import { Token } from "../../src/token/Token.sol"; +import { Governor } from "../../src/governance/governor/Governor.sol"; +import { IManager } from "../../src/manager/IManager.sol"; +import { Manager } from "../../src/manager/Manager.sol"; +import { UUPS } from "../../src/lib/proxy/UUPS.sol"; + +contract PurpleTests is Test { + Manager internal immutable manager = Manager(0xd310A3041dFcF14Def5ccBc508668974b5da7174); + Treasury internal immutable treasury = Treasury(payable(0xeB5977F7630035fe3b28f11F9Cb5be9F01A9557D)); + Auction internal immutable auction = Auction(payable(0x658D3A1B6DaBcfbaa8b75cc182Bf33efefDC200d)); + Token internal immutable token = Token(0xa45662638E9f3bbb7A6FeCb4B17853B7ba0F3a60); + Governor internal immutable governor = Governor(0xFB4A96541E1C70FC85Ee512420eB0B05C542df57); + address internal immutable fawkes = 0x617Cb4921071e73D0C41B5354F5246F12518745e; + address internal immutable upgradedTokenImplAddress = 0xb69dC36182Fe5dad045BD4B08Ffb042D10d0fB77; + address[] internal targets; + uint256[] internal values; + bytes[] internal calldatas; + string internal description; + + function setUp() public { + uint256 mainnetFork = vm.createFork(vm.envString("ETH_RPC_MAINNET")); + vm.selectFork(mainnetFork); + vm.rollFork(16171761); + + Token newTokenImpl = new Token(address(manager)); + + vm.prank(manager.owner()); + manager.registerUpgrade(address(0x3E8c48b46C5752F40c6772520f03a4D8EDa49706), address(newTokenImpl)); + + IManager.FounderParams[] memory newFounderParams = new IManager.FounderParams[](3); + newFounderParams[0] = IManager.FounderParams({ + wallet: address(0x06B59d0b6AdCc6A5Dc63553782750dc0b41266a3), + ownershipPct: 10, + vestExpiry:2556057600 + }); + newFounderParams[1] = IManager.FounderParams({ + wallet: address(0x349993989b5AC27Fd033AcCb86a84920DEb91ABa), + ownershipPct: 10, + vestExpiry:2556057600 + }); + newFounderParams[2] = IManager.FounderParams({ + wallet: address(0x0BC3807Ec262cB779b38D65b38158acC3bfedE10), + ownershipPct: 1, + vestExpiry: 2556057600 + }); + + targets = new address[](2); + targets[0] = address(token); + targets[1] = address(token); + values = new uint256[](2); + values[0] = 0; + values[1] = 0; + calldatas = new bytes[](2); + calldatas[0] = abi.encodeWithSelector(UUPS.upgradeTo.selector, address(newTokenImpl)); + calldatas[1] = abi.encodeWithSelector(Token.updateFounders.selector, newFounderParams); + } + + function test_purpleUpgrade() public { + vm.prank(fawkes); + bytes32 proposalId = governor.propose(targets, values, calldatas, ""); + + vm.warp(block.timestamp + 3 days); + vm.prank(fawkes); + governor.castVote(proposalId, 1); + vm.prank(0x8700B87C2A053BDE8Cdc84d5078B4AE47c127FeB); + governor.castVote(proposalId, 1); + + vm.warp(block.timestamp + 4 days); + governor.queue(proposalId); + + vm.warp(block.timestamp + 3 days); + governor.execute(targets, values, calldatas, keccak256(""), fawkes); + + + } +} \ No newline at end of file