From b1362d2dbc0897ed0f6cd9540ed9a4b89fb4f53a Mon Sep 17 00:00:00 2001 From: Kevin Siegler <17910833+topocount@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:16:08 -0500 Subject: [PATCH] feat(evm): allow SignerValidator to track up to 8 incentives per Boost This bitmap allows a single signature to allow the claiming of up to 8 incentives. We can enable more claims, it's just a question of utilizing more storage in exchange for more possible incentives supported in a given boost. --- packages/evm/contracts/shared/BoostError.sol | 9 +++ .../contracts/validators/SignerValidator.sol | 57 +++++++++++++++++-- .../evm/test/validators/SignerValidator.t.sol | 34 ++++++++++- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/packages/evm/contracts/shared/BoostError.sol b/packages/evm/contracts/shared/BoostError.sol index a67258a12..abba80be3 100644 --- a/packages/evm/contracts/shared/BoostError.sol +++ b/packages/evm/contracts/shared/BoostError.sol @@ -31,4 +31,13 @@ library BoostError { /// @notice Thrown when the requested action is unauthorized error Unauthorized(); + + /// @notice Thrown when an incentive id exceeds the available incentives + error InvalidIncentive(uint8 available, uint256 id); + + /// @notice thrown when an incentiveId is larger than 7 + error IncentiveToBig(uint8 incentiveId); + + /// @notice thrown when an incentiveId is already claimed against + error IncentiveClaimed(uint8 incentiveId); } diff --git a/packages/evm/contracts/validators/SignerValidator.sol b/packages/evm/contracts/validators/SignerValidator.sol index 542945645..1d5c7e7ed 100644 --- a/packages/evm/contracts/validators/SignerValidator.sol +++ b/packages/evm/contracts/validators/SignerValidator.sol @@ -16,9 +16,10 @@ import {ASignerValidator} from "contracts/validators/ASignerValidator.sol"; /// @notice A simple implementation of a Validator that verifies a given signature and checks the recovered address against a set of authorized signers contract SignerValidator is ASignerValidator, Ownable, EIP712 { using SignatureCheckerLib for address; + using IncentiveBits for IncentiveBits.IncentiveMap; - /// @dev The set of used hashes (for replay protection) - mapping(bytes32 => bool) internal _used; + /// @dev track claimed incentives using this bitmap + IncentiveBits.IncentiveMap _used; bytes32 internal constant _SIGNER_VALIDATOR_TYPEHASH = keccak256("SignerValidatorData(uint256 boostId,uint8 incentiveQuantity,address claimant,bytes incentiveData)"); @@ -52,11 +53,15 @@ contract SignerValidator is ASignerValidator, Ownable, EIP712 { abi.decode(claim.validatorData, (SignerValidatorInputParams)); bytes32 hash = hashSignerData(boostId, validatorData.incentiveQuantity, claimant, claim.incentiveData); + + if (uint256(validatorData.incentiveQuantity) <= incentiveId) { + revert BoostError.InvalidIncentive(validatorData.incentiveQuantity, incentiveId); + } if (!signers[validatorData.signer]) revert BoostError.Unauthorized(); - if (_used[hash]) revert BoostError.Replayed(validatorData.signer, hash, validatorData.signature); - // Mark the hash as used to prevent replays - _used[hash] = true; + // Mark the incentive as claimed to prevent replays + // checks internally if the incentive has already been claimed + _used.setOrThrow(hash, incentiveId); // Return the result of the signature check // no need for a sig prefix since it's encoded by the EIP712 lib @@ -95,3 +100,45 @@ contract SignerValidator is ASignerValidator, Ownable, EIP712 { result = true; } } + +library IncentiveBits { + /// @dev The set of used claimed incentives for a given hash (for replay protection) + struct IncentiveMap { + mapping(bytes32 => uint8) map; + } + + /// @notice an internal helper that manages the incentive bitmask + /// @dev this supports a maximum of 8 incentives for a given boost + /// @param bitmap the bitmap struct to operate on + /// @param hash the claim hash used to key on the incentive bitmap + /// @param incentive the incentive id to set in the bitmap + function setOrThrow(IncentiveMap storage bitmap, bytes32 hash, uint256 incentive) internal { + bytes4 invalidSelector = BoostError.IncentiveToBig.selector; + bytes4 claimedSelector = BoostError.IncentiveClaimed.selector; + /// @solidity memory-safe-assembly + assembly { + if gt(incentive, 7) { + // if the incentive is larger the 7 (the highest bit index) + // we revert + mstore(0, invalidSelector) + mstore(4, incentive) + revert(0x00, 0x24) + } + mstore(0x20, bitmap.slot) + mstore(0x00, hash) + let storageSlot := keccak256(0x00, 0x40) + // toggle the value that was stored inline on stack with xor + let updatedStorageValue := xor(sload(storageSlot), shl(incentive, 1)) + // isolate the toggled bit and see if it's been unset back to zero + let alreadySet := xor(1, shr(incentive, updatedStorageValue)) + if alreadySet { + // revert if the stored value was unset + mstore(0, claimedSelector) + mstore(4, incentive) + revert(0x00, 0x24) + } + // otherwise store the newly set value + sstore(storageSlot, updatedStorageValue) + } + } +} diff --git a/packages/evm/test/validators/SignerValidator.t.sol b/packages/evm/test/validators/SignerValidator.t.sol index 3495eb1c4..5a0082119 100644 --- a/packages/evm/test/validators/SignerValidator.t.sol +++ b/packages/evm/test/validators/SignerValidator.t.sol @@ -14,7 +14,7 @@ import {IBoostClaim} from "contracts/shared/IBoostClaim.sol"; import {BoostError} from "contracts/shared/BoostError.sol"; import {Cloneable} from "contracts/shared/Cloneable.sol"; import {AValidator} from "contracts/validators/AValidator.sol"; -import {SignerValidator, ASignerValidator} from "contracts/validators/SignerValidator.sol"; +import {SignerValidator, ASignerValidator, IncentiveBits} from "contracts/validators/SignerValidator.sol"; contract SignerValidatorTest is Test { SignerValidator baseValidator = new SignerValidator(); @@ -131,7 +131,7 @@ contract SignerValidatorTest is Test { function testValidate_ReplayedSignature() public { uint256 boostId = 0; uint256 incentiveId = 0; - uint8 incentiveQuantity = 0; + uint8 incentiveQuantity = 1; address claimant = address(0); bytes memory incentiveData = hex"def456232173821931823712381232131391321934"; bytes32 msgHash = validator.hashSignerData(boostId, incentiveQuantity, claimant, incentiveData); @@ -144,7 +144,7 @@ contract SignerValidatorTest is Test { assertTrue(validator.validate(boostId, incentiveId, claimant, claimData)); // Second (replayed) validation should revert - vm.expectRevert(abi.encodeWithSelector(BoostError.Replayed.selector, testSigner, msgHash, signature)); + vm.expectRevert(abi.encodeWithSelector(BoostError.IncentiveClaimed.selector, incentiveId)); validator.validate(boostId, incentiveId, claimant, claimData); } @@ -241,3 +241,31 @@ contract SignerValidatorTest is Test { return abi.encodePacked(r, s, v); } } + +contract IncentiveBitsTest is Test { + using IncentiveBits for IncentiveBits.IncentiveMap; + + IncentiveBits.IncentiveMap _used; + + bytes32 private fakeHash = hex"123abc"; + + function testIncentiveBitsWorks() public { + for (uint8 x = 0; x < 8; x++) { + _used.setOrThrow(fakeHash, x); + } + uint8 map = _used.map[fakeHash]; + assertEq(type(uint8).max, map); + } + + function testIncentiveBitsBitTooLarge(uint8 badIndex) public { + vm.assume(badIndex > 7); + vm.expectRevert(abi.encodeWithSelector(BoostError.IncentiveToBig.selector, badIndex)); + _used.setOrThrow(fakeHash, badIndex); + } + + function testIncentiveRevertsIfToggledAgain() public { + _used.setOrThrow(fakeHash, 7); + vm.expectRevert(abi.encodeWithSelector(BoostError.IncentiveClaimed.selector, 7)); + _used.setOrThrow(fakeHash, 7); + } +}