From e9a2a7a282a32b2aacb693c8601dd0ffad755205 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 20 Dec 2023 21:33:04 +0000 Subject: [PATCH 01/10] Add PlatformAirdropper --- contracts/modules/PlatformAirdropper.sol | 112 ++++++++++++++++++ .../interfaces/IPlatformAirdropper.sol | 42 +++++++ 2 files changed, 154 insertions(+) create mode 100644 contracts/modules/PlatformAirdropper.sol create mode 100644 contracts/modules/interfaces/IPlatformAirdropper.sol diff --git a/contracts/modules/PlatformAirdropper.sol b/contracts/modules/PlatformAirdropper.sol new file mode 100644 index 00000000..d9c236bc --- /dev/null +++ b/contracts/modules/PlatformAirdropper.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ISuperMinterV2 } from "@modules/interfaces/ISuperMinterV2.sol"; +import { IPlatformAirdropper } from "@modules/interfaces/IPlatformAirdropper.sol"; + +import { LibZip } from "solady/utils/LibZip.sol"; + +/** + * @title PlatformAirdropper + * @dev The `PlatformAirdropper` utility class to batch airdrop tokens. + */ +contract PlatformAirdropper is IPlatformAirdropper { + // ============================================================= + // STORAGE + // ============================================================= + + uint256 internal _numAliases; + + mapping(address => address) internal _aliasToAddress; + + mapping(address => address) internal _addressToAlias; + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + function platformAirdrop(address superMinter, ISuperMinterV2.PlatformAirdrop memory p) + public + returns (uint256 fromTokenId, address[] memory aliases) + { + unchecked { + uint256 n = p.to.length; + aliases = new address[](n); + for (uint256 i; i != n; ++i) { + (aliases[i], p.to[i]) = _getAliasAndAddress(p.to[i]); + } + fromTokenId = ISuperMinterV2(superMinter).platformAirdrop(p); + } + } + + 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]); + } + } + } + + fallback() external payable { + LibZip.cdFallback(); + } + + receive() external payable { + LibZip.cdFallback(); + } + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + function addressesToAliases(address[] memory a) public view returns (address[] memory) { + unchecked { + uint256 n = a.length; + for (uint256 i; i != n; ++i) { + a[i] = _addressToAlias[a[i]]; + } + return a; + } + } + + function aliasesToAddresses(address[] memory a) public view returns (address[] memory) { + unchecked { + uint256 n = a.length; + for (uint256 i; i != n; ++i) { + a[i] = _aliasToAddress[a[i]]; + } + return a; + } + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + function _getAliasAndAddress(address aliasOrAddress) internal returns (address alias_, address address_) { + if (aliasOrAddress == address(0)) revert AliasOrAddressCannotBeZero(); + + address_ = _aliasToAddress[aliasOrAddress]; + + if (address_ == address(0)) { + // If the address has not been registered, + address_ = aliasOrAddress; // then the input must be an address. + unchecked { + alias_ = address(uint160(++_numAliases)); // Increment the `_numAliases` and cast it into an alias. + } + // Add to the mappings. + _aliasToAddress[alias_] = address_; + _addressToAlias[address_] = alias_; + emit RegisteredAlias(address_, alias_); + } else { + // Otherwise, if the address has already been registered, + alias_ = aliasOrAddress; // then the input must be an alias. + } + } +} diff --git a/contracts/modules/interfaces/IPlatformAirdropper.sol b/contracts/modules/interfaces/IPlatformAirdropper.sol new file mode 100644 index 00000000..b77846b2 --- /dev/null +++ b/contracts/modules/interfaces/IPlatformAirdropper.sol @@ -0,0 +1,42 @@ +// 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 { + // ============================================================= + // EVENTS + // ============================================================= + + event RegisteredAlias(address indexed address_, address indexed alias_); + + // ============================================================= + // ERRORS + // ============================================================= + + error AliasOrAddressCannotBeZero(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + function platformAirdrop(address superMinter, ISuperMinterV2.PlatformAirdrop memory p) + external + returns (uint256 fromTokenId, address[] memory aliases); + + function platformAirdropMulti(address superMinter, ISuperMinterV2.PlatformAirdrop[] memory p) + external + returns (uint256[] memory fromTokenIds, address[][] memory aliases); + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + function addressesToAliases(address[] memory addresses) external view returns (address[] memory aliases); + + function aliasesToAddresses(address[] memory aliases) external view returns (address[] memory addresses); +} From 9eb2d8337117f95806beb5a1d88dd294499ed998 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 20 Dec 2023 23:49:06 +0000 Subject: [PATCH 02/10] Add comments and tests --- contracts/modules/PlatformAirdropper.sol | 90 ++++++-- .../interfaces/IPlatformAirdropper.sol | 59 ++++- tests/modules/PlatformAirdropper.t.sol | 212 ++++++++++++++++++ 3 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 tests/modules/PlatformAirdropper.t.sol diff --git a/contracts/modules/PlatformAirdropper.sol b/contracts/modules/PlatformAirdropper.sol index d9c236bc..6e6103d4 100644 --- a/contracts/modules/PlatformAirdropper.sol +++ b/contracts/modules/PlatformAirdropper.sol @@ -15,16 +15,28 @@ contract PlatformAirdropper is IPlatformAirdropper { // STORAGE // ============================================================= - uint256 internal _numAliases; - + /** + * @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 IPlatformAirdropper + */ function platformAirdrop(address superMinter, ISuperMinterV2.PlatformAirdrop memory p) public returns (uint256 fromTokenId, address[] memory aliases) @@ -39,6 +51,9 @@ contract PlatformAirdropper is IPlatformAirdropper { } } + /** + * @inheritdoc IPlatformAirdropper + */ function platformAirdropMulti(address superMinter, ISuperMinterV2.PlatformAirdrop[] memory p) public returns (uint256[] memory fromTokenIds, address[][] memory aliases) @@ -53,10 +68,32 @@ contract PlatformAirdropper is IPlatformAirdropper { } } + /** + * @inheritdoc IPlatformAirdropper + */ + function registerAliases(address[] memory a) public returns (address[] memory) { + unchecked { + uint256 n = a.length; + for (uint256 i; i != n; ++i) { + a[i] = _registerAlias(a[i]); + } + return a; + } + } + + // Misc functions: + // --------------- + + /** + * @dev For calldata compression. + */ fallback() external payable { LibZip.cdFallback(); } + /** + * @dev For calldata compression. + */ receive() external payable { LibZip.cdFallback(); } @@ -65,6 +102,9 @@ contract PlatformAirdropper is IPlatformAirdropper { // PUBLIC / EXTERNAL VIEW FUNCTIONS // ============================================================= + /** + * @inheritdoc IPlatformAirdropper + */ function addressesToAliases(address[] memory a) public view returns (address[] memory) { unchecked { uint256 n = a.length; @@ -75,6 +115,9 @@ contract PlatformAirdropper is IPlatformAirdropper { } } + /** + * @inheritdoc IPlatformAirdropper + */ function aliasesToAddresses(address[] memory a) public view returns (address[] memory) { unchecked { uint256 n = a.length; @@ -89,24 +132,43 @@ contract PlatformAirdropper is IPlatformAirdropper { // INTERNAL / PRIVATE HELPERS // ============================================================= + /** + * @dev Returns the alias and address for `aliasOrAddress`. + * If the `aliasOrAddress` 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 aliasOrAddress The alias or address. + * @return alias_ The alias. + * @return address_ The address. + */ function _getAliasAndAddress(address aliasOrAddress) internal returns (address alias_, address address_) { - if (aliasOrAddress == address(0)) revert AliasOrAddressCannotBeZero(); - - address_ = _aliasToAddress[aliasOrAddress]; + // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. + if (uint160(aliasOrAddress) <= type(uint32).max) { + alias_ = aliasOrAddress; + address_ = _aliasToAddress[alias_]; + if (address_ == address(0)) revert AliasNotFound(); + } else { + address_ = aliasOrAddress; + alias_ = _registerAlias(address_); + } + } - if (address_ == address(0)) { - // If the address has not been registered, - address_ = aliasOrAddress; // then the input must be an address. - unchecked { - alias_ = address(uint160(++_numAliases)); // Increment the `_numAliases` and cast it into an alias. - } + /** + * @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_); - } else { - // Otherwise, if the address has already been registered, - alias_ = aliasOrAddress; // then the input must be an alias. } } } diff --git a/contracts/modules/interfaces/IPlatformAirdropper.sol b/contracts/modules/interfaces/IPlatformAirdropper.sol index b77846b2..25087f0f 100644 --- a/contracts/modules/interfaces/IPlatformAirdropper.sol +++ b/contracts/modules/interfaces/IPlatformAirdropper.sol @@ -12,31 +12,86 @@ interface IPlatformAirdropper { // EVENTS // ============================================================= - event RegisteredAlias(address indexed address_, address indexed alias_); + /** + * @dev Emitted when an address is registered with an alias. + */ + event RegisteredAlias(address address_, address alias_); // ============================================================= // ERRORS // ============================================================= - error AliasOrAddressCannotBeZero(); + /** + * @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 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); + /** + * @dev Registers the addresses as aliases. + * If an address already has an alias, then it will be an no-op for the address. + * @param addresses The addresses to register. + * @return aliases The aliases for the addresses. + */ + function registerAliases(address[] memory addresses) external returns (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 Returns an array of aliases corresponding to each address. + * If the alias has not been registered, the address will be the zero address. + * @param addresses The array of addresses to query. + * @return aliases The array of aliases. + */ function addressesToAliases(address[] memory addresses) external view returns (address[] memory aliases); + /** + * @dev Returns an array of addresses corresponding to each alias. + * If the address has not been registered, the alias will be the zero address. + * @param aliases The array of aliases to query. + * @return addresses The array of addresses. + */ function aliasesToAddresses(address[] memory aliases) external view returns (address[] memory addresses); } diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol new file mode 100644 index 00000000..9afee270 --- /dev/null +++ b/tests/modules/PlatformAirdropper.t.sol @@ -0,0 +1,212 @@ +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 { 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; + + 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()); + pa = new PlatformAirdropper(); + } + + function test_registerAliases(uint256) public { + address[] memory addresses = _randomNonZeroAddressesGreaterThan(); + assertEq(pa.addressesToAliases(addresses), new address[](addresses.length)); + address[] memory aliases = pa.registerAliases(addresses); + assertEq(pa.aliasesToAddresses(aliases), addresses); + assertEq(pa.addressesToAliases(addresses), aliases); + assertEq(_uniquified(addresses).length, _uniquified(aliases).length); + assertEq(_uniquified(pa.aliasesToAddresses(aliases)), addresses); + assertEq(_uniquified(pa.addressesToAliases(addresses)), aliases); + } + + 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; + 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)); + } + } + } + + 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); + + 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 = pa.numAliases(); + (, aliases[1]) = pa.platformAirdrop(address(sm), p); + assertEq(pa.numAliases(), numAliases); + assertEq(aliases[0], aliases[1]); + + p.to = pa.aliasesToAddresses(p.to); + + 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); + + 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 = pa.aliasesToAddresses(p[j].to); + } + + 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, pa.numAliases()); + } + } + + 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); + } +} From 096ccb1c3fb081053125317ae3083aa1bfa8de13 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 20 Dec 2023 23:55:15 +0000 Subject: [PATCH 03/10] T --- contracts/modules/PlatformAirdropper.sol | 1 - tests/modules/PlatformAirdropper.t.sol | 47 +++++++++++++++--------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/contracts/modules/PlatformAirdropper.sol b/contracts/modules/PlatformAirdropper.sol index 6e6103d4..8695a7ad 100644 --- a/contracts/modules/PlatformAirdropper.sol +++ b/contracts/modules/PlatformAirdropper.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.16; import { ISuperMinterV2 } from "@modules/interfaces/ISuperMinterV2.sol"; import { IPlatformAirdropper } from "@modules/interfaces/IPlatformAirdropper.sol"; - import { LibZip } from "solady/utils/LibZip.sol"; /** diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol index 9afee270..87075abb 100644 --- a/tests/modules/PlatformAirdropper.t.sol +++ b/tests/modules/PlatformAirdropper.t.sol @@ -54,6 +54,9 @@ contract PlatformAirdropperTests is TestConfigV2_1 { 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 { @@ -104,10 +107,12 @@ contract PlatformAirdropperTests is TestConfigV2_1 { address[][2] memory aliases; (, aliases[0]) = pa.platformAirdrop(address(sm), p); - 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); + 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; @@ -122,10 +127,12 @@ contract PlatformAirdropperTests is TestConfigV2_1 { p.to = pa.aliasesToAddresses(p.to); - 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); + 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); @@ -168,11 +175,13 @@ contract PlatformAirdropperTests is TestConfigV2_1 { address[][][2] memory aliases; (, aliases[0]) = pa.platformAirdropMulti(address(sm), p); - 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); + 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); + } } } @@ -189,11 +198,13 @@ contract PlatformAirdropperTests is TestConfigV2_1 { p[j].to = pa.aliasesToAddresses(p[j].to); } - 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); + 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); + } } } From 15f4e6c276756f575d95bd6e55334817b44ae879 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Dec 2023 22:10:29 +0000 Subject: [PATCH 04/10] Add platform airdrop limit test --- tests/modules/PlatformAirdropper.t.sol | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol index 87075abb..15d424c4 100644 --- a/tests/modules/PlatformAirdropper.t.sol +++ b/tests/modules/PlatformAirdropper.t.sol @@ -212,6 +212,43 @@ contract PlatformAirdropperTests is TestConfigV2_1 { } } + 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 = 200; + + 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) From 0c224616fdca19441dc56b7c036010e81efa4606 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Thu, 21 Dec 2023 22:15:38 +0000 Subject: [PATCH 05/10] T --- tests/modules/PlatformAirdropper.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol index 15d424c4..1a70a56a 100644 --- a/tests/modules/PlatformAirdropper.t.sol +++ b/tests/modules/PlatformAirdropper.t.sol @@ -229,7 +229,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { vm.prank(c.platform); sm.setPlatformSigner(signer); - uint256 n = 200; + uint256 n = 256; ISuperMinterV2.PlatformAirdrop memory p; p.edition = address(edition); From 29a0a9e08f7ecf5ad8d0f06fca5c52880506f4fb Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 26 Jan 2024 02:41:28 +0000 Subject: [PATCH 06/10] Refactor to have separate AddressAliasRegistry --- contracts/modules/AddressAliasRegistry.sol | 149 ++++++++++++++++++ contracts/modules/PlatformAirdropper.sol | 114 ++------------ .../interfaces/IAddressAliasRegistry.sol | 66 ++++++++ .../interfaces/IPlatformAirdropper.sol | 53 +------ tests/modules/AddressAliasRegistry.t.sol | 70 ++++++++ tests/modules/PlatformAirdropper.t.sol | 86 +++++----- 6 files changed, 338 insertions(+), 200 deletions(-) create mode 100644 contracts/modules/AddressAliasRegistry.sol create mode 100644 contracts/modules/interfaces/IAddressAliasRegistry.sol create mode 100644 tests/modules/AddressAliasRegistry.t.sol diff --git a/contracts/modules/AddressAliasRegistry.sol b/contracts/modules/AddressAliasRegistry.sol new file mode 100644 index 00000000..0b7aa0e9 --- /dev/null +++ b/contracts/modules/AddressAliasRegistry.sol @@ -0,0 +1,149 @@ +// 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 a) + public + returns (address[] memory aliases, address[] memory addresses) + { + unchecked { + uint256 n = a.length; + aliases = new address[](n); + addresses = new address[](n); + for (uint256 i; i != n; ++i) { + (aliases[i], addresses[i]) = _resolveAndRegister(a[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 a) public view returns (address[] memory aliases, address[] memory addresses) { + unchecked { + uint256 n = a.length; + aliases = new address[](n); + addresses = new address[](n); + for (uint256 i; i != n; ++i) { + (aliases[i], addresses[i]) = _resolve(a[i]); + } + } + } + + // ============================================================= + // INTERNAL / PRIVATE HELPERS + // ============================================================= + + /** + * @dev Returns the alias and address for `aliasOrAddress`. + * If the `aliasOrAddress` 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 aliasOrAddress The alias or address. + * @return alias_ The alias. + * @return address_ The address. + */ + function _resolveAndRegister(address aliasOrAddress) internal returns (address alias_, address address_) { + // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. + if (uint160(aliasOrAddress) <= type(uint32).max) { + alias_ = aliasOrAddress; + address_ = _aliasToAddress[alias_]; + if (address_ == address(0)) revert AliasNotFound(); + } else { + address_ = aliasOrAddress; + alias_ = _registerAlias(address_); + } + } + + /** + * @dev Returns the alias and address for `aliasOrAddress`. + * If the `aliasOrAddress` is less than `2**31 - 1`, it is treated as an alias. + * Otherwise, it is treated as an address. + * @param aliasOrAddress The alias or address. + * @return alias_ The alias. + * @return address_ The address. + */ + function _resolve(address aliasOrAddress) internal view returns (address alias_, address address_) { + // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. + if (uint160(aliasOrAddress) <= type(uint32).max) { + alias_ = aliasOrAddress; + address_ = _aliasToAddress[alias_]; + } else { + address_ = aliasOrAddress; + 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 index 8695a7ad..91766d6c 100644 --- a/contracts/modules/PlatformAirdropper.sol +++ b/contracts/modules/PlatformAirdropper.sol @@ -3,6 +3,7 @@ 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"; /** @@ -11,23 +12,21 @@ import { LibZip } from "solady/utils/LibZip.sol"; */ contract PlatformAirdropper is IPlatformAirdropper { // ============================================================= - // STORAGE + // IMMUTABLES // ============================================================= /** - * @dev The current number of aliases. + * @dev The address alias registry. */ - uint32 public numAliases; + address public immutable addressAliasRegistry; - /** - * @dev Maps an alias to its original address. - */ - mapping(address => address) internal _aliasToAddress; + // ============================================================= + // CONSTRUCTOR + // ============================================================= - /** - * @dev Maps an address to its alias. - */ - mapping(address => address) internal _addressToAlias; + constructor(address addressAliasRegistry_) payable { + addressAliasRegistry = addressAliasRegistry_; + } // ============================================================= // PUBLIC / EXTERNAL WRITE FUNCTIONS @@ -41,11 +40,7 @@ contract PlatformAirdropper is IPlatformAirdropper { returns (uint256 fromTokenId, address[] memory aliases) { unchecked { - uint256 n = p.to.length; - aliases = new address[](n); - for (uint256 i; i != n; ++i) { - (aliases[i], p.to[i]) = _getAliasAndAddress(p.to[i]); - } + (aliases, p.to) = IAddressAliasRegistry(addressAliasRegistry).resolveAndRegister(p.to); fromTokenId = ISuperMinterV2(superMinter).platformAirdrop(p); } } @@ -67,19 +62,6 @@ contract PlatformAirdropper is IPlatformAirdropper { } } - /** - * @inheritdoc IPlatformAirdropper - */ - function registerAliases(address[] memory a) public returns (address[] memory) { - unchecked { - uint256 n = a.length; - for (uint256 i; i != n; ++i) { - a[i] = _registerAlias(a[i]); - } - return a; - } - } - // Misc functions: // --------------- @@ -96,78 +78,4 @@ contract PlatformAirdropper is IPlatformAirdropper { receive() external payable { LibZip.cdFallback(); } - - // ============================================================= - // PUBLIC / EXTERNAL VIEW FUNCTIONS - // ============================================================= - - /** - * @inheritdoc IPlatformAirdropper - */ - function addressesToAliases(address[] memory a) public view returns (address[] memory) { - unchecked { - uint256 n = a.length; - for (uint256 i; i != n; ++i) { - a[i] = _addressToAlias[a[i]]; - } - return a; - } - } - - /** - * @inheritdoc IPlatformAirdropper - */ - function aliasesToAddresses(address[] memory a) public view returns (address[] memory) { - unchecked { - uint256 n = a.length; - for (uint256 i; i != n; ++i) { - a[i] = _aliasToAddress[a[i]]; - } - return a; - } - } - - // ============================================================= - // INTERNAL / PRIVATE HELPERS - // ============================================================= - - /** - * @dev Returns the alias and address for `aliasOrAddress`. - * If the `aliasOrAddress` 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 aliasOrAddress The alias or address. - * @return alias_ The alias. - * @return address_ The address. - */ - function _getAliasAndAddress(address aliasOrAddress) internal returns (address alias_, address address_) { - // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. - if (uint160(aliasOrAddress) <= type(uint32).max) { - alias_ = aliasOrAddress; - address_ = _aliasToAddress[alias_]; - if (address_ == address(0)) revert AliasNotFound(); - } else { - address_ = aliasOrAddress; - alias_ = _registerAlias(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/interfaces/IAddressAliasRegistry.sol b/contracts/modules/interfaces/IAddressAliasRegistry.sol new file mode 100644 index 00000000..486235c9 --- /dev/null +++ b/contracts/modules/interfaces/IAddressAliasRegistry.sol @@ -0,0 +1,66 @@ +// 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 a An array of addresses, which can be aliases. + * @return aliases The aliases for the addresses. + * @return addresses The resolved addresses. + */ + function resolveAndRegister(address[] memory a) + external + returns (address[] memory aliases, address[] memory addresses); + + // ============================================================= + // 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 aliases, 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 a An array of addresses, which can be aliases. + * @return aliases The aliases for the addresses. + * @return addresses The resolved addresses. + */ + function resolve(address[] memory a) external view returns (address[] memory aliases, address[] memory addresses); +} diff --git a/contracts/modules/interfaces/IPlatformAirdropper.sol b/contracts/modules/interfaces/IPlatformAirdropper.sol index 25087f0f..b1a06888 100644 --- a/contracts/modules/interfaces/IPlatformAirdropper.sol +++ b/contracts/modules/interfaces/IPlatformAirdropper.sol @@ -8,29 +8,6 @@ import { ISuperMinterV2 } from "./ISuperMinterV2.sol"; * @dev The `PlatformAirdropper` utility class to batch airdrop tokens. */ interface IPlatformAirdropper { - // ============================================================= - // 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 // ============================================================= @@ -61,37 +38,13 @@ interface IPlatformAirdropper { external returns (uint256[] memory fromTokenIds, address[][] memory aliases); - /** - * @dev Registers the addresses as aliases. - * If an address already has an alias, then it will be an no-op for the address. - * @param addresses The addresses to register. - * @return aliases The aliases for the addresses. - */ - function registerAliases(address[] memory addresses) external returns (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 Returns an array of aliases corresponding to each address. - * If the alias has not been registered, the address will be the zero address. - * @param addresses The array of addresses to query. - * @return aliases The array of aliases. - */ - function addressesToAliases(address[] memory addresses) external view returns (address[] memory aliases); - - /** - * @dev Returns an array of addresses corresponding to each alias. - * If the address has not been registered, the alias will be the zero address. - * @param aliases The array of aliases to query. - * @return addresses The array of addresses. + * @dev Returns the address alias registry. + * @return The immutable value. */ - function aliasesToAddresses(address[] memory aliases) external view returns (address[] memory addresses); + function addressAliasRegistry() external view returns (address); } diff --git a/tests/modules/AddressAliasRegistry.t.sol b/tests/modules/AddressAliasRegistry.t.sol new file mode 100644 index 00000000..bb6ceb65 --- /dev/null +++ b/tests/modules/AddressAliasRegistry.t.sol @@ -0,0 +1,70 @@ +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.aliases, r0.addresses) = aar.resolve(new address[](addresses.length)); + assertEq(r0.aliases, new address[](addresses.length)); + assertEq(r0.addresses, new address[](addresses.length)); + } + (r0.aliases, r0.addresses) = aar.resolveAndRegister(addresses); + (r1.aliases, r1.addresses) = aar.resolve(addresses); + assertEq(r1.aliases, r0.aliases); + assertEq(r1.addresses, r0.addresses); + (r1.aliases, r1.addresses) = 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 index 1a70a56a..16db6d93 100644 --- a/tests/modules/PlatformAirdropper.t.sol +++ b/tests/modules/PlatformAirdropper.t.sol @@ -3,6 +3,7 @@ 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"; @@ -14,6 +15,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { SuperMinterV2 sm; SoundEditionV2_1 edition; PlatformAirdropper pa; + AddressAliasRegistry aar; mapping(uint256 => mapping(address => uint256)) internal _expectedMintCounts; @@ -28,48 +30,8 @@ contract PlatformAirdropperTests is TestConfigV2_1 { edition = createSoundEdition(init); sm = new SuperMinterV2(); edition.grantRoles(address(sm), edition.MINTER_ROLE()); - pa = new PlatformAirdropper(); - } - - function test_registerAliases(uint256) public { - address[] memory addresses = _randomNonZeroAddressesGreaterThan(); - assertEq(pa.addressesToAliases(addresses), new address[](addresses.length)); - address[] memory aliases = pa.registerAliases(addresses); - assertEq(pa.aliasesToAddresses(aliases), addresses); - assertEq(pa.addressesToAliases(addresses), aliases); - assertEq(_uniquified(addresses).length, _uniquified(aliases).length); - assertEq(_uniquified(pa.aliasesToAddresses(aliases)), addresses); - assertEq(_uniquified(pa.addressesToAliases(addresses)), aliases); - } - - 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)); - } - } + aar = new AddressAliasRegistry(); + pa = new PlatformAirdropper(address(aar)); } function test_platformAirdrop(uint256) public { @@ -120,12 +82,12 @@ contract PlatformAirdropperTests is TestConfigV2_1 { // Note that we replace the addresses AFTER signing. p.to = aliases[0]; - uint256 numAliases = pa.numAliases(); + uint256 numAliases = aar.numAliases(); (, aliases[1]) = pa.platformAirdrop(address(sm), p); - assertEq(pa.numAliases(), numAliases); + assertEq(aar.numAliases(), numAliases); assertEq(aliases[0], aliases[1]); - p.to = pa.aliasesToAddresses(p.to); + (, p.to) = aar.resolve(p.to); if (_random() % 8 == 0) { for (uint256 i; i < p.to.length; ++i) { @@ -195,7 +157,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { (, aliases[1]) = pa.platformAirdropMulti(address(sm), p); for (uint256 j; j != 2; ++j) { assertEq(aliases[0][j], aliases[1][j]); - p[j].to = pa.aliasesToAddresses(p[j].to); + (, p[j].to) = aar.resolve(p[j].to); } if (_random() % 8 == 0) { @@ -208,7 +170,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { } } - assertEq(LibSort.union(_uniquified(p[0].to), _uniquified(p[1].to)).length, pa.numAliases()); + assertEq(LibSort.union(_uniquified(p[0].to), _uniquified(p[1].to)).length, aar.numAliases()); } } @@ -257,4 +219,34 @@ contract PlatformAirdropperTests is TestConfigV2_1 { (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)); + } + } + } } From e9ed65dbc9108d06f1d6ab45d1a4442376da9e4f Mon Sep 17 00:00:00 2001 From: Vectorized Date: Fri, 26 Jan 2024 02:51:46 +0000 Subject: [PATCH 07/10] Swap address and aliases return arg positions --- contracts/modules/AddressAliasRegistry.sol | 20 +++++++++---------- contracts/modules/PlatformAirdropper.sol | 2 +- .../interfaces/IAddressAliasRegistry.sol | 8 ++++---- tests/modules/AddressAliasRegistry.t.sol | 12 +++++++---- tests/modules/PlatformAirdropper.t.sol | 4 ++-- 5 files changed, 25 insertions(+), 21 deletions(-) diff --git a/contracts/modules/AddressAliasRegistry.sol b/contracts/modules/AddressAliasRegistry.sol index 0b7aa0e9..c395726d 100644 --- a/contracts/modules/AddressAliasRegistry.sol +++ b/contracts/modules/AddressAliasRegistry.sol @@ -37,14 +37,14 @@ contract AddressAliasRegistry is IAddressAliasRegistry { */ function resolveAndRegister(address[] memory a) public - returns (address[] memory aliases, address[] memory addresses) + returns (address[] memory addresses, address[] memory aliases) { unchecked { uint256 n = a.length; + addresses = a; aliases = new address[](n); - addresses = new address[](n); for (uint256 i; i != n; ++i) { - (aliases[i], addresses[i]) = _resolveAndRegister(a[i]); + (addresses[i], aliases[i]) = _resolveAndRegister(a[i]); } } } @@ -73,13 +73,13 @@ contract AddressAliasRegistry is IAddressAliasRegistry { /** * @inheritdoc IAddressAliasRegistry */ - function resolve(address[] memory a) public view returns (address[] memory aliases, address[] memory addresses) { + function resolve(address[] memory a) public view returns (address[] memory addresses, address[] memory aliases) { unchecked { uint256 n = a.length; + addresses = a; aliases = new address[](n); - addresses = new address[](n); for (uint256 i; i != n; ++i) { - (aliases[i], addresses[i]) = _resolve(a[i]); + (addresses[i], aliases[i]) = _resolve(a[i]); } } } @@ -93,10 +93,10 @@ contract AddressAliasRegistry is IAddressAliasRegistry { * If the `aliasOrAddress` 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 aliasOrAddress The alias or address. - * @return alias_ The alias. * @return address_ The address. + * @return alias_ The alias. */ - function _resolveAndRegister(address aliasOrAddress) internal returns (address alias_, address address_) { + function _resolveAndRegister(address aliasOrAddress) internal returns (address address_, address alias_) { // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. if (uint160(aliasOrAddress) <= type(uint32).max) { alias_ = aliasOrAddress; @@ -113,10 +113,10 @@ contract AddressAliasRegistry is IAddressAliasRegistry { * If the `aliasOrAddress` is less than `2**31 - 1`, it is treated as an alias. * Otherwise, it is treated as an address. * @param aliasOrAddress The alias or address. - * @return alias_ The alias. * @return address_ The address. + * @return alias_ The alias. */ - function _resolve(address aliasOrAddress) internal view returns (address alias_, address address_) { + function _resolve(address aliasOrAddress) internal view returns (address address_, address alias_) { // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. if (uint160(aliasOrAddress) <= type(uint32).max) { alias_ = aliasOrAddress; diff --git a/contracts/modules/PlatformAirdropper.sol b/contracts/modules/PlatformAirdropper.sol index 91766d6c..687fc91e 100644 --- a/contracts/modules/PlatformAirdropper.sol +++ b/contracts/modules/PlatformAirdropper.sol @@ -40,7 +40,7 @@ contract PlatformAirdropper is IPlatformAirdropper { returns (uint256 fromTokenId, address[] memory aliases) { unchecked { - (aliases, p.to) = IAddressAliasRegistry(addressAliasRegistry).resolveAndRegister(p.to); + (p.to, aliases) = IAddressAliasRegistry(addressAliasRegistry).resolveAndRegister(p.to); fromTokenId = ISuperMinterV2(superMinter).platformAirdrop(p); } } diff --git a/contracts/modules/interfaces/IAddressAliasRegistry.sol b/contracts/modules/interfaces/IAddressAliasRegistry.sol index 486235c9..83c839ba 100644 --- a/contracts/modules/interfaces/IAddressAliasRegistry.sol +++ b/contracts/modules/interfaces/IAddressAliasRegistry.sol @@ -37,12 +37,12 @@ interface IAddressAliasRegistry { * @dev Resolve the addresses or aliases. * If an address does not have an aliases, an alias will be registered for it. * @param a An array of addresses, which can be aliases. - * @return aliases The aliases for the addresses. * @return addresses The resolved addresses. + * @return aliases The aliases for the addresses. */ function resolveAndRegister(address[] memory a) external - returns (address[] memory aliases, address[] memory addresses); + returns (address[] memory addresses, address[] memory aliases); // ============================================================= // PUBLIC / EXTERNAL VIEW FUNCTIONS @@ -59,8 +59,8 @@ interface IAddressAliasRegistry { * If an address does not have an aliases, 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 a An array of addresses, which can be aliases. - * @return aliases The aliases for the addresses. * @return addresses The resolved addresses. + * @return aliases The aliases for the addresses. */ - function resolve(address[] memory a) external view returns (address[] memory aliases, address[] memory addresses); + function resolve(address[] memory a) external view returns (address[] memory addresses, address[] memory aliases); } diff --git a/tests/modules/AddressAliasRegistry.t.sol b/tests/modules/AddressAliasRegistry.t.sol index bb6ceb65..857adf7f 100644 --- a/tests/modules/AddressAliasRegistry.t.sol +++ b/tests/modules/AddressAliasRegistry.t.sol @@ -22,15 +22,19 @@ contract AddressAliasRegistryTests is TestConfigV2_1 { Resolved memory r1; address[] memory addresses = _randomNonZeroAddressesGreaterThan(); if (_random() % 32 == 0) { - (r0.aliases, r0.addresses) = aar.resolve(new address[](addresses.length)); + (r0.addresses, r0.aliases) = aar.resolve(new address[](addresses.length)); assertEq(r0.aliases, new address[](addresses.length)); assertEq(r0.addresses, new address[](addresses.length)); } - (r0.aliases, r0.addresses) = aar.resolveAndRegister(addresses); - (r1.aliases, r1.addresses) = aar.resolve(addresses); + (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); + } assertEq(r1.aliases, r0.aliases); assertEq(r1.addresses, r0.addresses); - (r1.aliases, r1.addresses) = aar.resolve(r0.aliases); + (r1.addresses, r1.aliases) = aar.resolve(r0.aliases); assertEq(r1.aliases, r0.aliases); assertEq(r1.addresses, r0.addresses); uint256 n = _uniquified(addresses).length; diff --git a/tests/modules/PlatformAirdropper.t.sol b/tests/modules/PlatformAirdropper.t.sol index 16db6d93..ba89e85c 100644 --- a/tests/modules/PlatformAirdropper.t.sol +++ b/tests/modules/PlatformAirdropper.t.sol @@ -87,7 +87,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { assertEq(aar.numAliases(), numAliases); assertEq(aliases[0], aliases[1]); - (, p.to) = aar.resolve(p.to); + (p.to, ) = aar.resolve(p.to); if (_random() % 8 == 0) { for (uint256 i; i < p.to.length; ++i) { @@ -157,7 +157,7 @@ contract PlatformAirdropperTests is TestConfigV2_1 { (, 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); + (p[j].to, ) = aar.resolve(p[j].to); } if (_random() % 8 == 0) { From d46b528be3e84ab9d31ff9bacad12a562766a733 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Wed, 31 Jan 2024 17:24:41 +0000 Subject: [PATCH 08/10] Add addressOf and aliasOf functions --- contracts/modules/AddressAliasRegistry.sol | 67 ++++++++++++------- .../interfaces/IAddressAliasRegistry.sol | 27 ++++++-- tests/modules/AddressAliasRegistry.t.sol | 7 ++ 3 files changed, 72 insertions(+), 29 deletions(-) diff --git a/contracts/modules/AddressAliasRegistry.sol b/contracts/modules/AddressAliasRegistry.sol index c395726d..5c504ac2 100644 --- a/contracts/modules/AddressAliasRegistry.sol +++ b/contracts/modules/AddressAliasRegistry.sol @@ -35,16 +35,16 @@ contract AddressAliasRegistry is IAddressAliasRegistry { /** * @inheritdoc IAddressAliasRegistry */ - function resolveAndRegister(address[] memory a) + function resolveAndRegister(address[] memory addressesOrAliases) public returns (address[] memory addresses, address[] memory aliases) { unchecked { - uint256 n = a.length; - addresses = a; + uint256 n = addressesOrAliases.length; + addresses = addressesOrAliases; aliases = new address[](n); for (uint256 i; i != n; ++i) { - (addresses[i], aliases[i]) = _resolveAndRegister(a[i]); + (addresses[i], aliases[i]) = _resolveAndRegister(addressesOrAliases[i]); } } } @@ -73,56 +73,75 @@ contract AddressAliasRegistry is IAddressAliasRegistry { /** * @inheritdoc IAddressAliasRegistry */ - function resolve(address[] memory a) public view returns (address[] memory addresses, address[] memory aliases) { + function resolve(address[] memory addressesOrAliases) + public + view + returns (address[] memory addresses, address[] memory aliases) + { unchecked { - uint256 n = a.length; - addresses = a; + uint256 n = addressesOrAliases.length; + addresses = addressesOrAliases; aliases = new address[](n); for (uint256 i; i != n; ++i) { - (addresses[i], aliases[i]) = _resolve(a[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 `aliasOrAddress`. - * If the `aliasOrAddress` is less than `2**31 - 1`, it is treated as an alias. + * @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 aliasOrAddress The alias or address. + * @param addressOrAlias The alias or address. * @return address_ The address. * @return alias_ The alias. */ - function _resolveAndRegister(address aliasOrAddress) internal returns (address address_, address alias_) { - // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. - if (uint160(aliasOrAddress) <= type(uint32).max) { - alias_ = aliasOrAddress; + 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_ = aliasOrAddress; + address_ = addressOrAlias; alias_ = _registerAlias(address_); } } /** - * @dev Returns the alias and address for `aliasOrAddress`. - * If the `aliasOrAddress` is less than `2**31 - 1`, it is treated as an alias. + * @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 aliasOrAddress The alias or address. + * @param addressOrAlias The alias or address. * @return address_ The address. * @return alias_ The alias. */ - function _resolve(address aliasOrAddress) internal view returns (address address_, address alias_) { - // If the `aliasOrAddress` is less than or equal to `2**32 - 1`, we will consider it an alias. - if (uint160(aliasOrAddress) <= type(uint32).max) { - alias_ = aliasOrAddress; + 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_ = aliasOrAddress; + address_ = addressOrAlias; alias_ = _addressToAlias[address_]; } } diff --git a/contracts/modules/interfaces/IAddressAliasRegistry.sol b/contracts/modules/interfaces/IAddressAliasRegistry.sol index 83c839ba..02b46486 100644 --- a/contracts/modules/interfaces/IAddressAliasRegistry.sol +++ b/contracts/modules/interfaces/IAddressAliasRegistry.sol @@ -36,11 +36,11 @@ interface IAddressAliasRegistry { /** * @dev Resolve the addresses or aliases. * If an address does not have an aliases, an alias will be registered for it. - * @param a An array of addresses, which can be aliases. + * @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 a) + function resolveAndRegister(address[] memory addressesOrAliases) external returns (address[] memory addresses, address[] memory aliases); @@ -56,11 +56,28 @@ interface IAddressAliasRegistry { /** * @dev Resolve the addresses or aliases. - * If an address does not have an aliases, it's corresponding returned alias will be zero. + * 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 a An array of addresses, which can be aliases. + * @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 a) external view returns (address[] memory addresses, address[] memory aliases); + 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/tests/modules/AddressAliasRegistry.t.sol b/tests/modules/AddressAliasRegistry.t.sol index 857adf7f..09ed394f 100644 --- a/tests/modules/AddressAliasRegistry.t.sol +++ b/tests/modules/AddressAliasRegistry.t.sol @@ -25,12 +25,19 @@ contract AddressAliasRegistryTests is TestConfigV2_1 { (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); From 6339572fe96d1cd9f16fb537abf86b0e27eced1b Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 5 Feb 2024 23:14:13 +0000 Subject: [PATCH 09/10] Reinstall solady --- lib/solady | 1 - 1 file changed, 1 deletion(-) delete mode 160000 lib/solady diff --git a/lib/solady b/lib/solady deleted file mode 160000 index cde0a5fb..00000000 --- a/lib/solady +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cde0a5fb594da8655ba6bfcdc2e40a7c870c0cc0 From 46c60f56ddcc6a88f57bacb96f00ed319a59f892 Mon Sep 17 00:00:00 2001 From: Vectorized Date: Mon, 5 Feb 2024 23:14:14 +0000 Subject: [PATCH 10/10] forge install: solady v0.0.167 --- lib/solady | 1 + 1 file changed, 1 insertion(+) create mode 160000 lib/solady diff --git a/lib/solady b/lib/solady new file mode 160000 index 00000000..9de1fe26 --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit 9de1fe26af4f4b1bbb4b5efcedc503342fc55ee8