From 44c5e4153917e7e989dce0513e50fa0731e7f629 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 17 Nov 2023 14:51:24 -0500 Subject: [PATCH 1/4] add bitmap nonces --- patterns/bitmap-nonces/README.md | 110 ++++++++++++++++++ patterns/bitmap-nonces/TransferRelay.sol | 58 ++++++++++ test/TransferRelay.t.sol | 135 +++++++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 patterns/bitmap-nonces/README.md create mode 100644 patterns/bitmap-nonces/TransferRelay.sol create mode 100644 test/TransferRelay.t.sol diff --git a/patterns/bitmap-nonces/README.md b/patterns/bitmap-nonces/README.md new file mode 100644 index 0000000..a54ade0 --- /dev/null +++ b/patterns/bitmap-nonces/README.md @@ -0,0 +1,110 @@ +# Bitmap Nonces + +- [📜 Example Code](./TransferRelay.sol) +- [🐞 Tests](../../test/TransferRelay.t.sol) + +What do filling a stop-loss order, executing a governance proposal, or meta transactions have in common? They're all operations meant to be consumed once and only once. This guarantee needs to be enforced on-chain to prevent replay attacks. To do this, many protocols will derive some unique identifier from the operation's parameters, then map that identifier to a storage slot dedicated to that operation which holds a status flag. + +## Naive Approach + +Take the following example of a protocol that executes off-chain signed messages to transfer (compliant) ERC20 tokens on behalf of the signer after a given time: + +```solidity +contract TransferRelay { + struct Message { + address from; + address to; + uint256 validAfter; + IERC20 token; + uint256 amount; + uint256 nonce; + } + + mapping (address => mapping (uint256 => bool)) public isSignerNonceConsumed; + + function executeTransferMessage( + Message calldata mess, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + require(mess.from != address(0), 'bad from'); + require(mess.validAfter < block.timestamp, 'not ready'); + require(!isSignerNonceConsumed[mess.from][mess.nonce], 'already consumed'); + { + bytes32 messHash = keccak256(abi.encode(block.chainid, address(this), mess)); + require(ecrecover(messHash, v, r, s) == mess.from, 'bad signature'); + } + // Mark the message consumed. + isSignerNonceConsumed[mess.from][mess.nonce] = true; + // Perform the transfer. + mess.token.transferFrom(address(mess.from), mess.to, mess.amount); + } +} +``` + +We expect the signer to choose a `nonce` value that is unique across all their messages. Our contract uses this `nonce` value to identify and record the status of the message in the `isSignerNonceConsumed` mapping. Pretty straight-forward and intuitive... but we can do better! + +## Looking At Gas costs + +Let's look at the gas cost associated with this operation. Because every `Message.nonce` maps to a unique storage slot, we write to an **empty** slot each time a message gets consumed. Writing to an empty storage slot costs 20k(\*) gas. This can represent 15% of the total gas cost for a simple AMM swap. For high frequency defi operations, the costs can add up. In contrast, writing to a non-empty storage slot only costs 3k(\*) gas. Bitmap nonces minimize how often we write to empty slots, cuting down the cost down by 85% for 99% of operations. + +*(\*) Not accounting for EIP-2929 cold/warm state access costs.* + +## One More Time, With Bitmap Nonces + +If we think about it, we don't need a whole 32-byte word, or even a whole 8-bit boolean to represent whether a message was consumed; we only need one bit (`0` or `1`). Therefore, if we wanted to minimize the frequency of writes to empty slots, instead of mapping nonces to entire storage slots, we could map nonces to bit positions within storage slots. Each storage slot is a 32-byte word so we have 256 bits to work with before we have to move on to a different slot. + +![nonces slot usage](./???.png) + +We accomplish this by mapping the upper 248 bits of the `nonce` to a unique slot (similar to before), then mapping the lower 8 bits to a bit inside that slot. If the user assigns nonces to operations incrementally instead of randomly they will only write to a new slot every 255 operations! + +Let's apply bitmap nonces to our contract: + +```solidity +contract TransferRelay { + // ... + + mapping (address => mapping (uint248 => uint256)) public signerNonceBitmap; + + function executeTransferMessage( + Message calldata mess, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + require(mess.from != address(0), 'bad from'); + require(mess.validAfter < block.timestamp, 'not ready'); + require(!_getSignerNonceState(mess.from, mess.nonce), 'already consumed'); + { + bytes32 messHash = keccak256(abi.encode(block.chainid, address(this), mess)); + require(ecrecover(messHash, v, r, s) == mess.from, 'bad signature'); + } + // Mark the message consumed. + _setSignerNonce(mess.from, mess.nonce); + // Perform the transfer. + mess.token.transferFrom(address(mess.from), mess.to, mess.amount); + } + + function _getSignerNonceState(address signer, uint256 nonce) private view returns (bool) { + uint256 bitmap = signerNonceBitmap[signer][uint248(nonce >> 8)]; + return bitmap & (1 << (nonce & 0xFF)) != 0; + } + + function _setSignerNonce(address signer, uint256 nonce) private { + signerNonceBitmap[signer][uint248(nonce >> 8)] |= 1 << (nonce & 0xFF); + } +} +``` + +## In the Wild + +You can find bitmap nonces being used in major protocols such as Uniswap's [Permit2](https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/SignatureTransfer.sol#L142) and 0x's [Exchange Proxy](https://github.com/0xProject/protocol/blob/e66307ba319e8c3e2a456767403298b576abc85e/contracts/zero-ex/contracts/src/features/nft_orders/ERC721OrdersFeature.sol#L662). + +## The Demo + +The full, working example can be found [here](./TransferRelay.sol) with complete tests detailing its usage [here](../../test/TransferRelay.sol). \ No newline at end of file diff --git a/patterns/bitmap-nonces/TransferRelay.sol b/patterns/bitmap-nonces/TransferRelay.sol new file mode 100644 index 0000000..7d9ea29 --- /dev/null +++ b/patterns/bitmap-nonces/TransferRelay.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +// Minimal ERC20 interface. +interface IERC20 { + function transferFrom(address from, address to, uint256 amount) external returns (bool); +} + +// Allows anyone to execute an ERC20 token transfer on someone else's behalf. +// Payers grant an allowance to this contract on every ERC20 they wish to send. +// Then they sign an off-chain message (hash of the `Message` struct) indicating +// recipient, amount, and time. Afterwards, anyone can submit the message to this +// contract's `executeTransferMessage()` function which consumes the message and +// executes the transfer. Messages are marked consumed using bitmap nonces rather +// than traditional, dedicated nonce slots. +contract TransferRelay { + struct Message { + address from; + address to; + uint256 validAfter; + IERC20 token; + uint256 amount; + uint256 nonce; + } + + mapping (address => mapping (uint248 => uint256)) public signerNonceBitmap; + + // Consume a signed transfer message and transfer tokens specified within (if valid). + function executeTransferMessage( + Message calldata mess, + uint8 v, + bytes32 r, + bytes32 s + ) + external + { + require(mess.from != address(0), 'bad from'); + require(mess.validAfter < block.timestamp, 'not ready'); + require(!_getSignerNonceState(mess.from, mess.nonce), 'already consumed'); + { + bytes32 messHash = keccak256(abi.encode(block.chainid, address(this), mess)); + require(ecrecover(messHash, v, r, s) == mess.from, 'bad signature'); + } + // Mark the message consumed. + _setSignerNonce(mess.from, mess.nonce); + // Perform the transfer. + mess.token.transferFrom(address(mess.from), mess.to, mess.amount); + } + + function _getSignerNonceState(address signer, uint256 nonce) private view returns (bool) { + uint256 bitmap = signerNonceBitmap[signer][uint248(nonce >> 8)]; + return bitmap & (1 << (nonce & 0xFF)) != 0; + } + + function _setSignerNonce(address signer, uint256 nonce) private { + signerNonceBitmap[signer][uint248(nonce >> 8)] |= 1 << (nonce & 0xFF); + } +} \ No newline at end of file diff --git a/test/TransferRelay.t.sol b/test/TransferRelay.t.sol new file mode 100644 index 0000000..7676404 --- /dev/null +++ b/test/TransferRelay.t.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import "./TestUtils.sol"; +import "../patterns/bitmap-nonces/TransferRelay.sol"; +import "solmate/tokens/ERC20.sol"; + +contract TestERC20 is ERC20("TEST", "TEST", 18) { + function mint(address to, uint256 amount) external { + _mint(to, amount); + } +} + +contract TransferRelayTest is TestUtils { + TransferRelay relay = new TransferRelay(); + TestERC20 token = new TestERC20(); + address receiver; + address signer; + uint256 signerKey; + + function setUp() external { + receiver = makeAddr('receiver'); + (signer, signerKey) = makeAddrAndKey('sender'); + token.mint(signer, 1e6); + vm.prank(signer); + token.approve(address(relay), type(uint256).max); + } + + function test_canExecuteTransfer() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: signer, + to: receiver, + validAfter: block.timestamp - 1, + token: IERC20(address(token)), + amount: 0.25e6, + nonce: _randomUint256() + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _hashMessage(mess)); + relay.executeTransferMessage(mess, v, r, s); + assertEq(token.balanceOf(receiver), 0.25e6); + assertEq(token.balanceOf(signer), 0.75e6); + } + + function test_cannotExecuteTransferFromZero() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: address(0), + to: receiver, + validAfter: block.timestamp - 1, + token: IERC20(address(token)), + amount: 0.25e6, + nonce: _randomUint256() + }); + vm.expectRevert('bad from'); + relay.executeTransferMessage(mess, 0, 0, 0); + } + + function test_cannotExecuteTransferTooEarly() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: signer, + to: receiver, + validAfter: block.timestamp, + token: IERC20(address(token)), + amount: 0.25e6, + nonce: _randomUint256() + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _hashMessage(mess)); + vm.expectRevert('not ready'); + relay.executeTransferMessage(mess, v, r, s); + } + + function test_cannotExecuteTransferWithBadSignature() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: signer, + to: receiver, + validAfter: block.timestamp - 1, + token: IERC20(address(token)), + amount: 0.25e6, + nonce: _randomUint256() + }); + (, uint256 notSignerKey) = makeAddrAndKey('not signer'); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(notSignerKey, _hashMessage(mess)); + vm.expectRevert('bad signature'); + relay.executeTransferMessage(mess, v, r, s); + } + + // This checks that the bitmap nonce works. + function test_cannotExecuteTransferTwice() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: signer, + to: receiver, + validAfter: block.timestamp - 1, + token: IERC20(address(token)), + amount: 0.25e6, + nonce: _randomUint256() + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _hashMessage(mess)); + relay.executeTransferMessage(mess, v, r, s); + vm.expectRevert('already consumed'); + relay.executeTransferMessage(mess, v, r, s); + } + + // This checks that the bitmap nonce writes to the same storage slot. + function test_reusesNonceSlot() external { + TransferRelay.Message memory mess = TransferRelay.Message({ + from: signer, + to: receiver, + validAfter: block.timestamp - 1, + token: IERC20(address(token)), + amount: 0.1e6, + nonce: _randomUint256() & ~uint256(0xFF) + }); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _hashMessage(mess)); + // Use gas usage to determine if the same slot was written to or not. + uint256 gasUsed = gasleft(); + relay.executeTransferMessage(mess, v, r, s); + gasUsed -= gasleft(); + // Writing to an empty slot costs at least 20k. + assertGt(gasUsed, 20e3); + mess.nonce += 1; + (v, r, s) = vm.sign(signerKey, _hashMessage(mess)); + gasUsed = gasleft(); + relay.executeTransferMessage(mess, v, r, s); + gasUsed -= gasleft(); + // Writing to a non-empty slot costs less than 20k. + assertLt(gasUsed, 20e3); + } + + function _hashMessage(TransferRelay.Message memory mess) + private + view + returns (bytes32 h) + { + return keccak256(abi.encode(block.chainid, address(relay), mess)); + } +} \ No newline at end of file From ff534400b63a2d60714391fde20f70bf00a01339 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Fri, 17 Nov 2023 14:52:14 -0500 Subject: [PATCH 2/4] update bitmap nonces readme --- patterns/bitmap-nonces/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patterns/bitmap-nonces/README.md b/patterns/bitmap-nonces/README.md index a54ade0..861e70c 100644 --- a/patterns/bitmap-nonces/README.md +++ b/patterns/bitmap-nonces/README.md @@ -107,4 +107,4 @@ You can find bitmap nonces being used in major protocols such as Uniswap's [Perm ## The Demo -The full, working example can be found [here](./TransferRelay.sol) with complete tests detailing its usage [here](../../test/TransferRelay.sol). \ No newline at end of file +The full, working example can be found [here](./TransferRelay.sol) with complete tests detailing its usage and gas savings [here](../../test/TransferRelay.sol). \ No newline at end of file From 5e07321bd34f50c79cc0bf1f5b27f30af2a429c8 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 18 Nov 2023 23:01:25 -0500 Subject: [PATCH 3/4] finish bitmap nonces --- patterns/bitmap-nonces/README.md | 20 +- .../bitmap-nonces/nonces-slots.drawio.svg | 1132 +++++++++++++++++ 2 files changed, 1142 insertions(+), 10 deletions(-) create mode 100644 patterns/bitmap-nonces/nonces-slots.drawio.svg diff --git a/patterns/bitmap-nonces/README.md b/patterns/bitmap-nonces/README.md index 861e70c..7584ceb 100644 --- a/patterns/bitmap-nonces/README.md +++ b/patterns/bitmap-nonces/README.md @@ -3,11 +3,11 @@ - [📜 Example Code](./TransferRelay.sol) - [🐞 Tests](../../test/TransferRelay.t.sol) -What do filling a stop-loss order, executing a governance proposal, or meta transactions have in common? They're all operations meant to be consumed once and only once. This guarantee needs to be enforced on-chain to prevent replay attacks. To do this, many protocols will derive some unique identifier from the operation's parameters, then map that identifier to a storage slot dedicated to that operation which holds a status flag. +What do filling a stop-loss order, executing a governance proposal, or meta transactions have in common? They're all operations meant to be consumed once and only once. You'll find these kinds of operations across many major protocols. This single-use guarantee needs to be enforced on-chain to prevent replay attacks. To do this, many protocols will derive some unique identifier (nonce) for the operation then map that identifier to a storage slot dedicated to that operation which holds a status flag indicating whether its been consumed or not. -## Naive Approach +## The Naive Approach -Take the following example of a protocol that executes off-chain signed messages to transfer (compliant) ERC20 tokens on behalf of the signer after a given time: +Take the following example of a contract that executes off-chain signed messages to transfer (compliant) ERC20 tokens on behalf of the signer after a given time: ```solidity contract TransferRelay { @@ -45,21 +45,21 @@ contract TransferRelay { } ``` -We expect the signer to choose a `nonce` value that is unique across all their messages. Our contract uses this `nonce` value to identify and record the status of the message in the `isSignerNonceConsumed` mapping. Pretty straight-forward and intuitive... but we can do better! +We expect the signer to choose a `nonce` value that is unique across all their messages. Our contract uses this `nonce` value to uniquely identify the message and record its status in the `isSignerNonceConsumed` mapping. Pretty straight-forward and intuitive... but we can do better! -## Looking At Gas costs +## Examining Gas costs -Let's look at the gas cost associated with this operation. Because every `Message.nonce` maps to a unique storage slot, we write to an **empty** slot each time a message gets consumed. Writing to an empty storage slot costs 20k(\*) gas. This can represent 15% of the total gas cost for a simple AMM swap. For high frequency defi operations, the costs can add up. In contrast, writing to a non-empty storage slot only costs 3k(\*) gas. Bitmap nonces minimize how often we write to empty slots, cuting down the cost down by 85% for 99% of operations. +Let's look at the gas cost associated with marking a message consumed. Because every `Message.nonce` maps to a unique storage slot, we will write to an **empty** slot each time a message gets consumed. Writing to an empty storage slot costs 20k(\*) gas. For context, this can represent 15% of the total gas cost for a simple AMM swap. Especially for high frequency defi operations, the costs can add up. In contrast, writing to a *non-empty* storage slot only costs 3k(\*) gas. Bitmap nonces can minimize how often we write to empty slots, cutting down this cost down by 85% for most operations. *(\*) Not accounting for EIP-2929 cold/warm state access costs.* ## One More Time, With Bitmap Nonces -If we think about it, we don't need a whole 32-byte word, or even a whole 8-bit boolean to represent whether a message was consumed; we only need one bit (`0` or `1`). Therefore, if we wanted to minimize the frequency of writes to empty slots, instead of mapping nonces to entire storage slots, we could map nonces to bit positions within storage slots. Each storage slot is a 32-byte word so we have 256 bits to work with before we have to move on to a different slot. +If we think about it, we don't need a whole 32-byte word, or even a whole 8-bit boolean to represent whether a message was consumed. We only need one bit (`0` or `1`). So if we wanted to minimize the frequency of writes to empty slots, instead of mapping nonces to an *entire* storage slot, we could map nonces to bit positions within a storage slot. Each storage slot in the EVM is a 32-byte word so we can fit the status of 256 operations inside a single storage slot before we have to move on to the next. -![nonces slot usage](./???.png) +![nonces slot usage](./nonces-slots.drawio.svg) -We accomplish this by mapping the upper 248 bits of the `nonce` to a unique slot (similar to before), then mapping the lower 8 bits to a bit inside that slot. If the user assigns nonces to operations incrementally instead of randomly they will only write to a new slot every 255 operations! +The addressing is done by mapping the upper 248 bits of the `nonce` to a unique slot (similar to before), then map the lower 8 bits to a bit offset inside that slot. If the user assigns nonces to operations incrementally (1, 2, 3, ...) instead of randomly then they will only write to a new slot every 255 operations. Let's apply bitmap nonces to our contract: @@ -107,4 +107,4 @@ You can find bitmap nonces being used in major protocols such as Uniswap's [Perm ## The Demo -The full, working example can be found [here](./TransferRelay.sol) with complete tests detailing its usage and gas savings [here](../../test/TransferRelay.sol). \ No newline at end of file +The full, working example can be found [here](./TransferRelay.sol) with complete tests demonstrating its usage and gas savings [here](../../test/TransferRelay.sol). \ No newline at end of file diff --git a/patterns/bitmap-nonces/nonces-slots.drawio.svg b/patterns/bitmap-nonces/nonces-slots.drawio.svg new file mode 100644 index 0000000..91829ed --- /dev/null +++ b/patterns/bitmap-nonces/nonces-slots.drawio.svg @@ -0,0 +1,1132 @@ + + + + + + + + + +
+
+
+ nonce: 0 +
+
+
+
+ + nonce: 0 + +
+
+ + + + + + +
+
+
+ nonce: 2 +
+
+
+
+ + nonce: 2 + +
+
+ + + + + + +
+
+
+ nonce: 256 +
+
+
+
+ + nonce: 256 + +
+
+ + + + +
+
+
+ slot + + s + + 0 + + +
+
+
+
+ + slot s0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ slot + + s + + 2 + + +
+
+
+
+ + slot s2 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ = true +
+
+
+
+ + = true + +
+
+ + + + +
+
+
+ = true +
+
+
+
+ + = true + +
+
+ + + + +
+
+
+ slot + + s + + 256 + + +
+
+
+
+ + slot s256 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ = true +
+
+
+
+ + = true + +
+
+ + + + + + +
+
+
+ nonce: 0 +
+
+
+
+ + nonce: 0 + +
+
+ + + + + + +
+
+
+ nonce: 2 +
+
+
+
+ + nonce: 2 + +
+
+ + + + + + +
+
+
+ nonce: 256 +
+
+
+
+ + nonce: 256 + +
+
+ + + + +
+
+
+ slot + + s + + 0 + + +
+
+
+
+ + slot s0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ slot + + s + + + 1 + + + +
+
+
+
+ + slot s1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ ... +
+
+
+
+ + ... + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 0 +
+
+
+
+ + 0 + +
+
+ + + + +
+
+
+ = 1 +
+
+
+
+ + = 1 + +
+
+ + + + +
+
+
+ = 5 +
+
+
+
+ + = 5 + +
+
+ + + + +
+
+
+

