diff --git a/contracts/modules/CoreActions.sol b/contracts/modules/CoreActions.sol index dd548763..e1f9c110 100644 --- a/contracts/modules/CoreActions.sol +++ b/contracts/modules/CoreActions.sol @@ -100,39 +100,25 @@ contract CoreActions is ICoreActions, EIP712 { external returns (address[] memory targetAliases, address[][] memory actorAliases) { + _validateArrayLengths(r); + uint256 n = r.targets.length; address[] memory resolvedTargets; address[][] memory resolvedActors = new address[][](n); actorAliases = new address[][](n); - // Check input array lengths and resolve aliases. + // Resolve and register aliases. unchecked { - if (n != r.actors.length) revert ArrayLengthsMismatch(); - if (n != r.timestamps.length) revert ArrayLengthsMismatch(); - IAddressAliasRegistry registry = IAddressAliasRegistry(addressAliasRegistry); (resolvedTargets, targetAliases) = registry.resolveAndRegister(r.targets); - for (uint256 i; i != n; ++i) { - if (r.actors[i].length != r.timestamps[i].length) revert ArrayLengthsMismatch(); (resolvedActors[i], actorAliases[i]) = registry.resolveAndRegister(r.actors[i]); } } // Check the signature and invalidate the nonce. { - bytes32 digest = _hashTypedData( - keccak256( - abi.encode( - CORE_ACTION_REGISTRATIONS_TYPEHASH, - r.coreActionType, // uint256 - _hashOf(resolvedTargets), // address[] - _hashOf(resolvedActors), // address[][] - _hashOf(r.timestamps), // uint256[][] - r.nonce // uint256 - ) - ) - ); + bytes32 digest = _computeDigest(r, resolvedTargets, resolvedActors); address signer = platformSigner[r.platform]; if (!SignatureCheckerLib.isValidSignatureNowCalldata(signer, digest, r.signature)) @@ -283,10 +269,60 @@ contract CoreActions is ICoreActions, EIP712 { } } + /** + * @inheritdoc ICoreActions + */ + function computeDigest(CoreActionRegistrations calldata r) external view returns (bytes32) { + _validateArrayLengths(r); + + uint256 n = r.targets.length; + address[] memory resolvedTargets; + address[][] memory resolvedActors = new address[][](n); + + // Resolve aliases. + unchecked { + IAddressAliasRegistry registry = IAddressAliasRegistry(addressAliasRegistry); + (resolvedTargets, ) = registry.resolve(r.targets); + for (uint256 i; i != n; ++i) { + if (r.actors[i].length != r.timestamps[i].length) revert ArrayLengthsMismatch(); + (resolvedActors[i], ) = registry.resolve(r.actors[i]); + } + } + + return _computeDigest(r, resolvedTargets, resolvedActors); + } + // ============================================================= // INTERNAL / PRIVATE HELPERS // ============================================================= + /** + * @dev Returns the digest for `r`, with `resolvedTargets` and `resolvedActors`. + * @param r The core actions to register. + * @param resolvedTargets The list of resolved targets. + * @param resolvedActors The list of resolved actors. + * @return The computed digest. + */ + function _computeDigest( + CoreActionRegistrations calldata r, + address[] memory resolvedTargets, + address[][] memory resolvedActors + ) internal view returns (bytes32) { + return + _hashTypedData( + keccak256( + abi.encode( + CORE_ACTION_REGISTRATIONS_TYPEHASH, + r.coreActionType, // uint256 + _hashOf(resolvedTargets), // address[] + _hashOf(resolvedActors), // address[][] + _hashOf(r.timestamps), // uint256[][] + r.nonce // uint256 + ) + ) + ); + } + /** * @dev Override for EIP-712. * @return name_ The EIP-712 name. @@ -303,6 +339,21 @@ contract CoreActions is ICoreActions, EIP712 { version_ = "1"; } + /** + * @dev Validate the array lengths. + * @param r The core actions to register. + */ + function _validateArrayLengths(CoreActionRegistrations calldata r) internal pure { + unchecked { + uint256 n = r.targets.length; + if (n != r.actors.length) revert ArrayLengthsMismatch(); + if (n != r.timestamps.length) revert ArrayLengthsMismatch(); + for (uint256 i; i != n; ++i) { + if (r.actors[i].length != r.timestamps[i].length) revert ArrayLengthsMismatch(); + } + } + } + /** * @dev Returns the hash of `a`. * @param a The input to hash. diff --git a/contracts/modules/interfaces/ICoreActions.sol b/contracts/modules/interfaces/ICoreActions.sol index 7b295d41..72469650 100644 --- a/contracts/modules/interfaces/ICoreActions.sol +++ b/contracts/modules/interfaces/ICoreActions.sol @@ -125,6 +125,13 @@ interface ICoreActions { */ function CORE_ACTION_REGISTRATIONS_TYPEHASH() external pure returns (bytes32); + /** + * @dev Returns the digest for the core actions to register. + * @param r The core actions to register. + * @return The computed value. + */ + function computeDigest(CoreActionRegistrations memory r) external view returns (bytes32); + /** * @dev Returns the configured signer for `platform`. * @param platform The platform. diff --git a/tests/modules/CoreActions.t.sol b/tests/modules/CoreActions.t.sol index 44b09195..32a2c4a2 100644 --- a/tests/modules/CoreActions.t.sol +++ b/tests/modules/CoreActions.t.sol @@ -1,33 +1,138 @@ 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 { ICoreActions, CoreActions } from "@modules/CoreActions.sol"; +import { IAddressAliasRegistry, AddressAliasRegistry } from "@modules/AddressAliasRegistry.sol"; +import { EnumerableMap } from "openzeppelin/utils/structs/EnumerableMap.sol"; +import { LibSort } from "solady/utils/LibSort.sol"; import "../TestConfigV2_1.sol"; contract CoreActionsTests is TestConfigV2_1 { + using EnumerableMap for *; + + AddressAliasRegistry aar; + CoreActions ca; + + EnumerableMap.Bytes32ToUintMap expectedTimestamps; + 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 _computeDigest() + aar = new AddressAliasRegistry(); + ca = new CoreActions(address(aar)); + } + + struct _TestTemps { + address platform; + uint256 platformSignerPrivateKey; + address platformSigner; + address[] targetAliases; + address[][] actorAliases; + address[] targets; + address[] actors; + uint32[] timestamps; + } + + function testRegisterCoreActions(uint256) public { + _TestTemps memory t; + t.platform = _randomNonZeroAddress(); + (t.platformSigner, t.platformSignerPrivateKey) = _randomSigner(); + + vm.prank(t.platform); + ca.setPlatformSigner(t.platformSigner); + + CoreActions.CoreActionRegistrations memory rs; + rs.platform = t.platform; + rs.coreActionType = _random(); + rs.targets = _randomNonZeroAddressesGreaterThan(); + rs.actors = new address[][](rs.targets.length); + rs.timestamps = new uint32[][](rs.targets.length); + rs.nonce = _random(); + for (uint256 i; i != rs.targets.length; ++i) { + rs.actors[i] = _randomNonZeroAddressesGreaterThan(); + rs.timestamps[i] = _randomTimestamps(rs.actors[i].length); + for (uint256 j; j != rs.actors[i].length; ++j) { + bytes32 h = keccak256(abi.encodePacked(rs.targets[i], rs.actors[i][j])); + if (!expectedTimestamps.contains(h)) { + expectedTimestamps.set(h, rs.timestamps[i][j]); + } + } + } + rs.signature = _generateSignature(rs, t.platformSignerPrivateKey); + + (t.targetAliases, t.actorAliases) = ca.register(rs); + + for (uint256 i; i != rs.targets.length; ++i) { + for (uint256 j; j != rs.actors[i].length; ++j) { + uint32 timestamp = ca.getCoreActionTimestamp( + rs.platform, + rs.coreActionType, + rs.targets[i], + rs.actors[i][j] + ); + bytes32 h = keccak256(abi.encodePacked(rs.targets[i], rs.actors[i][j])); + assertEq(timestamp, expectedTimestamps.get(h)); + } + } + + uint256 actionsSum; + t.targets = LibSort.difference(rs.targets, new address[](0)); + LibSort.sort(t.targets); + LibSort.uniquifySorted(t.targets); + for (uint256 i; i != t.targets.length; ++i) { + (t.actors, t.timestamps) = ca.getCoreActions(rs.platform, rs.coreActionType, t.targets[i]); + assertEq(t.actors.length, t.timestamps.length); + actionsSum += t.actors.length; + for (uint256 j; j != t.actors.length; ++j) { + bytes32 h = keccak256(abi.encodePacked(t.targets[i], t.actors[j])); + assertEq(t.timestamps[j], expectedTimestamps.get(h)); + } + } + assertEq(actionsSum, expectedTimestamps.length()); + + vm.expectRevert(ICoreActions.InvalidSignature.selector); + ca.register(rs); + } + + function _generateSignature(CoreActions.CoreActionRegistrations memory rs, uint256 privateKey) + internal + returns (bytes memory signature) + { + bytes32 digest = ca.computeDigest(rs); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + signature = abi.encodePacked(r, s, v); + } + + 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)); + } + } + } + + function _randomTimestamps(uint256 n) internal returns (uint32[] memory a) { + a = new uint32[](n); + unchecked { + for (uint256 i; i != n; ++i) { + a[i] = uint32(_bound(_random(), 1, type(uint32).max)); + } + } + } function _hashOf(address[] memory a) internal pure returns (bytes32) { return keccak256(abi.encodePacked(a));