From 547d98b09eac04f4bf8fe0fc65a63147e99c2748 Mon Sep 17 00:00:00 2001 From: Riley Date: Fri, 29 Sep 2023 01:08:59 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20EIP-6909=20(#389)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: t11s --- .gas-snapshot | 76 ++++--- README.md | 1 + src/test/ERC6909.t.sol | 307 +++++++++++++++++++++++++++ src/test/utils/mocks/MockERC6909.sol | 22 ++ src/tokens/ERC6909.sol | 140 ++++++++++++ 5 files changed, 519 insertions(+), 27 deletions(-) create mode 100644 src/test/ERC6909.t.sol create mode 100644 src/test/utils/mocks/MockERC6909.sol create mode 100644 src/tokens/ERC6909.sol diff --git a/.gas-snapshot b/.gas-snapshot index a5babd3c..057990e7 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -42,11 +42,11 @@ DSTestPlusTest:testRelApproxEqBothZeroesPasses() (gas: 425) ERC1155Test:testApproveAll() (gas: 31009) ERC1155Test:testApproveAll(address,bool) (runs: 256, μ: 16872, ~: 11440) ERC1155Test:testBatchBalanceOf() (gas: 157631) -ERC1155Test:testBatchBalanceOf(address[],uint256[],uint256[],bytes) (runs: 256, μ: 3309892, ~: 2596398) +ERC1155Test:testBatchBalanceOf(address[],uint256[],uint256[],bytes) (runs: 256, μ: 3309878, ~: 2596398) ERC1155Test:testBatchBurn() (gas: 151074) -ERC1155Test:testBatchBurn(address,uint256[],uint256[],uint256[],bytes) (runs: 256, μ: 3490281, ~: 3058687) +ERC1155Test:testBatchBurn(address,uint256[],uint256[],uint256[],bytes) (runs: 256, μ: 3490436, ~: 3058687) ERC1155Test:testBatchMintToEOA() (gas: 137337) -ERC1155Test:testBatchMintToEOA(address,uint256[],uint256[],bytes) (runs: 256, μ: 3309311, ~: 2953384) +ERC1155Test:testBatchMintToEOA(address,uint256[],uint256[],bytes) (runs: 256, μ: 3293138, ~: 2941772) ERC1155Test:testBatchMintToERC1155Recipient() (gas: 995703) ERC1155Test:testBatchMintToERC1155Recipient(uint256[],uint256[],bytes) (runs: 256, μ: 7395823, ~: 6396323) ERC1155Test:testBurn() (gas: 38598) @@ -54,7 +54,7 @@ ERC1155Test:testBurn(address,uint256,uint256,bytes,uint256) (runs: 256, μ: 3991 ERC1155Test:testFailBalanceOfBatchWithArrayMismatch() (gas: 7933) ERC1155Test:testFailBalanceOfBatchWithArrayMismatch(address[],uint256[]) (runs: 256, μ: 53386, ~: 54066) ERC1155Test:testFailBatchBurnInsufficientBalance() (gas: 136156) -ERC1155Test:testFailBatchBurnInsufficientBalance(address,uint256[],uint256[],uint256[],bytes) (runs: 256, μ: 1301088, ~: 440335) +ERC1155Test:testFailBatchBurnInsufficientBalance(address,uint256[],uint256[],uint256[],bytes) (runs: 256, μ: 1301377, ~: 440335) ERC1155Test:testFailBatchBurnWithArrayLengthMismatch() (gas: 135542) ERC1155Test:testFailBatchBurnWithArrayLengthMismatch(address,uint256[],uint256[],uint256[],bytes) (runs: 256, μ: 77137, ~: 78751) ERC1155Test:testFailBatchMintToNonERC1155Recipient() (gas: 167292) @@ -78,17 +78,17 @@ ERC1155Test:testFailMintToWrongReturnDataERC155Recipient(uint256,uint256,bytes) ERC1155Test:testFailMintToZero() (gas: 33705) ERC1155Test:testFailMintToZero(uint256,uint256,bytes) (runs: 256, μ: 33815, ~: 34546) ERC1155Test:testFailSafeBatchTransferFromToNonERC1155Recipient() (gas: 321377) -ERC1155Test:testFailSafeBatchTransferFromToNonERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3541063, ~: 2963551) +ERC1155Test:testFailSafeBatchTransferFromToNonERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3541296, ~: 2963551) ERC1155Test:testFailSafeBatchTransferFromToRevertingERC1155Recipient() (gas: 512956) -ERC1155Test:testFailSafeBatchTransferFromToRevertingERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3732600, ~: 3155082) +ERC1155Test:testFailSafeBatchTransferFromToRevertingERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3732833, ~: 3155082) ERC1155Test:testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient() (gas: 464847) -ERC1155Test:testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3684518, ~: 3107003) +ERC1155Test:testFailSafeBatchTransferFromToWrongReturnDataERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3684751, ~: 3107003) ERC1155Test:testFailSafeBatchTransferFromToZero() (gas: 286556) -ERC1155Test:testFailSafeBatchTransferFromToZero(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3505494, ~: 2928396) +ERC1155Test:testFailSafeBatchTransferFromToZero(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 3505805, ~: 2928396) ERC1155Test:testFailSafeBatchTransferFromWithArrayLengthMismatch() (gas: 162674) ERC1155Test:testFailSafeBatchTransferFromWithArrayLengthMismatch(address,uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 81184, ~: 82042) ERC1155Test:testFailSafeBatchTransferInsufficientBalance() (gas: 163555) -ERC1155Test:testFailSafeBatchTransferInsufficientBalance(address,uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 1528611, ~: 499517) +ERC1155Test:testFailSafeBatchTransferInsufficientBalance(address,uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 1528922, ~: 499517) ERC1155Test:testFailSafeTransferFromInsufficientBalance() (gas: 63245) ERC1155Test:testFailSafeTransferFromInsufficientBalance(address,uint256,uint256,uint256,bytes,bytes) (runs: 256, μ: 63986, ~: 67405) ERC1155Test:testFailSafeTransferFromSelfInsufficientBalance() (gas: 34297) @@ -106,16 +106,16 @@ ERC1155Test:testMintToEOA(address,uint256,uint256,bytes) (runs: 256, μ: 35562, ERC1155Test:testMintToERC1155Recipient() (gas: 661411) ERC1155Test:testMintToERC1155Recipient(uint256,uint256,bytes) (runs: 256, μ: 691094, ~: 684374) ERC1155Test:testSafeBatchTransferFromToEOA() (gas: 297822) -ERC1155Test:testSafeBatchTransferFromToEOA(address,uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 4802177, ~: 3787204) +ERC1155Test:testSafeBatchTransferFromToEOA(address,uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 4801633, ~: 3787204) ERC1155Test:testSafeBatchTransferFromToERC1155Recipient() (gas: 1175327) -ERC1155Test:testSafeBatchTransferFromToERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 7759308, ~: 6618414) +ERC1155Test:testSafeBatchTransferFromToERC1155Recipient(uint256[],uint256[],uint256[],bytes,bytes) (runs: 256, μ: 7759541, ~: 6618414) ERC1155Test:testSafeTransferFromSelf() (gas: 64177) ERC1155Test:testSafeTransferFromSelf(uint256,uint256,bytes,uint256,address,bytes) (runs: 256, μ: 64552, ~: 68564) ERC1155Test:testSafeTransferFromToEOA() (gas: 93167) ERC1155Test:testSafeTransferFromToEOA(uint256,uint256,bytes,uint256,address,bytes) (runs: 256, μ: 92808, ~: 97450) ERC1155Test:testSafeTransferFromToERC1155Recipient() (gas: 739583) -ERC1155Test:testSafeTransferFromToERC1155Recipient(uint256,uint256,bytes,uint256,bytes) (runs: 256, μ: 769591, ~: 765729) -ERC20Invariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2365) +ERC1155Test:testSafeTransferFromToERC1155Recipient(uint256,uint256,bytes,uint256,bytes) (runs: 256, μ: 769513, ~: 765729) +ERC20Invariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2369) ERC20Test:invariantMetadata() (runs: 256, calls: 3840, reverts: 2537) ERC20Test:testApprove() (gas: 31058) ERC20Test:testApprove(address,uint256) (runs: 256, μ: 30424, ~: 31280) @@ -146,7 +146,7 @@ ERC20Test:testTransfer() (gas: 60272) ERC20Test:testTransfer(address,uint256) (runs: 256, μ: 58773, ~: 60484) ERC20Test:testTransferFrom() (gas: 83777) ERC20Test:testTransferFrom(address,uint256,uint256) (runs: 256, μ: 86464, ~: 92841) -ERC4626Test:invariantMetadata() (runs: 256, calls: 3840, reverts: 2921) +ERC4626Test:invariantMetadata() (runs: 256, calls: 3840, reverts: 2961) ERC4626Test:testFailDepositWithNoApproval() (gas: 13369) ERC4626Test:testFailDepositWithNotEnoughApproval() (gas: 87005) ERC4626Test:testFailDepositZero() (gas: 7780) @@ -156,13 +156,35 @@ ERC4626Test:testFailRedeemWithNotEnoughShareAmount() (gas: 203643) ERC4626Test:testFailRedeemZero() (gas: 7967) ERC4626Test:testFailWithdrawWithNoUnderlyingAmount() (gas: 32289) ERC4626Test:testFailWithdrawWithNotEnoughUnderlyingAmount() (gas: 203607) -ERC4626Test:testMetadata(string,string) (runs: 256, μ: 1506582, ~: 1495306) +ERC4626Test:testMetadata(string,string) (runs: 256, μ: 1506515, ~: 1495306) ERC4626Test:testMintZero() (gas: 54607) ERC4626Test:testMultipleMintDepositRedeemWithdraw() (gas: 411804) ERC4626Test:testSingleDepositWithdraw(uint128) (runs: 256, μ: 201539, ~: 201550) ERC4626Test:testSingleMintRedeem(uint128) (runs: 256, μ: 201465, ~: 201476) ERC4626Test:testVaultInteractionsForSomeoneElse() (gas: 286238) ERC4626Test:testWithdrawZero() (gas: 52468) +ERC6909Test:testApprove() (gas: 31594) +ERC6909Test:testApprove(address,uint256,uint256) (runs: 256, μ: 30905, ~: 31838) +ERC6909Test:testBurn() (gas: 58841) +ERC6909Test:testBurn(address,uint256,uint256) (runs: 256, μ: 41755, ~: 42865) +ERC6909Test:testFailTransfer() (gas: 10823) +ERC6909Test:testFailTransfer(address,address,uint256,uint256) (runs: 256, μ: 13203, ~: 13203) +ERC6909Test:testFailTransferFrom() (gas: 10846) +ERC6909Test:testFailTransferFrom(address,address,uint256,uint256) (runs: 256, μ: 13426, ~: 13426) +ERC6909Test:testFailTransferFromNotAuthorized() (gas: 58287) +ERC6909Test:testFailTransferFromNotAuthorized(address,address,uint256,uint256) (runs: 256, μ: 60842, ~: 60842) +ERC6909Test:testMint() (gas: 54790) +ERC6909Test:testMint(address,uint256,uint256) (runs: 256, μ: 53116, ~: 54982) +ERC6909Test:testSetOperator() (gas: 31115) +ERC6909Test:testSetOperator(address,bool) (runs: 256, μ: 17261, ~: 11596) +ERC6909Test:testTransfer() (gas: 83944) +ERC6909Test:testTransfer(address,address,uint256,uint256,uint256) (runs: 256, μ: 82256, ~: 86620) +ERC6909Test:testTransferFromAsOperator() (gas: 109488) +ERC6909Test:testTransferFromAsOperator(address,address,uint256,uint256,uint256) (runs: 256, μ: 107795, ~: 112159) +ERC6909Test:testTransferFromWithApproval() (gas: 114045) +ERC6909Test:testTransferFromWithApproval(address,address,uint256,uint256,uint256) (runs: 256, μ: 111390, ~: 116764) +ERC6909Test:testTransferFromWithInfiniteApproval() (gas: 113608) +ERC6909Test:testTransferFromWithInfiniteApproval(address,address,uint256,uint256,uint256) (runs: 256, μ: 111864, ~: 116306) ERC721Test:invariantMetadata() (runs: 256, calls: 3840, reverts: 2156) ERC721Test:testApprove() (gas: 78427) ERC721Test:testApprove(address,uint256) (runs: 256, μ: 78637, ~: 78637) @@ -468,7 +490,7 @@ SafeCastLibTest:testSafeCastTo80(uint256) (runs: 256, μ: 2736, ~: 2736) SafeCastLibTest:testSafeCastTo88(uint256) (runs: 256, μ: 2755, ~: 2755) SafeCastLibTest:testSafeCastTo96() (gas: 536) SafeCastLibTest:testSafeCastTo96(uint256) (runs: 256, μ: 2800, ~: 2800) -SafeTransferLibTest:testApproveWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 2675, ~: 2231) +SafeTransferLibTest:testApproveWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 2684, ~: 2231) SafeTransferLibTest:testApproveWithMissingReturn() (gas: 30757) SafeTransferLibTest:testApproveWithMissingReturn(address,uint256,bytes) (runs: 256, μ: 30334, ~: 31572) SafeTransferLibTest:testApproveWithNonContract() (gas: 3041) @@ -477,7 +499,7 @@ SafeTransferLibTest:testApproveWithReturnsTooMuch() (gas: 31140) SafeTransferLibTest:testApproveWithReturnsTooMuch(address,uint256,bytes) (runs: 256, μ: 30802, ~: 32040) SafeTransferLibTest:testApproveWithStandardERC20() (gas: 30888) SafeTransferLibTest:testApproveWithStandardERC20(address,uint256,bytes) (runs: 256, μ: 30528, ~: 31766) -SafeTransferLibTest:testFailApproveWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 84301, ~: 77915) +SafeTransferLibTest:testFailApproveWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 83705, ~: 77915) SafeTransferLibTest:testFailApproveWithReturnsFalse() (gas: 5633) SafeTransferLibTest:testFailApproveWithReturnsFalse(address,uint256,bytes) (runs: 256, μ: 6486, ~: 6481) SafeTransferLibTest:testFailApproveWithReturnsTooLittle() (gas: 5574) @@ -486,7 +508,7 @@ SafeTransferLibTest:testFailApproveWithReturnsTwo(address,uint256,bytes) (runs: SafeTransferLibTest:testFailApproveWithReverting() (gas: 5508) SafeTransferLibTest:testFailApproveWithReverting(address,uint256,bytes) (runs: 256, μ: 6409, ~: 6404) SafeTransferLibTest:testFailTransferETHToContractWithoutFallback() (gas: 7244) -SafeTransferLibTest:testFailTransferETHToContractWithoutFallback(uint256,bytes) (runs: 256, μ: 7757, ~: 8055) +SafeTransferLibTest:testFailTransferETHToContractWithoutFallback(uint256,bytes) (runs: 256, μ: 7758, ~: 8055) SafeTransferLibTest:testFailTransferFromWithGarbage(address,address,uint256,bytes,bytes) (runs: 256, μ: 122802, ~: 117413) SafeTransferLibTest:testFailTransferFromWithReturnsFalse() (gas: 13675) SafeTransferLibTest:testFailTransferFromWithReturnsFalse(address,address,uint256,bytes) (runs: 256, μ: 14605, ~: 14600) @@ -495,7 +517,7 @@ SafeTransferLibTest:testFailTransferFromWithReturnsTooLittle(address,address,uin SafeTransferLibTest:testFailTransferFromWithReturnsTwo(address,address,uint256,bytes) (runs: 256, μ: 14571, ~: 14566) SafeTransferLibTest:testFailTransferFromWithReverting() (gas: 9757) SafeTransferLibTest:testFailTransferFromWithReverting(address,address,uint256,bytes) (runs: 256, μ: 10685, ~: 10680) -SafeTransferLibTest:testFailTransferWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 90210, ~: 83995) +SafeTransferLibTest:testFailTransferWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 89567, ~: 83995) SafeTransferLibTest:testFailTransferWithReturnsFalse() (gas: 8538) SafeTransferLibTest:testFailTransferWithReturnsFalse(address,uint256,bytes) (runs: 256, μ: 9457, ~: 9452) SafeTransferLibTest:testFailTransferWithReturnsTooLittle() (gas: 8544) @@ -504,17 +526,17 @@ SafeTransferLibTest:testFailTransferWithReturnsTwo(address,uint256,bytes) (runs: SafeTransferLibTest:testFailTransferWithReverting() (gas: 8500) SafeTransferLibTest:testFailTransferWithReverting(address,uint256,bytes) (runs: 256, μ: 9356, ~: 9351) SafeTransferLibTest:testTransferETH() (gas: 34592) -SafeTransferLibTest:testTransferETH(address,uint256,bytes) (runs: 256, μ: 35312, ~: 37975) -SafeTransferLibTest:testTransferFromWithGarbage(address,address,uint256,bytes,bytes) (runs: 256, μ: 2915, ~: 2247) +SafeTransferLibTest:testTransferETH(address,uint256,bytes) (runs: 256, μ: 35455, ~: 37975) +SafeTransferLibTest:testTransferFromWithGarbage(address,address,uint256,bytes,bytes) (runs: 256, μ: 2914, ~: 2247) SafeTransferLibTest:testTransferFromWithMissingReturn() (gas: 49196) -SafeTransferLibTest:testTransferFromWithMissingReturn(address,address,uint256,bytes) (runs: 256, μ: 48350, ~: 49599) +SafeTransferLibTest:testTransferFromWithMissingReturn(address,address,uint256,bytes) (runs: 256, μ: 48350, ~: 49602) SafeTransferLibTest:testTransferFromWithNonContract() (gas: 3047) -SafeTransferLibTest:testTransferFromWithNonContract(address,address,address,uint256,bytes) (runs: 256, μ: 4235, ~: 4240) +SafeTransferLibTest:testTransferFromWithNonContract(address,address,address,uint256,bytes) (runs: 256, μ: 4245, ~: 4240) SafeTransferLibTest:testTransferFromWithReturnsTooMuch() (gas: 49820) -SafeTransferLibTest:testTransferFromWithReturnsTooMuch(address,address,uint256,bytes) (runs: 256, μ: 48997, ~: 50238) +SafeTransferLibTest:testTransferFromWithReturnsTooMuch(address,address,uint256,bytes) (runs: 256, μ: 48997, ~: 50242) SafeTransferLibTest:testTransferFromWithStandardERC20() (gas: 47612) -SafeTransferLibTest:testTransferFromWithStandardERC20(address,address,uint256,bytes) (runs: 256, μ: 46782, ~: 48049) -SafeTransferLibTest:testTransferWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 2631, ~: 2187) +SafeTransferLibTest:testTransferFromWithStandardERC20(address,address,uint256,bytes) (runs: 256, μ: 46782, ~: 48048) +SafeTransferLibTest:testTransferWithGarbage(address,uint256,bytes,bytes) (runs: 256, μ: 2640, ~: 2187) SafeTransferLibTest:testTransferWithMissingReturn() (gas: 36672) SafeTransferLibTest:testTransferWithMissingReturn(address,uint256,bytes) (runs: 256, μ: 36007, ~: 37552) SafeTransferLibTest:testTransferWithNonContract() (gas: 3018) @@ -530,7 +552,7 @@ SignedWadMathTest:testFailWadMulEdgeCase2() (gas: 309) SignedWadMathTest:testFailWadMulOverflow(int256,int256) (runs: 256, μ: 354, ~: 319) SignedWadMathTest:testWadDiv(uint256,uint256,bool,bool) (runs: 256, μ: 5697, ~: 5714) SignedWadMathTest:testWadMul(uint256,uint256,bool,bool) (runs: 256, μ: 5745, ~: 5712) -WETHInvariants:invariantTotalSupplyEqualsBalance() (runs: 256, calls: 3840, reverts: 1838) +WETHInvariants:invariantTotalSupplyEqualsBalance() (runs: 256, calls: 3840, reverts: 1839) WETHTest:testDeposit() (gas: 63535) WETHTest:testDeposit(uint256) (runs: 256, μ: 63337, ~: 65880) WETHTest:testFallbackDeposit() (gas: 63249) diff --git a/README.md b/README.md index 2bc3e3a8..87fda969 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ tokens ├─ ERC20 — "Modern and gas efficient ERC20 + EIP-2612 implementation" ├─ ERC721 — "Modern, minimalist, and gas efficient ERC721 implementation" ├─ ERC1155 — "Minimalist and gas efficient standard ERC1155 implementation" +├─ ERC6909 — "Minimalist and gas efficient standard ERC6909 implementation" utils ├─ SSTORE2 — "Library for cheaper reads and writes to persistent storage" ├─ CREATE3 — "Deploy to deterministic addresses without an initcode factor" diff --git a/src/test/ERC6909.t.sol b/src/test/ERC6909.t.sol new file mode 100644 index 00000000..8cf0d993 --- /dev/null +++ b/src/test/ERC6909.t.sol @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {DSTestPlus} from "./utils/DSTestPlus.sol"; +import {DSInvariantTest} from "./utils/DSInvariantTest.sol"; + +import {MockERC6909} from "./utils/mocks/MockERC6909.sol"; + +contract ERC6909Test is DSTestPlus { + MockERC6909 token; + + mapping(address => mapping(uint256 => uint256)) public userMintAmounts; + mapping(address => mapping(uint256 => uint256)) public userTransferOrBurnAmounts; + + function setUp() public { + token = new MockERC6909(); + } + + function testMint() public { + token.mint(address(0xBEEF), 1337, 100); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 100); + assertEq(token.totalSupply(1337), 100); + } + + function testBurn() public { + token.mint(address(0xBEEF), 1337, 100); + token.burn(address(0xBEEF), 1337, 70); + + assertEq(token.balanceOf(address(0xBEEF), 1337), 30); + assertEq(token.totalSupply(1337), 30); + } + + function testSetOperator() public { + token.setOperator(address(0xBEEF), true); + + assertTrue(token.isOperator(address(this), address(0xBEEF))); + } + + function testApprove() public { + token.approve(address(0xBEEF), 1337, 100); + + assertEq(token.allowance(address(this), address(0xBEEF), 1337), 100); + } + + function testTransfer() public { + address sender = address(0xABCD); + + token.mint(sender, 1337, 100); + + hevm.prank(sender); + token.transfer(address(0xBEEF), 1337, 70); + + assertEq(token.balanceOf(sender, 1337), 30); + assertEq(token.balanceOf(address(0xBEEF), 1337), 70); + } + + function testTransferFromWithApproval() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + token.mint(sender, 1337, 100); + + hevm.prank(sender); + token.approve(address(this), 1337, 100); + + token.transferFrom(sender, receiver, 1337, 70); + + assertEq(token.allowance(sender, address(this), 1337), 30); + assertEq(token.balanceOf(sender, 1337), 30); + assertEq(token.balanceOf(receiver, 1337), 70); + } + + function testTransferFromWithInfiniteApproval() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + token.mint(sender, 1337, 100); + + hevm.prank(sender); + token.approve(address(this), 1337, type(uint256).max); + + token.transferFrom(sender, receiver, 1337, 70); + + assertEq(token.allowance(sender, address(this), 1337), type(uint256).max); + assertEq(token.balanceOf(sender, 1337), 30); + assertEq(token.balanceOf(receiver, 1337), 70); + } + + function testTransferFromAsOperator() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + token.mint(sender, 1337, 100); + + hevm.prank(sender); + token.setOperator(address(this), true); + + token.transferFrom(sender, receiver, 1337, 70); + + assertEq(token.balanceOf(sender, 1337), 30); + assertEq(token.balanceOf(receiver, 1337), 70); + } + + function testFailTransfer() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + hevm.prank(sender); + token.transferFrom(sender, receiver, 1337, 1); + } + + function testFailTransferFrom() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + hevm.prank(sender); + token.transferFrom(sender, receiver, 1337, 1); + } + + function testFailTransferFromNotAuthorized() public { + address sender = address(0xABCD); + address receiver = address(0xBEEF); + + token.mint(sender, 1337, 100); + + token.transferFrom(sender, receiver, 1337, 100); + } + + function testMint( + address receiver, + uint256 id, + uint256 amount + ) public { + token.mint(receiver, id, amount); + + assertEq(token.balanceOf(receiver, id), amount); + assertEq(token.totalSupply(id), amount); + } + + function testBurn( + address sender, + uint256 id, + uint256 amount + ) public { + token.mint(sender, id, amount); + token.burn(sender, id, amount); + + assertEq(token.balanceOf(sender, id), 0); + assertEq(token.totalSupply(id), 0); + } + + function testSetOperator(address operator, bool approved) public { + token.setOperator(operator, approved); + + assertBoolEq(token.isOperator(address(this), operator), approved); + } + + function testApprove( + address spender, + uint256 id, + uint256 amount + ) public { + token.approve(spender, id, amount); + + assertEq(token.allowance(address(this), spender, id), amount); + } + + function testTransfer( + address sender, + address receiver, + uint256 id, + uint256 mintAmount, + uint256 transferAmount + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(sender, id, mintAmount); + + hevm.prank(sender); + token.transfer(receiver, id, transferAmount); + + if (sender == receiver) { + assertEq(token.balanceOf(sender, id), mintAmount); + } else { + assertEq(token.balanceOf(sender, id), mintAmount - transferAmount); + assertEq(token.balanceOf(receiver, id), transferAmount); + } + } + + function testTransferFromWithApproval( + address sender, + address receiver, + uint256 id, + uint256 mintAmount, + uint256 transferAmount + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(sender, id, mintAmount); + + hevm.prank(sender); + token.approve(address(this), id, mintAmount); + + token.transferFrom(sender, receiver, id, transferAmount); + + if (mintAmount == type(uint256).max) { + assertEq(token.allowance(sender, address(this), id), type(uint256).max); + } else { + assertEq(token.allowance(sender, address(this), id), mintAmount - transferAmount); + } + + if (sender == receiver) { + assertEq(token.balanceOf(sender, id), mintAmount); + } else { + assertEq(token.balanceOf(sender, id), mintAmount - transferAmount); + assertEq(token.balanceOf(receiver, id), transferAmount); + } + } + + function testTransferFromWithInfiniteApproval( + address sender, + address receiver, + uint256 id, + uint256 mintAmount, + uint256 transferAmount + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(sender, id, mintAmount); + + hevm.prank(sender); + token.approve(address(this), id, type(uint256).max); + + token.transferFrom(sender, receiver, id, transferAmount); + + assertEq(token.allowance(sender, address(this), id), type(uint256).max); + + if (sender == receiver) { + assertEq(token.balanceOf(sender, id), mintAmount); + } else { + assertEq(token.balanceOf(sender, id), mintAmount - transferAmount); + assertEq(token.balanceOf(receiver, id), transferAmount); + } + } + + function testTransferFromAsOperator( + address sender, + address receiver, + uint256 id, + uint256 mintAmount, + uint256 transferAmount + ) public { + transferAmount = bound(transferAmount, 0, mintAmount); + + token.mint(sender, id, mintAmount); + + hevm.prank(sender); + token.setOperator(address(this), true); + + token.transferFrom(sender, receiver, id, transferAmount); + + if (sender == receiver) { + assertEq(token.balanceOf(sender, id), mintAmount); + } else { + assertEq(token.balanceOf(sender, id), mintAmount - transferAmount); + assertEq(token.balanceOf(receiver, id), transferAmount); + } + } + + function testFailTransfer( + address sender, + address receiver, + uint256 id, + uint256 amount + ) public { + amount = bound(amount, 1, type(uint256).max); + + hevm.prank(sender); + token.transfer(receiver, id, amount); + } + + function testFailTransferFrom( + address sender, + address receiver, + uint256 id, + uint256 amount + ) public { + amount = bound(amount, 1, type(uint256).max); + + hevm.prank(sender); + token.transferFrom(sender, receiver, id, amount); + } + + function testFailTransferFromNotAuthorized( + address sender, + address receiver, + uint256 id, + uint256 amount + ) public { + amount = bound(amount, 1, type(uint256).max); + + token.mint(sender, id, amount); + + token.transferFrom(sender, receiver, id, amount); + } +} diff --git a/src/test/utils/mocks/MockERC6909.sol b/src/test/utils/mocks/MockERC6909.sol new file mode 100644 index 00000000..5f7eabed --- /dev/null +++ b/src/test/utils/mocks/MockERC6909.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {ERC6909} from "../../../tokens/ERC6909.sol"; + +contract MockERC6909 is ERC6909 { + function mint( + address receiver, + uint256 id, + uint256 amount + ) public virtual { + _mint(receiver, id, amount); + } + + function burn( + address sender, + uint256 id, + uint256 amount + ) public virtual { + _burn(sender, id, amount); + } +} diff --git a/src/tokens/ERC6909.sol b/src/tokens/ERC6909.sol new file mode 100644 index 00000000..eb04e172 --- /dev/null +++ b/src/tokens/ERC6909.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +/// @notice Minimalist and gas efficient standard ERC6909 implementation. +/// @author Solmate (https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC6909.sol) +abstract contract ERC6909 { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event OperatorSet(address indexed owner, address indexed operator, bool approved); + + event Approval(address indexed owner, address indexed spender, uint256 indexed id, uint256 amount); + + event Transfer(address caller, address indexed from, address indexed to, uint256 indexed id, uint256 amount); + + /*////////////////////////////////////////////////////////////// + ERC6909 STORAGE + //////////////////////////////////////////////////////////////*/ + + mapping(uint256 => uint256) public totalSupply; + + mapping(address => mapping(address => bool)) public isOperator; + + mapping(address => mapping(uint256 => uint256)) public balanceOf; + + mapping(address => mapping(address => mapping(uint256 => uint256))) public allowance; + + /*////////////////////////////////////////////////////////////// + ERC6909 LOGIC + //////////////////////////////////////////////////////////////*/ + + function transfer( + address receiver, + uint256 id, + uint256 amount + ) public virtual returns (bool) { + balanceOf[msg.sender][id] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[receiver][id] += amount; + } + + emit Transfer(msg.sender, msg.sender, receiver, id, amount); + + return true; + } + + function transferFrom( + address sender, + address receiver, + uint256 id, + uint256 amount + ) public virtual returns (bool) { + if (msg.sender != sender && !isOperator[sender][msg.sender]) { + uint256 allowed = allowance[sender][msg.sender][id]; + if (allowed != type(uint256).max) allowance[sender][msg.sender][id] = allowed - amount; + } + + balanceOf[sender][id] -= amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + balanceOf[receiver][id] += amount; + } + + emit Transfer(msg.sender, sender, receiver, id, amount); + + return true; + } + + function approve( + address spender, + uint256 id, + uint256 amount + ) public virtual returns (bool) { + allowance[msg.sender][spender][id] = amount; + + emit Approval(msg.sender, spender, id, amount); + + return true; + } + + function setOperator(address operator, bool approved) public virtual returns (bool) { + isOperator[msg.sender][operator] = approved; + + emit OperatorSet(msg.sender, operator, approved); + + return true; + } + + /*////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0xb2e69f8a; // ERC165 Interface ID for ERC6909 + } + + /*////////////////////////////////////////////////////////////// + INTERNAL MINT/BURN LOGIC + //////////////////////////////////////////////////////////////*/ + + function _mint( + address receiver, + uint256 id, + uint256 amount + ) internal virtual { + balanceOf[receiver][id] += amount; + + // Cannot overflow because the sum of all user + // balances can't exceed the max uint256 value. + unchecked { + totalSupply[id] += amount; + } + + emit Transfer(msg.sender, address(0), receiver, id, amount); + } + + function _burn( + address sender, + uint256 id, + uint256 amount + ) internal virtual { + balanceOf[sender][id] -= amount; + + // Cannot underflow because a user's balance + // will never be larger than the total supply. + unchecked { + totalSupply[id] -= amount; + } + + emit Transfer(msg.sender, sender, address(0), id, amount); + } +}