+ Vanilla nonces +

+
+
+
+
+ + Vanilla nonces + +
+
+ + + + +
+
+
+

+ Bitmap Nonces +

+
+
+
+
+ + Bitmap Nonces + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file From bda49ab31cf0fd874358bb2c33ea620b50a1e356 Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 18 Nov 2023 23:19:01 -0500 Subject: [PATCH 4/4] update root readme. update bitmap nonces readme --- README.md | 4 +++- patterns/bitmap-nonces/README.md | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8f4ac87..4765a95 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ This repo is an ongoing collection of useful, and occasionally clever, solidity/ - Contracts with upgradeable logic. - [Big Data Storage (SSTORE2)](./patterns/big-data-storage) - Cost efficient on-chain storage of multi-word data accessible to contracts. +- [Bitmap Nonces](./patterns/bitmap-nonces/) + - Efficiently tracking the state of many operations identifiable by a unique nonce. - [Commit + Reveal](./patterns/commit-reveal) - A two-step process for performing partially obscured on-chain actions that can't be front or back runned. - [EIP712 Signed Messages](./patterns/eip712-signed-messages) @@ -52,7 +54,7 @@ This repo is an ongoing collection of useful, and occasionally clever, solidity/ - [Read-Only Delegatecall](./patterns/readonly-delegatecall) - Execute arbitrary delegatecalls in your contract in a read-only manner, without side-effects. - [Reentrancy](./patterns/reentrancy) - - Explaining reentrancy vulnerabilities and patterns for addressing them. + - Explaining reentrancy vulnerabilities and patterns for addressing them (Checks-Effects-Interactions and reentrancy guards). - [Separate Allowance Targets](./patterns/separate-allowance-targets/) - Avoid having to migrate user allowances between upgrades with a dedicated approval contract. - [Stack-Too-Deep Workarounds](./patterns/stack-too-deep/) diff --git a/patterns/bitmap-nonces/README.md b/patterns/bitmap-nonces/README.md index 7584ceb..db18150 100644 --- a/patterns/bitmap-nonces/README.md +++ b/patterns/bitmap-nonces/README.md @@ -101,10 +101,8 @@ contract TransferRelay { } ``` -## In the Wild +## Final Thoughts -You can find bitmap nonces being used in major protocols such as Uniswap's [Permit2](https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/SignatureTransfer.sol#L142) and 0x's [Exchange Proxy](https://github.com/0xProject/protocol/blob/e66307ba319e8c3e2a456767403298b576abc85e/contracts/zero-ex/contracts/src/features/nft_orders/ERC721OrdersFeature.sol#L662). - -## The Demo - -The full, working example can be found [here](./TransferRelay.sol) with complete tests demonstrating its usage and gas savings [here](../../test/TransferRelay.sol). \ No newline at end of file +- You can find bitmap nonces being used in major protocols such as Uniswap's [Permit2](https://github.com/Uniswap/permit2/blob/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219/src/SignatureTransfer.sol#L142) and 0x's [Exchange Proxy](https://github.com/0xProject/protocol/blob/e66307ba319e8c3e2a456767403298b576abc85e/contracts/zero-ex/contracts/src/features/nft_orders/ERC721OrdersFeature.sol#L662). +- There is no reason you couldn't track operations that have more than 2 states using bitmap nonces. You would just simply increase the number of bits in a word assigned to each operation and adjust the mapping formula accordingly. +- The full, working example can be found [here](./TransferRelay.sol) with complete tests demonstrating its usage and gas savings [here](../../test/TransferRelay.sol). \ No newline at end of file