Skip to content

Commit

Permalink
feat(evm): allow SignerValidator to track up to 8 incentives per Boost
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
topocount committed Sep 3, 2024
1 parent ef7633a commit b1362d2
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 8 deletions.
9 changes: 9 additions & 0 deletions packages/evm/contracts/shared/BoostError.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
57 changes: 52 additions & 5 deletions packages/evm/contracts/validators/SignerValidator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
34 changes: 31 additions & 3 deletions packages/evm/test/validators/SignerValidator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand Down Expand Up @@ -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);
}
}

0 comments on commit b1362d2

Please sign in to comment.