diff --git a/contracts/modules/AddressAliasRegistry.sol b/contracts/modules/AddressAliasRegistry.sol new file mode 100644 index 00000000..5c504ac2 --- /dev/null +++ b/contracts/modules/AddressAliasRegistry.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { IAddressAliasRegistry } from "@modules/interfaces/IAddressAliasRegistry.sol"; +import { LibZip } from "solady/utils/LibZip.sol"; + +/** + * @title AddressAliasRegistry + * @dev A registry for registering addresses with aliases. + */ +contract AddressAliasRegistry is IAddressAliasRegistry { + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev The current number of aliases. + */ + uint32 public numAliases; + + /** + * @dev Maps an alias to its original address. + */ + mapping(address => address) internal _aliasToAddress; + + /** + * @dev Maps an address to its alias. + */ + mapping(address => address) internal _addressToAlias; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IAddressAliasRegistry + */ + function resolveAndRegister(address[] memory addressesOrAliases) + public + returns (address[] memory addresses, address[] memory aliases) + { + unchecked { + uint256 n = addressesOrAliases.length; + addresses = addressesOrAliases; + aliases = new address[](n); + for (uint256 i; i != n; ++i) { + (addresses[i], aliases[i]) = _resolveAndRegister(addressesOrAliases[i]); + } + } + } + + // Misc functions: + // --------------- + + /** + * @dev For calldata compression. + */ + fallback() external payable { + LibZip.cdFallback(); + } + + /** + * @dev For calldata compression. + */ + receive() external payable { + LibZip.cdFallback(); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IAddressAliasRegistry + */ + function resolve(address[] memory addressesOrAliases) + public + view + returns (address[] memory addresses, address[] memory aliases) + { + unchecked { + uint256 n = addressesOrAliases.length; + addresses = addressesOrAliases; + aliases = new address[](n); + for (uint256 i; i != n; ++i) { + (addresses[i], aliases[i]) = _resolve(addressesOrAliases[i]); + } + } + } + + /** + * @inheritdoc IAddressAliasRegistry + */ + function addressOf(address addressOrAlias) public view returns (address) { + // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. + return uint160(addressOrAlias) <= type(uint32).max ? _aliasToAddress[addressOrAlias] : addressOrAlias; + } + + /** + * @inheritdoc IAddressAliasRegistry + */ + function aliasOf(address addressOrAlias) public view returns (address) { + return _addressToAlias[addressOf(addressOrAlias)]; + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Returns the alias and address for `addressOrAlias`. + * If the `addressOrAlias` is less than `2**31 - 1`, it is treated as an alias. + * Otherwise, it is treated as an address, and it's alias will be registered on-the-fly. + * @param addressOrAlias The alias or address. + * @return address_ The address. + * @return alias_ The alias. + */ + function _resolveAndRegister(address addressOrAlias) internal returns (address address_, address alias_) { + // If the `addressOrAlias` is less than or equal to `2**32 - 1`, we will consider it an alias. + if (uint160(addressOrAlias) <= type(uint32).max) { + alias_ = addressOrAlias; + address_ = _aliasToAddress[alias_]; + if (address_ == address(0)) revert AliasNotFound(); + } else { + address_ = addressOrAlias; + alias_ = _registerAlias(address_); + } + } + + /** + * @dev Returns the alias and address for `addressOrAlias`. + * If the `addressOrAlias` is less than `2**31 - 1`, it is treated as an alias. + * Otherwise, it is treated as an address. + * @param addressOrAlias The alias or address. + * @return address_ The address. + * @return alias_ The alias. + */ + function _resolve(address addressOrAlias) internal view returns (address address_, address alias_) { + // If the `addressOrAlias` is less than or equal to `2**32 - 1`, we will consider it an alias. + if (uint160(addressOrAlias) <= type(uint32).max) { + alias_ = addressOrAlias; + address_ = _aliasToAddress[alias_]; + } else { + address_ = addressOrAlias; + alias_ = _addressToAlias[address_]; + } + } + + /** + * @dev Registers the alias for the address on-the-fly. + * @param address_ The address. + * @return alias_ The alias registered for the address. + */ + function _registerAlias(address address_) internal returns (address alias_) { + if (uint160(address_) <= type(uint32).max) revert AddressTooSmall(); + + alias_ = _addressToAlias[address_]; + // If the address has no alias, register it's alias. + if (alias_ == address(0)) { + // Increment the `numAliases` and cast it into an alias. + alias_ = address(uint160(++numAliases)); + // Add to the mappings. + _aliasToAddress[alias_] = address_; + _addressToAlias[address_] = alias_; + emit RegisteredAlias(address_, alias_); + } + } +} diff --git a/contracts/modules/PlatformAirdropper.sol b/contracts/modules/PlatformAirdropper.sol new file mode 100644 index 00000000..687fc91e --- /dev/null +++ b/contracts/modules/PlatformAirdropper.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ISuperMinterV2 } from "@modules/interfaces/ISuperMinterV2.sol"; +import { IPlatformAirdropper } from "@modules/interfaces/IPlatformAirdropper.sol"; +import { IAddressAliasRegistry } from "@modules/interfaces/IAddressAliasRegistry.sol"; +import { LibZip } from "solady/utils/LibZip.sol"; + +/** + * @title PlatformAirdropper + * @dev The `PlatformAirdropper` utility class to batch airdrop tokens. + */ +contract PlatformAirdropper is IPlatformAirdropper { + // ============================================================= + // IMMUTABLES + // ============================================================= + + /** + * @dev The address alias registry. + */ + address public immutable addressAliasRegistry; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor(address addressAliasRegistry_) payable { + addressAliasRegistry = addressAliasRegistry_; + } + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc IPlatformAirdropper + */ + function platformAirdrop(address superMinter, ISuperMinterV2.PlatformAirdrop memory p) + public + returns (uint256 fromTokenId, address[] memory aliases) + { + unchecked { + (p.to, aliases) = IAddressAliasRegistry(addressAliasRegistry).resolveAndRegister(p.to); + fromTokenId = ISuperMinterV2(superMinter).platformAirdrop(p); + } + } + + /** + * @inheritdoc IPlatformAirdropper + */ + function platformAirdropMulti(address superMinter, ISuperMinterV2.PlatformAirdrop[] memory p) + public + returns (uint256[] memory fromTokenIds, address[][] memory aliases) + { + unchecked { + uint256 n = p.length; + fromTokenIds = new uint256[](n); + aliases = new address[][](n); + for (uint256 i; i != n; ++i) { + (fromTokenIds[i], aliases[i]) = platformAirdrop(superMinter, p[i]); + } + } + } + + // Misc functions: + // --------------- + + /** + * @dev For calldata compression. + */ + fallback() external payable { + LibZip.cdFallback(); + } + + /** + * @dev For calldata compression. + */ + receive() external payable { + LibZip.cdFallback(); + } +} diff --git a/contracts/modules/interfaces/IAddressAliasRegistry.sol b/contracts/modules/interfaces/IAddressAliasRegistry.sol new file mode 100644 index 00000000..02b46486 --- /dev/null +++ b/contracts/modules/interfaces/IAddressAliasRegistry.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +/** + * @title AddressAliasRegistry + * @dev A registry for registering addresses with aliases. + */ +interface IAddressAliasRegistry { + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when an address is registered with an alias. + */ + event RegisteredAlias(address address_, address alias_); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The alias has not been registered. + */ + error AliasNotFound(); + + /** + * @dev The address to be registered must be larger than `2**32 - 1`. + */ + error AddressTooSmall(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Resolve the addresses or aliases. + * If an address does not have an aliases, an alias will be registered for it. + * @param addressesOrAliases An array of addresses, which can be aliases. + * @return addresses The resolved addresses. + * @return aliases The aliases for the addresses. + */ + function resolveAndRegister(address[] memory addressesOrAliases) + external + returns (address[] memory addresses, address[] memory aliases); + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the current number of aliases. + * @return The latest value. + */ + function numAliases() external view returns (uint32); + + /** + * @dev Resolve the addresses or aliases. + * If an address does not have an alias, it's corresponding returned alias will be zero. + * If an alias does not have an address, it's corresponding returned address will be zero. + * @param addressesOrAliases An array of addresses, which can be aliases. + * @return addresses The resolved addresses. + * @return aliases The aliases for the addresses. + */ + function resolve(address[] memory addressesOrAliases) + external + view + returns (address[] memory addresses, address[] memory aliases); + + /** + * @dev Resolve the address or alias. + * @param addressesOrAliases An address or alias. + * @return The resolved address. + */ + function addressOf(address addressesOrAliases) external view returns (address); + + /** + * @dev Resolve the address or alias. + * @param addressesOrAliases An address or alias. + * @return The resolved alias. + */ + function aliasOf(address addressesOrAliases) external view returns (address); +} diff --git a/contracts/modules/interfaces/IPlatformAirdropper.sol b/contracts/modules/interfaces/IPlatformAirdropper.sol new file mode 100644 index 00000000..b1a06888 --- /dev/null +++ b/contracts/modules/interfaces/IPlatformAirdropper.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ISuperMinterV2 } from "./ISuperMinterV2.sol"; + +/** + * @title PlatformAirdropper + * @dev The `PlatformAirdropper` utility class to batch airdrop tokens. + */ +interface IPlatformAirdropper { + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Performs a platform airdrop. + * To save on calldata costs, you can optionally replace each address entry in `p.to` with its alias. + * Aliases are registered on-the-fly when a new address is seen. + * @param superMinter The superminter which has a `platformAirdrop` function. + * @param p The platform airdrop parameters. + * @return fromTokenId The first token ID minted. + * @return aliases The aliases of `p.to`. + */ + function platformAirdrop(address superMinter, ISuperMinterV2.PlatformAirdrop memory p) + external + returns (uint256 fromTokenId, address[] memory aliases); + + /** + * @dev Performs a platform airdrop. + * To save on calldata costs, you can optionally replace each address entry in `p.to` with its alias. + * Aliases are registered on-the-fly when a new address is seen. + * @param superMinter The superminter which has a `platformAirdrop` function. + * @param p The platform airdrop parameters. + * @return fromTokenIds The first token IDs minted. + * @return aliases The aliases of each `p.to`. + */ + function platformAirdropMulti(address superMinter, ISuperMinterV2.PlatformAirdrop[] memory p) + external + returns (uint256[] memory fromTokenIds, address[][] memory aliases); + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the address alias registry. + * @return The immutable value. + */ + function addressAliasRegistry() external view returns (address); +} diff --git a/lib/solady b/lib/solady index cde0a5fb..9de1fe26 160000 --- a/lib/solady +++ b/lib/solady @@ -1 +1 @@ -Subproject commit cde0a5fb594da8655ba6bfcdc2e40a7c870c0cc0 +Subproject commit 9de1fe26af4f4b1bbb4b5efcedc503342fc55ee8 diff --git a/tests/modules/AddressAliasRegistry.t.sol b/tests/modules/AddressAliasRegistry.t.sol new file mode 100644 index 00000000..09ed394f --- /dev/null +++ b/tests/modules/AddressAliasRegistry.t.sol @@ -0,0 +1,81 @@ +pragma solidity ^0.8.16; + +import { IAddressAliasRegistry, AddressAliasRegistry } from "@modules/AddressAliasRegistry.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import "../TestConfigV2_1.sol"; + +contract AddressAliasRegistryTests is TestConfigV2_1 { + AddressAliasRegistry aar; + + function setUp() public virtual override { + super.setUp(); + aar = new AddressAliasRegistry(); + } + + struct Resolved { + address[] aliases; + address[] addresses; + } + + function test_registerAliases(uint256) public { + Resolved memory r0; + Resolved memory r1; + address[] memory addresses = _randomNonZeroAddressesGreaterThan(); + if (_random() % 32 == 0) { + (r0.addresses, r0.aliases) = aar.resolve(new address[](addresses.length)); + assertEq(r0.aliases, new address[](addresses.length)); + assertEq(r0.addresses, new address[](addresses.length)); + address a = addresses.length > 0 ? addresses[0] : address(0); + assertEq(aar.addressOf(a), a); + assertEq(uint160(aar.aliasOf(a)), 0); + } + (r0.addresses, r0.aliases) = aar.resolveAndRegister(addresses); + (r1.addresses, r1.aliases) = aar.resolve(addresses); + if (addresses.length != 0) { + assertEq(uint160(r0.aliases[0]), 1); + assertEq(uint160(r1.aliases[0]), 1); + address a = addresses[0]; + assertEq(aar.addressOf(a), a); + assertEq(aar.addressOf(aar.aliasOf(a)), a); + assertEq(uint160(aar.aliasOf(a)), 1); + } + assertEq(r1.aliases, r0.aliases); + assertEq(r1.addresses, r0.addresses); + (r1.addresses, r1.aliases) = aar.resolve(r0.aliases); + assertEq(r1.aliases, r0.aliases); + assertEq(r1.addresses, r0.addresses); + uint256 n = _uniquified(addresses).length; + assertEq(n, _uniquified(r0.aliases).length); + assertEq(n, _uniquified(r0.addresses).length); + } + + function _uniquified(address[] memory a) internal pure returns (address[] memory) { + LibSort.sort(a); + LibSort.uniquifySorted(a); + return a; + } + + function _randomNonZeroAddressesGreaterThan() internal returns (address[] memory a) { + a = _randomNonZeroAddressesGreaterThan(0xffffffff); + } + + function _randomNonZeroAddressesGreaterThan(uint256 t) internal returns (address[] memory a) { + uint256 n = _random() % 4; + if (_random() % 32 == 0) { + n = _random() % 32; + } + a = new address[](n); + require(t != 0, "t must not be zero"); + unchecked { + for (uint256 i; i != n; ++i) { + uint256 r; + if (_random() & 1 == 0) { + while (r <= t) r = uint256(uint160(_random())); + } else { + r = type(uint256).max ^ _bound(_random(), 1, 8); + } + a[i] = address(uint160(r)); + } + } + } +} diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol new file mode 100644 index 00000000..ba89e85c --- /dev/null +++ b/tests/modules/PlatformAirdropper.t.sol @@ -0,0 +1,252 @@ +pragma solidity ^0.8.16; + +import { IERC721AUpgradeable, ISoundEditionV2_1, SoundEditionV2_1 } from "@core/SoundEditionV2_1.sol"; +import { ISuperMinterV2, SuperMinterV2 } from "@modules/SuperMinterV2.sol"; +import { IPlatformAirdropper, PlatformAirdropper } from "@modules/PlatformAirdropper.sol"; +import { IAddressAliasRegistry, AddressAliasRegistry } from "@modules/AddressAliasRegistry.sol"; +import { LibOps } from "@core/utils/LibOps.sol"; +import { Ownable } from "solady/auth/Ownable.sol"; +import { LibZip } from "solady/utils/LibZip.sol"; +import { SafeCastLib } from "solady/utils/SafeCastLib.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; +import "../TestConfigV2_1.sol"; + +contract PlatformAirdropperTests is TestConfigV2_1 { + SuperMinterV2 sm; + SoundEditionV2_1 edition; + PlatformAirdropper pa; + AddressAliasRegistry aar; + + mapping(uint256 => mapping(address => uint256)) internal _expectedMintCounts; + + function setUp() public virtual override { + super.setUp(); + ISoundEditionV2_1.EditionInitialization memory init = genericEditionInitialization(); + init.tierCreations = new ISoundEditionV2_1.TierCreation[](2); + init.tierCreations[0].tier = 0; + init.tierCreations[1].tier = 1; + init.tierCreations[1].maxMintableLower = type(uint32).max; + init.tierCreations[1].maxMintableUpper = type(uint32).max; + edition = createSoundEdition(init); + sm = new SuperMinterV2(); + edition.grantRoles(address(sm), edition.MINTER_ROLE()); + aar = new AddressAliasRegistry(); + pa = new PlatformAirdropper(address(aar)); + } + + function test_platformAirdrop(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + + ISuperMinterV2.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.startTime = 0; + c.tier = uint8(_random() % 2); + c.endTime = type(uint32).max; + c.maxMintablePerAccount = uint32(_random()); // Doesn't matter, will be auto set to max. + c.mode = sm.PLATFORM_AIRDROP(); + assertEq(sm.createEditionMint(c), 0); + + vm.prank(c.platform); + sm.setPlatformSigner(signer); + + unchecked { + ISuperMinterV2.PlatformAirdrop memory p; + p.edition = address(edition); + p.tier = c.tier; + p.scheduleNum = 0; + while (p.to.length == 0) p.to = _randomNonZeroAddressesGreaterThan(); + p.signedQuantity = uint32(_bound(_random(), 1, 8)); + p.signedClaimTicket = uint32(_bound(_random(), 0, type(uint32).max)); + p.signedDeadline = type(uint32).max; + p.signature = _generatePlatformAirdropSignature(p, privateKey); + + for (uint256 i; i != p.to.length; ++i) { + _expectedMintCounts[0][p.to[i]] += p.signedQuantity; + } + + address[][2] memory aliases; + (, aliases[0]) = pa.platformAirdrop(address(sm), p); + + if (_random() % 8 == 0) { + for (uint256 i; i < p.to.length; ++i) { + uint256 k = _expectedMintCounts[0][p.to[i]]; + assertEq(edition.balanceOf(p.to[i]), k); + assertEq(sm.numberMinted(address(edition), p.tier, p.scheduleNum, p.to[i]), k); + } + } + + p.signedClaimTicket ^= 1; + p.signature = _generatePlatformAirdropSignature(p, privateKey); + // Note that we replace the addresses AFTER signing. + p.to = aliases[0]; + + uint256 numAliases = aar.numAliases(); + (, aliases[1]) = pa.platformAirdrop(address(sm), p); + assertEq(aar.numAliases(), numAliases); + assertEq(aliases[0], aliases[1]); + + (p.to, ) = aar.resolve(p.to); + + if (_random() % 8 == 0) { + for (uint256 i; i < p.to.length; ++i) { + uint256 k = _expectedMintCounts[0][p.to[i]] * 2; + assertEq(edition.balanceOf(p.to[i]), k); + assertEq(sm.numberMinted(address(edition), p.tier, p.scheduleNum, p.to[i]), k); + } + } + + assertEq(_uniquified(p.to).length, numAliases); + } + } + + function test_platformAirdropMulti(uint256) public { + (address signer, uint256 privateKey) = _randomSigner(); + + ISuperMinterV2.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.startTime = 0; + c.tier = uint8(_random() % 2); + c.endTime = type(uint32).max; + c.maxMintablePerAccount = uint32(_random()); // Doesn't matter, will be auto set to max. + c.mode = sm.PLATFORM_AIRDROP(); + assertEq(sm.createEditionMint(c), 0); + + vm.prank(c.platform); + sm.setPlatformSigner(signer); + + unchecked { + ISuperMinterV2.PlatformAirdrop[] memory p = new ISuperMinterV2.PlatformAirdrop[](2); + for (uint256 j; j != 2; ++j) { + p[j].edition = address(edition); + p[j].tier = c.tier; + p[j].scheduleNum = 0; + while (p[j].to.length == 0) p[j].to = _randomNonZeroAddressesGreaterThan(); + p[j].signedQuantity = uint32(_bound(_random(), 1, 8)); + p[j].signedClaimTicket = uint32(j); + p[j].signedDeadline = type(uint32).max; + p[j].signature = _generatePlatformAirdropSignature(p[j], privateKey); + for (uint256 i; i != p[j].to.length; ++i) { + _expectedMintCounts[0][p[j].to[i]] += p[j].signedQuantity; + } + } + + address[][][2] memory aliases; + (, aliases[0]) = pa.platformAirdropMulti(address(sm), p); + + if (_random() % 8 == 0) { + for (uint256 j; j != 2; ++j) { + for (uint256 i; i < p[j].to.length; ++i) { + uint256 k = _expectedMintCounts[0][p[j].to[i]]; + assertEq(edition.balanceOf(p[j].to[i]), k); + assertEq(sm.numberMinted(address(edition), p[j].tier, p[j].scheduleNum, p[j].to[i]), k); + } + } + } + + for (uint256 j; j != 2; ++j) { + p[j].signedClaimTicket = uint32(2 + j); + p[j].signature = _generatePlatformAirdropSignature(p[j], privateKey); + // Note that we replace the addresses AFTER signing. + p[j].to = aliases[0][j]; + } + + (, aliases[1]) = pa.platformAirdropMulti(address(sm), p); + for (uint256 j; j != 2; ++j) { + assertEq(aliases[0][j], aliases[1][j]); + (p[j].to, ) = aar.resolve(p[j].to); + } + + if (_random() % 8 == 0) { + for (uint256 j; j != 2; ++j) { + for (uint256 i; i < p[j].to.length; ++i) { + uint256 k = _expectedMintCounts[0][p[j].to[i]] * 2; + assertEq(edition.balanceOf(p[j].to[i]), k); + assertEq(sm.numberMinted(address(edition), p[j].tier, p[j].scheduleNum, p[j].to[i]), k); + } + } + } + + assertEq(LibSort.union(_uniquified(p[0].to), _uniquified(p[1].to)).length, aar.numAliases()); + } + } + + function test_platformAirdropLimit() public { + (address signer, uint256 privateKey) = _randomSigner(); + + ISuperMinterV2.MintCreation memory c; + c.maxMintable = type(uint32).max; + c.platform = address(this); + c.edition = address(edition); + c.startTime = 0; + c.tier = 0; + c.endTime = type(uint32).max; + c.maxMintablePerAccount = uint32(_random()); // Doesn't matter, will be auto set to max. + c.mode = sm.PLATFORM_AIRDROP(); + assertEq(sm.createEditionMint(c), 0); + + vm.prank(c.platform); + sm.setPlatformSigner(signer); + + uint256 n = 256; + + ISuperMinterV2.PlatformAirdrop memory p; + p.edition = address(edition); + p.tier = c.tier; + p.scheduleNum = 0; + p.to = new address[](n); + unchecked { + for (uint256 i; i != n; ++i) { + p.to[i] = address(uint160(0x123456789abcdef + i)); + } + } + p.signedQuantity = 1; + p.signedClaimTicket = 1; + p.signedDeadline = type(uint32).max; + p.signature = _generatePlatformAirdropSignature(p, privateKey); + + pa.platformAirdrop(address(sm), p); + } + + function _generatePlatformAirdropSignature(ISuperMinterV2.PlatformAirdrop memory p, uint256 privateKey) + internal + returns (bytes memory signature) + { + bytes32 digest = sm.computePlatformAirdropDigest(p); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + signature = abi.encodePacked(r, s, v); + } + + function _uniquified(address[] memory a) internal pure returns (address[] memory) { + LibSort.sort(a); + LibSort.uniquifySorted(a); + return a; + } + + function _randomNonZeroAddressesGreaterThan() internal returns (address[] memory a) { + a = _randomNonZeroAddressesGreaterThan(0xffffffff); + } + + function _randomNonZeroAddressesGreaterThan(uint256 t) internal returns (address[] memory a) { + uint256 n = _random() % 4; + if (_random() % 32 == 0) { + n = _random() % 32; + } + a = new address[](n); + require(t != 0, "t must not be zero"); + unchecked { + for (uint256 i; i != n; ++i) { + uint256 r; + if (_random() & 1 == 0) { + while (r <= t) r = uint256(uint160(_random())); + } else { + r = type(uint256).max ^ _bound(_random(), 1, 8); + } + a[i] = address(uint160(r)); + } + } + } +}