diff --git a/foundry.toml b/foundry.toml index e0452d9..c3c0e1d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ src = "src" out = "out" libs = ["lib", "dependencies"] +gas_limit = "18446744073709551615" optimizer = true optimizer_runs = 10000 @@ -18,6 +19,7 @@ gas_reports = [ "GhostBondingCalculator", "GhostTreasury", "GhostGovernorExposed", + "GatekeeperVerification", ] remappings = [ "@openzeppelin-contracts/=dependencies/@openzeppelin-contracts-5.0.2/", diff --git a/test/gatekeeper/GatekeeperWeaver.t.sol b/test/gatekeeper/GatekeeperWeaver.t.sol new file mode 100644 index 0000000..328e21c --- /dev/null +++ b/test/gatekeeper/GatekeeperWeaver.t.sol @@ -0,0 +1,212 @@ +pragma solidity 0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Gatekeeper} from "../../src/Gatekeeper.sol"; +import {Hashes} from "../../src/libraries/Hashes.sol"; +import {Checkpoints} from "../../src/libraries/Checkpoints.sol"; + +contract GatekeeperVerification is Gatekeeper { + using Checkpoints for Checkpoints.Trace256; + using Checkpoints for Checkpoints.Trace160; + + constructor( + address staking, + uint256 ghostedSupply, + uint256 existential, + uint48 deployedAt, + uint104 amountIn, + uint104 amountOut + ) Gatekeeper( + staking, + ghostedSupply, + existential, + deployedAt, + amountIn, + amountOut + ) {} + + function filledEntries(uint256 session) public view returns (uint256) { + return _filledEntries[session]; + } + + function slotLengths(uint256 session, uint256 globalIndex) public view returns (uint160) { + uint256 slotIndex = globalIndex % SLOTS; + return _slotLenghts[session][slotIndex].latest(); + } + + function slotValues(uint256 session, uint256 globalIndex) public view returns (bytes32) { + uint256 slotIndex = globalIndex % SLOTS; + uint256 valueIndex = globalIndex / SLOTS; + return _slotValues[session][slotIndex][valueIndex]; + } + + function treeNodesLatest(uint256 session, uint256 globalIndex) public view returns (bytes32) { + uint256 slotIndex = globalIndex % SLOTS; + return bytes32(_treeNodes[session][slotIndex].latest()); + } + + function computePreimage(uint256 globalIndex, uint256 a, bytes32 r) public pure returns (bytes32) { + uint256 valueIndex = globalIndex / SLOTS; + return _computeArgumentsHash(valueIndex, a, r); + } + + function testVerify( + bytes32[] calldata proof, + bytes32[] calldata values, + uint256 globalIndex, + uint256 amount, + bytes32 who + ) public pure returns (bytes32) { + if (proof.length != DEPTH) { return bytes32(0); } + if (values.length == 0) { return bytes32(0); } + + uint256 slotIndex = globalIndex % SLOTS; + uint256 valueIndex = globalIndex / SLOTS; + if (valueIndex >= values.length) { return bytes32(0); } + + bytes32 computedHash = _computeArgumentsHash(valueIndex, amount, who); + if (computedHash != values[valueIndex]) { return bytes32(0); } + computedHash = bytes32(0); + + uint256 i; + for (; i < values.length;) { + bytes32 currHash = Hashes.efficientKeccak256(values[i]); + computedHash = Hashes.efficientKeccak256(computedHash, currHash); + unchecked { ++i; } + } + + i = 0; + for (; i < proof.length; ) { + if (slotIndex % 2 == 0) { + computedHash = Hashes.efficientKeccak256(computedHash, proof[i]); + } else { + computedHash = Hashes.efficientKeccak256(proof[i], computedHash); + } + slotIndex >>= 1; + unchecked { ++i; } + } + + return computedHash; + } +} + +contract GatekeeperWeaverTest is Test { + address constant ALICE = 0x0000000000000000000000000000000000000001; + address constant BOB = 0x0000000000000000000000000000000000000002; + uint256 constant EXISTENTIAL = 1337; + uint256 constant AMOUNT = 1 * 1e7; + + GatekeeperVerification gatekeeper; + + function setUp() public { + gatekeeper = new GatekeeperVerification(ALICE, 0, EXISTENTIAL, 0, 0, 0); + } + + function test_insertationWorksAsExpected() public { + uint256 currentSession = gatekeeper.currentSession(); + uint256 maxCount = gatekeeper.ENTRIES(); + uint256 globalIndex; + + (bytes32[] memory whos, uint256[] memory amounts) = _prepareArrays(maxCount); + + for (uint256 i = 0; i < maxCount; i++) { + if (i % 5 == 0) { vm.roll(block.number + 1); } + globalIndex = _insertWithAssert(currentSession, amounts[i], whos[i]); + assertTrue(_verifyProof(globalIndex, currentSession, block.number, amounts[i], whos[i])); + } + + globalIndex = _insertWithAssert(currentSession, amounts[69], whos[69]); + uint256 newSession = gatekeeper.currentSession(); + + assertEq(currentSession + 1, newSession); + vm.roll(block.number + 1337); + + assertTrue(_verifyProof(globalIndex, newSession, block.number, amounts[69], whos[69])); + assertTrue(_verifyProof(0, currentSession, block.number, amounts[0], whos[0])); + assertTrue(_verifyProof(69, currentSession, block.number, amounts[69], whos[69])); + assertTrue(_verifyProof(420, currentSession, block.number, amounts[420], whos[420])); + assertTrue(_verifyProof(1337, currentSession, block.number, amounts[1337], whos[1337])); + assertTrue(_verifyProof(2047, currentSession, block.number, amounts[2047], whos[2047])); + + assertTrue(_verifyProof(0, currentSession, 100, amounts[0], whos[0])); + assertTrue(_verifyProof(69, currentSession, 100, amounts[69], whos[69])); + assertTrue(_verifyProof(420, currentSession, 100, amounts[420], whos[420])); + + assertFalse(_verifyProof(globalIndex, newSession, 100, amounts[69], whos[69])); + assertFalse(_verifyProof(1337, currentSession, 100, amounts[1337], whos[1337])); + assertFalse(_verifyProof(2047, currentSession, 100, amounts[2047], whos[2047])); + } + + function _verifyProof( + uint256 globalIndex, + uint256 session, + uint256 atBlock, + uint256 amount, + bytes32 who + ) private view returns (bool) { + bytes32[] memory proof = gatekeeper.getProof(globalIndex, session, atBlock); + bytes32[] memory values = gatekeeper.getSlotValues(globalIndex, session, atBlock); + + bytes32 root1 = gatekeeper.testVerify(proof, values, globalIndex, amount, who); + (bytes32 root2, uint256 entries) = gatekeeper.getRoot(session, atBlock); + assertTrue(entries <= gatekeeper.ENTRIES()); + + return root1 == root2; + } + + function _insertWithAssert( + uint256 session, + uint256 amount, + bytes32 who + ) private returns (uint256 globalIndex) { + uint256 targetSlot = gatekeeper.filledEntries(session); + bytes32 previousHash = gatekeeper.treeNodesLatest(session, targetSlot); + uint256 prevEntries = gatekeeper.filledEntries(session); + uint160 prevLength = gatekeeper.slotLengths(session, targetSlot); + + vm.prank(ALICE); + globalIndex = gatekeeper.ghost(who, amount); + + if (session + 1 == gatekeeper.currentSession()) { + assertEq(prevLength, gatekeeper.DEPTH()); + assertEq(gatekeeper.slotLengths(session + 1, 0), 1); + assertEq(prevEntries, gatekeeper.ENTRIES()); + assertEq(gatekeeper.filledEntries(session + 1), 1); + + session += 1; + previousHash = bytes32(0); + prevLength = gatekeeper.slotLengths(session, 0); + } else { + assertEq(prevLength + 1, gatekeeper.slotLengths(session, targetSlot)); + assertEq(prevEntries + 1, gatekeeper.filledEntries(session)); + } + + { + bytes32 preimage1 = gatekeeper.slotValues(session, globalIndex); + bytes32 preimage2 = gatekeeper.computePreimage(globalIndex, amount, who); + assertEq(preimage1, preimage2); + } + + { + bytes32 nodeHash1 = gatekeeper.treeNodesLatest(session, globalIndex); + bytes32 preimageHash = Hashes.efficientKeccak256( + gatekeeper.computePreimage(globalIndex, amount, who) + ); + bytes32 nodeHash2 = Hashes.efficientKeccak256(previousHash, preimageHash); + assertEq(nodeHash1, nodeHash2); + } + } + + function _prepareArrays(uint256 count) private pure returns (bytes32[] memory, uint256[] memory) { + bytes32[] memory whos = new bytes32[](count); + uint256[] memory amounts = new uint256[](count); + + for (uint256 i = 0; i < count; i++) { + whos[i] = keccak256(abi.encodePacked("user", i)); + amounts[i] = EXISTENTIAL + 1 + i; + } + + return (whos, amounts); + } +}