diff --git a/contracts/modules/CoreActions.sol b/contracts/modules/CoreActions.sol new file mode 100644 index 00000000..b57147a5 --- /dev/null +++ b/contracts/modules/CoreActions.sol @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import { ICoreActions } from "@modules/interfaces/ICoreActions.sol"; +import { IAddressAliasRegistry } from "@modules/interfaces/IAddressAliasRegistry.sol"; +import { EIP712 } from "solady/utils/EIP712.sol"; +import { LibBitmap } from "solady/utils/LibBitmap.sol"; +import { SignatureCheckerLib } from "solady/utils/SignatureCheckerLib.sol"; +import { LibMap } from "solady/utils/LibMap.sol"; +import { LibZip } from "solady/utils/LibZip.sol"; +import { LibMulticaller } from "multicaller/LibMulticaller.sol"; + +/** + * @title CoreActions + * @dev The registry for social coreActions. + */ +contract CoreActions is ICoreActions, EIP712 { + using LibBitmap for LibBitmap.Bitmap; + using LibMap for LibMap.Uint32Map; + + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev Storage struct for storing mapping from `actors` => `timestamps`. + */ + struct ActorsAndTimestamps { + // Number of entries, for enumeration. + uint256 length; + // `actorAlias` => `timestamp`. + LibMap.Uint32Map timestamps; + // `index` => `actorAlias`. + LibMap.Uint32Map actorAliases; + } + + // ============================================================= + // CONSTANTS + // ============================================================= + + /** + * @dev For EIP-712 signature digest calculation. + */ + bytes32 public constant CORE_ACTION_REGISTRATIONS_TYPEHASH = + // prettier-ignore + keccak256( + "CoreActionRegistrations(" + "uint256 coreActionType," + "address[] targets," + "address[][] actors," + "uint32[][] timestamps," + "uint256 nonce" + ")" + ); + + // ============================================================= + // IMMUTABLES + // ============================================================= + + /** + * @dev The address alias registry. + */ + address public immutable addressAliasRegistry; + + // ============================================================= + // STORAGE + // ============================================================= + + /** + * @dev Mapping of `platform` => `coreActionType` => `target` => `actorAndTimestamps`. + */ + mapping(address => mapping(uint256 => mapping(address => ActorsAndTimestamps))) internal _coreActions; + + /** + * @dev For storing the invalidated nonces. + */ + mapping(address => LibBitmap.Bitmap) internal _invalidatedNonces; + + /** + * @dev A mapping of `platform` => `platformSigner`. + */ + mapping(address => address) public platformSigner; + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor(address addressAliasRegistry_) payable { + addressAliasRegistry = addressAliasRegistry_; + } + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @inheritdoc ICoreActions + */ + function register(CoreActionRegistrations calldata r) + 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); + + // Resolve and register aliases. + unchecked { + IAddressAliasRegistry registry = IAddressAliasRegistry(addressAliasRegistry); + (resolvedTargets, targetAliases) = registry.resolveAndRegister(r.targets); + for (uint256 i; i != n; ++i) { + (resolvedActors[i], actorAliases[i]) = registry.resolveAndRegister(r.actors[i]); + } + } + + // Check the signature and invalidate the nonce. + { + bytes32 digest = _computeDigest(r, resolvedTargets, resolvedActors); + + address signer = platformSigner[r.platform]; + if (!SignatureCheckerLib.isValidSignatureNowCalldata(signer, digest, r.signature)) + revert InvalidSignature(); + if (!_invalidatedNonces[signer].toggle(r.nonce)) revert InvalidSignature(); + + uint256[] memory nonces = new uint256[](1); + nonces[0] = r.nonce; + emit NoncesInvalidated(signer, nonces); + } + + // Store and emit events. + unchecked { + for (uint256 i; i != n; ++i) { + address target = resolvedTargets[i]; + ActorsAndTimestamps storage m = _coreActions[r.platform][r.coreActionType][target]; + for (uint256 j; j != resolvedActors[i].length; ++j) { + uint32 actorAlias = uint32(uint160(actorAliases[i][j])); + if (m.timestamps.get(uint256(actorAlias)) == 0) { + uint32 timestamp = r.timestamps[i][j]; + if (timestamp == 0) revert TimestampIsZero(); + m.timestamps.set(actorAlias, timestamp); + m.actorAliases.set(m.length++, actorAlias); + emit Interacted(r.platform, r.coreActionType, target, resolvedActors[i][j], timestamp); + } + } + } + } + } + + /** + * @inheritdoc ICoreActions + */ + function invalidateNonces(uint256[] calldata nonces) external { + unchecked { + address sender = LibMulticaller.sender(); + LibBitmap.Bitmap storage s = _invalidatedNonces[sender]; + for (uint256 i; i != nonces.length; ++i) { + s.set(nonces[i]); + } + emit NoncesInvalidated(sender, nonces); + } + } + + /** + * @inheritdoc ICoreActions + */ + function setPlatformSigner(address signer) public { + address sender = LibMulticaller.senderOrSigner(); + platformSigner[sender] = signer; + emit PlatformSignerSet(sender, signer); + } + + // 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 ICoreActions + */ + function noncesInvalidated(address signer, uint256[] calldata nonces) public view returns (bool[] memory result) { + unchecked { + result = new bool[](nonces.length); + LibBitmap.Bitmap storage s = _invalidatedNonces[signer]; + for (uint256 i; i != nonces.length; ++i) { + result[i] = s.get(nonces[i]); + } + } + } + + /** + * @inheritdoc ICoreActions + */ + function getCoreActionTimestamp( + address platform, + uint256 coreActionType, + address target, + address actor + ) public view returns (uint32) { + target = IAddressAliasRegistry(addressAliasRegistry).addressOf(target); + ActorsAndTimestamps storage m = _coreActions[platform][coreActionType][target]; + uint256 actorAlias = uint160(IAddressAliasRegistry(addressAliasRegistry).aliasOf(actor)); + return m.timestamps.get(actorAlias); + } + + /** + * @inheritdoc ICoreActions + */ + function numCoreActions( + address platform, + uint256 coreActionType, + address target + ) public view returns (uint256) { + target = IAddressAliasRegistry(addressAliasRegistry).addressOf(target); + return _coreActions[platform][coreActionType][target].length; + } + + /** + * @inheritdoc ICoreActions + */ + function getCoreActions( + address platform, + uint256 coreActionType, + address target + ) public view returns (address[] memory actors, uint32[] memory timestamps) { + return getCoreActionsIn(platform, coreActionType, target, 0, type(uint256).max); + } + + /** + * @inheritdoc ICoreActions + */ + function getCoreActionsIn( + address platform, + uint256 coreActionType, + address target, + uint256 start, + uint256 stop + ) public view returns (address[] memory actors, uint32[] memory timestamps) { + target = IAddressAliasRegistry(addressAliasRegistry).addressOf(target); + ActorsAndTimestamps storage m = _coreActions[platform][coreActionType][target]; + unchecked { + uint256 n = m.length; + if (stop > n) stop = n; + uint256 l = stop - start; + if (start > stop) revert InvalidQueryRange(); + actors = new address[](l); + timestamps = new uint32[](l); + IAddressAliasRegistry registry = IAddressAliasRegistry(addressAliasRegistry); + for (uint256 i; i != l; ++i) { + uint32 actorAlias = m.actorAliases.get(start + i); + actors[i] = registry.addressOf(address(uint160(actorAlias))); + timestamps[i] = m.timestamps.get(actorAlias); + } + } + } + + /** + * @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. + * @return version_ The EIP-712 version. + */ + function _domainNameAndVersion() + internal + pure + virtual + override + returns (string memory name_, string memory version_) + { + name_ = "CoreActions"; + 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. + * @return result The hash. + */ + function _hashOf(address[] memory a) internal pure returns (bytes32 result) { + assembly { + result := keccak256(add(0x20, a), shl(5, mload(a))) + } + } + + /** + * @dev Returns the hash of `a`. + * @param a The input to hash. + * @return result The hash. + */ + function _hashOf(address[][] memory a) internal pure returns (bytes32 result) { + assembly { + let m := mload(0x40) + let n := shl(5, mload(a)) + // prettier-ignore + for { let i := 0 } iszero(eq(i, n)) { i := add(i, 0x20) } { + let o := mload(add(add(a, 0x20), i)) + mstore(add(m, i), keccak256(add(0x20, o), shl(5, mload(o)))) + } + result := keccak256(m, n) + } + } + + /** + * @dev Returns the hash of `a`. + * @param a The input to hash. + * @return result The hash. + */ + function _hashOf(uint32[][] calldata a) internal pure returns (bytes32 result) { + assembly { + let m := mload(0x40) + let n := shl(5, a.length) + // prettier-ignore + for { let i := 0 } iszero(eq(i, n)) { i := add(i, 0x20) } { + let o := add(a.offset, calldataload(add(a.offset, i))) + let p := add(m, i) + calldatacopy(p, add(o, 0x20), shl(5, calldataload(o))) + mstore(p, keccak256(p, shl(5, calldataload(o)))) + } + result := keccak256(m, n) + } + } +} diff --git a/contracts/modules/interfaces/ICoreActions.sol b/contracts/modules/interfaces/ICoreActions.sol new file mode 100644 index 00000000..7f97e135 --- /dev/null +++ b/contracts/modules/interfaces/ICoreActions.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +/** + * @title CoreActions + * @dev The registry for social core actions. + */ +interface ICoreActions { + // ============================================================= + // STRUCTS + // ============================================================= + + /** + * @dev A struct containing the arguments for registering core actions. + */ + struct CoreActionRegistrations { + // The platform. + address platform; + // The core action type. + uint256 coreActionType; + // The list of targets. + // Can be full addresses or aliases. + address[] targets; + // The list of lists of timestamps. + // Can be full addresses or aliases. + // Must have the same dimensions as `actors`. + address[][] actors; + // The list of lists of timestamps. + // Must have the same dimensions as `actors`. + uint32[][] timestamps; + // The nonce of the signature (per platform's signer). + uint256 nonce; + // A signature by the current `platform` signer to authorize registration. + bytes signature; + } + + // ============================================================= + // EVENTS + // ============================================================= + + /** + * @dev Emitted when `actor` performs a core action with `target`, + * of `coreActionType`, on `platform`, at `timestamp`. + * @param platform The platform address. + * @param coreActionType The core action type. + * @param target The core action target. + * @param actor The core action actor. + * @param timestamp The core action timestamp. + */ + event Interacted( + address indexed platform, + uint256 coreActionType, + address indexed target, + address indexed actor, + uint32 timestamp + ); + + /** + * @dev Emitted when the `nonces` of `signer` are invalidated. + * @param signer The signer of the nonces. + * @param nonces The nonces. + */ + event NoncesInvalidated(address indexed signer, uint256[] nonces); + + /** + * @dev Emitted when the signer for a platform is set. + * @param platform The platform address. + * @param signer The signer for the platform. + */ + event PlatformSignerSet(address indexed platform, address signer); + + // ============================================================= + // ERRORS + // ============================================================= + + /** + * @dev The signature is invalid. + */ + error InvalidSignature(); + + /** + * @dev The length of the input arrays must be the same. + */ + error ArrayLengthsMismatch(); + + /** + * @dev The timestamp cannot be zero. + */ + error TimestampIsZero(); + + /** + * @dev The query range exceeds the bounds. + */ + error InvalidQueryRange(); + + // ============================================================= + // PUBLIC / EXTERNAL WRITE FUNCTIONS + // ============================================================= + + /** + * @dev Registers a batch of core actions. + * @param r The core actions to register. + * @return targetAliases A list of aliases corresponding to `targets`. + * @return actorAliases A list of aliases corresponding to `actors`. + */ + function register(CoreActionRegistrations memory r) + external + returns (address[] memory targetAliases, address[][] memory actorAliases); + + /** + * @dev Allows the platform to set their signer. + * @param signer The signer for the platform. + */ + function setPlatformSigner(address signer) external; + + /** + * @dev Invalidates the nonces for the `msg.sender`. + * @param nonces The nonces. + */ + function invalidateNonces(uint256[] calldata nonces) external; + + // ============================================================= + // PUBLIC / EXTERNAL VIEW FUNCTIONS + // ============================================================= + + /** + * @dev Returns the CoreActionRegistrations struct's EIP-712 typehash. + * @return The constant value. + */ + 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. + * @return The configured value. + */ + function platformSigner(address platform) external view returns (address); + + /** + * @dev Returns whether each of the `nonces` of `signer` has been invalidated. + * @param signer The signer of the signature. + * @param nonces An array of nonces. + * @return A bool array representing whether each nonce has been invalidated. + */ + function noncesInvalidated(address signer, uint256[] calldata nonces) external view returns (bool[] memory); + + /** + * @dev Returns the core action timestamp of `actor` on target`, + * of `coreActionType`, on `platform`. + * @param platform The platform. + * @param coreActionType The core action type. + * @param target The core action target. + * @param actor The actor + * @return The timestamp value. + */ + function getCoreActionTimestamp( + address platform, + uint256 coreActionType, + address target, + address actor + ) external view returns (uint32); + + /** + * @dev Returns the number of core actions on `target`, + * of `coreActionType` on `platform`. + * @param platform The platform. + * @param coreActionType The core action type. + * @param target The core action target. + * @return The latest value. + */ + function numCoreActions( + address platform, + uint256 coreActionType, + address target + ) external view returns (uint256); + + /** + * @dev Returns the list of `actors` and `timestamps` + * for coreActions on `target`, of `coreActionType`, on `platform`. + * @param platform The platform. + * @param coreActionType The core action type. + * @param target The core action target. + * @return actors The actors for the core actions. + * @return timestamps The timestamps of the core actions. + */ + function getCoreActions( + address platform, + uint256 coreActionType, + address target + ) external view returns (address[] memory actors, uint32[] memory timestamps); + + /** + * @dev Returns the list of `actors` and `timestamps` + * for coreActions on `target`, of `coreActionType`, on `platform`. + * @param platform The platform. + * @param coreActionType The core action type. + * @param target The core action target. + * @param start The start index of the range. + * @param stop The end index of the range (exclusive). + * @return actors The actors for the core actions. + * @return timestamps The timestamps of the core actions. + */ + function getCoreActionsIn( + address platform, + uint256 coreActionType, + address target, + uint256 start, + uint256 stop + ) external view returns (address[] memory actors, uint32[] memory timestamps); +} diff --git a/tests/modules/CoreActions.t.sol b/tests/modules/CoreActions.t.sol new file mode 100644 index 00000000..32a2c4a2 --- /dev/null +++ b/tests/modules/CoreActions.t.sol @@ -0,0 +1,158 @@ +pragma solidity ^0.8.16; + +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(); + 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)); + } + + function _hashOf(address[][] memory a) internal pure returns (bytes32) { + uint256 n = a.length; + bytes32[] memory encoded = new bytes32[](n); + for (uint256 i = 0; i != n; ++i) { + encoded[i] = keccak256(abi.encodePacked(a[i])); + } + return keccak256(abi.encodePacked(encoded)); + } + + function _hashOf(uint256[][] calldata a) internal pure returns (bytes32) { + uint256 n = a.length; + bytes32[] memory encoded = new bytes32[](n); + for (uint256 i = 0; i != n; ++i) { + encoded[i] = keccak256(abi.encodePacked(a[i])); + } + return keccak256(abi.encodePacked(encoded)); + } +}