diff --git a/.gitignore b/.gitignore index 919f67e..2aea512 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ deployments-zk node_modules out zkout +remappings.txt diff --git a/ethereum/KDAO.sol b/ethereum/KDAO.sol index 422c3d5..2e28370 100644 --- a/ethereum/KDAO.sol +++ b/ethereum/KDAO.sol @@ -126,9 +126,7 @@ contract KDAO is IERC20Permit { // keccak256( // abi.encode( - // keccak256( - // "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - // ), + // keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), // keccak256(bytes("KDAO")), // keccak256(bytes("1")), // 0x1, diff --git a/lib/interfaces b/lib/interfaces index 5a4d0a1..068f05e 160000 --- a/lib/interfaces +++ b/lib/interfaces @@ -1 +1 @@ -Subproject commit 5a4d0a155d74c451c0f980be01416ef6bbd45a40 +Subproject commit 068f05ec1f581d78bb1caca8643ff9eecccab615 diff --git a/test/ethereum/KDAO.permit.t.sol b/test/ethereum/KDAO.permit.t.sol new file mode 100644 index 0000000..e401b85 --- /dev/null +++ b/test/ethereum/KDAO.permit.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {KDAO} from "ethereum/KDAO.sol"; +import {Test, stdError} from "forge-std/Test.sol"; +import {KDAO_ETHEREUM, KDAO_ETHEREUM_DEPLOYER} from "interfaces/kimlikdao/addresses.sol"; + +contract KDAOPermitTest is Test { + KDAO private kdao; + + function setUp() external { + vm.prank(KDAO_ETHEREUM_DEPLOYER); + kdao = new KDAO(); + } + + function testDomainSeparator() external view { + assertEq( + kdao.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("KDAO")), + keccak256(bytes("1")), + 0x1, + KDAO_ETHEREUM + ) + ) + ); + } + + function authorizePayment( + uint256 ownerPrivateKey, + address spender, + uint256 amount, + uint256 deadline, + uint256 nonce + ) internal view returns (uint8, bytes32, bytes32) { + bytes32 digest = keccak256( + abi.encodePacked( + "\x19\x01", + kdao.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9, + vm.addr(ownerPrivateKey), + spender, + amount, + nonce, + deadline + ) + ) + ) + ); + return vm.sign(ownerPrivateKey, digest); + } + + function testPermit() external { + { + uint256 deadline = block.timestamp + 1000; + (uint8 v, bytes32 r, bytes32 s) = + authorizePayment(1337, address(888), 100_000e6, deadline, 0); + kdao.permit(vm.addr(1337), address(888), 100_000e6, deadline, v, r, s); + + assertEq(kdao.allowance(vm.addr(1337), address(888)), 100_000e6); + } + + { + uint256 deadline = block.timestamp + 2000; + (uint8 v, bytes32 r, bytes32 s) = + authorizePayment(1337, address(888), 200_000e6, deadline, 1); + kdao.permit(vm.addr(1337), address(888), 200_000e6, deadline, v, r, s); + + assertEq(kdao.allowance(vm.addr(1337), address(888)), 200_000e6); + } + + { + uint256 deadline = block.timestamp + 3000; + (uint8 v, bytes32 r, bytes32 s) = + authorizePayment(1337, address(999), 300_000e6, deadline, 2); + kdao.permit(vm.addr(1337), address(999), 300_000e6, deadline, v, r, s); + + assertEq(kdao.allowance(vm.addr(1337), address(999)), 300_000e6); + } + } + + function testPermitMalformedSignature() external { + uint256 deadline = block.timestamp + 1000; + (uint8 v, bytes32 r, bytes32 s) = + authorizePayment(1337, address(888), 100_000e6, deadline, 0); + + vm.expectRevert(); + kdao.permit(vm.addr(1337), address(888), 100_000e6, deadline, 2, r, s); + + vm.expectRevert(); + kdao.permit(vm.addr(1338), address(888), 100_000e6, deadline, 2, r, s); + + kdao.permit(vm.addr(1337), address(888), 100_000e6, deadline, v, r, s); + } + + function testExpiredPermitSignature() external { + uint256 deadline = block.timestamp + 1000; + (uint8 v, bytes32 r, bytes32 s) = + authorizePayment(1337, address(888), 100_000e6, deadline, 0); + vm.warp(deadline + 1); + vm.expectRevert(); + kdao.permit(vm.addr(1337), address(888), 100_000e6, deadline, v, r, s); + } +} diff --git a/test/ethereum/KDAO.redeem.t.sol b/test/ethereum/KDAO.redeem.t.sol new file mode 100644 index 0000000..5f62d87 --- /dev/null +++ b/test/ethereum/KDAO.redeem.t.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {KDAO, MockKDAO} from "./MockKDAO.sol"; +import {Test, console, stdError} from "forge-std/Test.sol"; +import {USDT} from "interfaces/ethereum/addresses.sol"; +import {deployMockTokens} from "interfaces/ethereum/mockTokens.sol"; +import {IProtocolFund, ProtocolFund, RedeemInfoFrom} from "interfaces/kimlikdao/IProtocolFund.sol"; +import { + KDAO_ETHEREUM, + KDAO_ETHEREUM_DEPLOYER, + PROTOCOL_FUND, + PROTOCOL_FUND_DEPLOYER +} from "interfaces/kimlikdao/addresses.sol"; +import {MockERC20} from "interfaces/testing/MockERC20Permit.sol"; +import {MockProtocolFundV1} from "interfaces/testing/MockProtocolFundV1.sol"; +import {uint48x2From} from "interfaces/types/uint48x2.sol"; + +contract KDAORedeemTest is Test { + KDAO private kdao; + IProtocolFund private protocolFund; + + function setUp() external { + vm.prank(KDAO_ETHEREUM_DEPLOYER); + kdao = new MockKDAO(); + + vm.prank(PROTOCOL_FUND_DEPLOYER); + protocolFund = new MockProtocolFundV1(); + + assertEq(address(protocolFund), PROTOCOL_FUND); + + deployMockTokens(); + vm.deal(PROTOCOL_FUND, 100 ether); + + vm.prank(PROTOCOL_FUND); + MockERC20(address(USDT)).mint(100_000_000e6); + } + + function testInitialBalances() external view { + assertEq(PROTOCOL_FUND.balance, 100 ether); + assertEq(USDT.balanceOf(PROTOCOL_FUND), 100_000_000e6); + } + + function testRedeem() external { + // KimlikDAO protocol has 100M USDT and 100 ethere. + // address(20) owns 1% of the KimlikDAO protocol. + // address(20) redeems their entire stake. + assertEq(address(20).balance, 0); + assertEq(USDT.balanceOf(address(20)), 0); + assertEq(kdao.balanceOf(address(20)), 1_000_000e6); + assertEq(kdao.totalSupply(), 1_002_000e6); + assertEq(kdao.maxSupply(), 100_000_000e6); + assertEq(kdao.circulatingSupply(), 1_002_000e6); + + vm.prank(address(20), address(20)); + kdao.redeem(1_000_000e6); + + assertEq(kdao.balanceOf(address(20)), 0); + assertEq(address(20).balance, 1 ether); + assertEq(USDT.balanceOf(address(20)), 1_000_000e6); + assertEq(kdao.maxSupply(), 99_000_000e6); + assertEq(kdao.totalSupply(), 2_000e6); + assertEq(kdao.circulatingSupply(), 2_000e6); + } + + function testRedeemViaTransfer() external { + // KimlikDAO protocol has 100M USDT and 100 ethere. + // address(20) owns 1% of the KimlikDAO protocol. + // address(20) redeems their entire stake. + assertEq(address(20).balance, 0); + assertEq(USDT.balanceOf(address(20)), 0); + assertEq(kdao.balanceOf(address(20)), 1_000_000e6); + assertEq(kdao.totalSupply(), 1_002_000e6); + assertEq(kdao.maxSupply(), 100_000_000e6); + assertEq(kdao.circulatingSupply(), 1_002_000e6); + + vm.prank(address(20), address(20)); + kdao.transfer(PROTOCOL_FUND, 1_000_000e6); + + assertEq(kdao.balanceOf(address(20)), 0); + assertEq(address(20).balance, 1 ether); + assertEq(USDT.balanceOf(address(20)), 1_000_000e6); + assertEq(kdao.maxSupply(), 99_000_000e6); + assertEq(kdao.totalSupply(), 2_000e6); + assertEq(kdao.circulatingSupply(), 2_000e6); + } + + function testRedeemOnlyEOA() external { + vm.prank(address(20), address(21)); + vm.expectRevert(); + kdao.redeem(1_000_000e6); + } +} diff --git a/test/ethereum/KDAO.t.sol b/test/ethereum/KDAO.t.sol index 1ab8a46..3e9da4d 100644 --- a/test/ethereum/KDAO.t.sol +++ b/test/ethereum/KDAO.t.sol @@ -2,66 +2,178 @@ pragma solidity ^0.8.0; +import {MockKDAO} from "./MockKDAO.sol"; import {KDAO} from "ethereum/KDAO.sol"; -import {Test} from "forge-std/Test.sol"; -import {console2} from "forge-std/console2.sol"; -import {KDAO_ETHEREUM, KDAO_ETHEREUM_DEPLOYER} from "interfaces/kimlikdao/addresses.sol"; -import {uint48x2From} from "interfaces/types/uint48x2.sol"; - -contract KDAOPremined is KDAO { - constructor() { - balanceOf[address(0x1337)] = 100e6; - balanceOf[address(0x1338)] = 100e6; - balanceOf[address(0x1339)] = 100e6; - balanceOf[address(0x1310)] = 100e6; - - totals = uint48x2From(100_000_000e6, 4 * 100e6); - } -} +import {Test, stdError} from "forge-std/Test.sol"; +import { + KDAO_ETHEREUM, + KDAO_ETHEREUM_DEPLOYER, + PROTOCOL_FUND +} from "interfaces/kimlikdao/addresses.sol"; contract KDAOTest is Test { KDAO private kdao; function setUp() public { vm.prank(KDAO_ETHEREUM_DEPLOYER); - kdao = new KDAOPremined(); + kdao = new MockKDAO(); + } + + function testAddressConsistency() external pure { + assertEq(vm.computeCreateAddress(KDAO_ETHEREUM_DEPLOYER, 0), KDAO_ETHEREUM); + } + + function testMetadata() external view { + assertEq(kdao.name(), "KimlikDAO"); + assertEq(kdao.symbol(), "KDAO"); + assertEq(kdao.decimals(), 6); } - function testDomainSeparator() external view { - assertEq( - kdao.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes("KDAO")), - keccak256(bytes("1")), - 0x1, - KDAO_ETHEREUM - ) - ) - ); + function testTransferToContractNotAllowed() external { + vm.startPrank(address(0)); + vm.expectRevert(); + kdao.transfer(address(kdao), 100e6); + vm.stopPrank(); + } + + function testTransferToSelfPreservesBalance() external { + // Transfer to self and assert that the balance remains the same + vm.startPrank(address(0)); + for (uint256 i = 0; i <= 100; ++i) { + kdao.transfer(address(0), i * 1e6); + assertEq(kdao.balanceOf(address(0)), 100e6); + } + vm.stopPrank(); } - function testTransfer() external { - assertEq(kdao.balanceOf(address(0x1337)), 100e6); + function testTransferInsuffucientBalance() external { + vm.startPrank(address(2)); + for (uint256 i = 1; i < 100; ++i) { + vm.expectRevert(); + kdao.transfer(address(3), 100e6 + i); + } + vm.stopPrank(); + } + + function testTransferPreservesBalances() external { + // Transfer all balance of i to -3i (mod 19) + // Since this is bijective, all balances should remain the same + for (uint160 i = 0; i < 19; ++i) { + vm.prank(address(i)); + kdao.transfer(address((i * 16) % 19), 100e6); + } + for (uint160 i = 0; i < 20; ++i) { + assertEq(kdao.balanceOf(address(i)), 100e6); + } + // Transfer all balance of i to -5i (mod 19) for i = 0..19 + // This should move the balance of 19 to 0. + for (uint160 i = 0; i < 20; ++i) { + vm.prank(address(i)); + kdao.transfer(address((i * 16) % 19), 100e6); + } + assertEq(kdao.balanceOf(address(0)), 200e6); + assertEq(kdao.balanceOf(address(19)), 0); + for (uint160 i = 1; i < 19; ++i) { + assertEq(kdao.balanceOf(address(i)), 100e6); + } + } + + function testTransferFrom() external { + vm.prank(address(11)); + kdao.approve(address(12), 3); + + vm.startPrank(address(12)); + kdao.transferFrom(address(11), address(12), 1); + kdao.transferFrom(address(11), address(13), 1); + kdao.transferFrom(address(11), address(14), 1); + + vm.expectRevert(); + kdao.transferFrom(address(11), address(15), 1); + + assertEq(kdao.balanceOf(address(12)), 100e6 + 1); + assertEq(kdao.balanceOf(address(13)), 100e6 + 1); + assertEq(kdao.balanceOf(address(14)), 100e6 + 1); + } + + function testTransferFromDisallowedAddresses() external { + vm.startPrank(address(11)); + kdao.approve(address(11), 100e6); + + vm.expectRevert(); + kdao.transferFrom(address(11), address(kdao), 100e6); + + vm.expectRevert(); + kdao.transferFrom(address(11), PROTOCOL_FUND, 100e6); + + vm.stopPrank(); + } - vm.prank(address(0x1337)); - kdao.transfer(address(0x1338), 100e6); - assertEq(kdao.balanceOf(address(0x1337)), 0); - assertEq(kdao.balanceOf(address(0x1338)), 200e6); + function testTransferFromInfiniteApproval() external { + vm.prank(address(19)); + kdao.approve(address(11), ~uint256(0)); - vm.startPrank(address(0x1338)); + vm.startPrank(address(11)); + kdao.transferFrom(address(19), address(20), 50e6); + kdao.transferFrom(address(19), address(21), 50e6); vm.expectRevert(); - kdao.transfer(address(0x1339), 200e6 + 1); + kdao.transferFrom(address(19), address(22), 1); + } + + function testTransferFromToSelfPreservesBalance() external { + vm.startPrank(address(0)); + // Approve self for infinite KDAOs + kdao.approve(address(0), ~uint256(0)); + for (uint256 i = 0; i <= 100; ++i) { + kdao.transferFrom(address(0), address(0), i * 1e6); + assertEq(kdao.balanceOf(address(0)), 100e6); + } + // Do the same with exact amount approval + kdao.approve(address(0), (100e6 * 101) / 2); + for (uint256 i = 0; i <= 100; ++i) { + kdao.transferFrom(address(0), address(0), i * 1e6); + assertEq(kdao.balanceOf(address(0)), 100e6); + } + vm.stopPrank(); + } + + function testIncreaseDecreaseAllowance() external { + vm.startPrank(address(1)); + kdao.increaseAllowance(address(2), 12); + kdao.increaseAllowance(address(2), 18); + + assertEq(kdao.allowance(address(1), address(2)), 30); - assertEq(kdao.balanceOf(address(0x1338)), 200e6); + kdao.decreaseAllowance(address(2), 10); - kdao.transfer(address(0x1339), 200e6); + assertEq(kdao.allowance(address(1), address(2)), 20); vm.stopPrank(); - assertEq(kdao.balanceOf(address(0x1338)), 0); - assertEq(kdao.balanceOf(address(0x1339)), 300e6); + vm.prank(address(2)); + kdao.transferFrom(address(1), address(3), 20); + + assertEq(kdao.balanceOf(address(3)), 100e6 + 20); + assertEq(kdao.balanceOf(address(1)), 100e6 - 20); + assertEq(kdao.allowance(address(1), address(2)), 0); + + vm.startPrank(address(1)); + kdao.increaseAllowance(address(2), 10); + kdao.decreaseAllowance(address(2), 10); + + assertEq(kdao.allowance(address(1), address(2)), 0); + } + + function testIncreaseDecreaseAllowanceOverflow() external { + vm.startPrank(address(1)); + kdao.increaseAllowance(address(3), 10); + + vm.expectRevert(); + kdao.decreaseAllowance(address(3), 11); + + kdao.approve(address(3), type(uint256).max - 1); + + vm.expectRevert(stdError.arithmeticError); + kdao.increaseAllowance(address(3), 2); + + vm.stopPrank(); } } diff --git a/test/ethereum/MockKDAO.sol b/test/ethereum/MockKDAO.sol new file mode 100644 index 0000000..7974ee0 --- /dev/null +++ b/test/ethereum/MockKDAO.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import {KDAO} from "ethereum/KDAO.sol"; +import {uint48x2From} from "interfaces/types/uint48x2.sol"; + +// Premined KDAO contract for testing purposes. +// Addresses 0 through 19 are given 100 KDAOs. +contract MockKDAO is KDAO { + constructor() { + for (uint160 i = 0; i < 20; ++i) { + balanceOf[address(i)] = 100e6; + } + + balanceOf[address(20)] = 1_000_000e6; + + totals = uint48x2From(100_000_000e6, 20 * 100e6 + 1_000_000e6); + } +} diff --git a/test/zksync/KDAO.permit.t.sol b/test/zksync/KDAO.permit.t.sol index 0746925..3919159 100644 --- a/test/zksync/KDAO.permit.t.sol +++ b/test/zksync/KDAO.permit.t.sol @@ -30,6 +30,23 @@ contract KDAOPermitTest is Test { mintAll(1e12, VOTING); } + function testDomainSeparator() external view { + assertEq( + kdao.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes("KDAO")), + keccak256(bytes("1")), + 0x144, + KDAO_ZKSYNC + ) + ) + ); + } + function authorizePayment( uint256 ownerPrivateKey, address spender, diff --git a/test/zksync/KDAO.t.sol b/test/zksync/KDAO.t.sol index 6fb06bc..45c7344 100644 --- a/test/zksync/KDAO.t.sol +++ b/test/zksync/KDAO.t.sol @@ -46,23 +46,6 @@ contract KDAOTest is Test { assertEq(computeZkSyncCreateAddress(KDAO_ZKSYNC_DEPLOYER, 0), KDAO_ZKSYNC); } - function testDomainSeparator() external view { - assertEq( - kdao.DOMAIN_SEPARATOR(), - keccak256( - abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes("KDAO")), - keccak256(bytes("1")), - 0x144, - KDAO_ZKSYNC - ) - ) - ); - } - function testMetadataMethods() external view { assertEq(kdao.decimals(), kdaol.decimals()); // Increase coverage so we can always aim at 100%.