From 48ae9ebec52c8ab1c345afbd68f832ccc5e2ff6a Mon Sep 17 00:00:00 2001 From: 0age <37939117+0age@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:09:02 -0700 Subject: [PATCH] add batch (one failing test) --- foundry.toml | 3 +- src/TheCompact.sol | 140 +++++++++++++++++++++++++++++++++-- src/interfaces/IOracle.sol | 6 ++ src/lib/EfficiencyLib.sol | 38 ++++++++++ src/lib/IdLib.sol | 118 +++++------------------------ src/lib/MetadataLib.sol | 105 ++++++++++++++++++++++++++ src/lib/MetadataRenderer.sol | 13 ++++ src/types/EIP712Types.sol | 35 +++++++++ test/TheCompact.t.sol | 140 ++++++++++++++++++++++++++++++++++- 9 files changed, 491 insertions(+), 107 deletions(-) create mode 100644 src/lib/EfficiencyLib.sol create mode 100644 src/lib/MetadataLib.sol create mode 100644 src/lib/MetadataRenderer.sol diff --git a/foundry.toml b/foundry.toml index 0e51f5d..6ab4400 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,8 +2,7 @@ solc = '0.8.27' evm_version='cancun' via_ir = true -# optimizer_runs = 4_294_967_295 # turn this back on after making the compact more compact -optimizer_runs = 200 +optimizer_runs = 4_294_967_295 bytecode_hash = 'none' src = "src" out = "out" diff --git a/src/TheCompact.sol b/src/TheCompact.sol index 2788889..1aef2d6 100644 --- a/src/TheCompact.sol +++ b/src/TheCompact.sol @@ -4,6 +4,8 @@ pragma solidity ^0.8.27; import { Lock } from "./types/Lock.sol"; import { IdLib } from "./lib/IdLib.sol"; import { ConsumerLib } from "./lib/ConsumerLib.sol"; +import { EfficiencyLib } from "./lib/EfficiencyLib.sol"; +import { MetadataLib } from "./lib/MetadataLib.sol"; import { ERC6909 } from "solady/tokens/ERC6909.sol"; import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol"; import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol"; @@ -13,8 +15,12 @@ import { ISignatureTransfer } from "permit2/src/interfaces/ISignatureTransfer.so import { Allocation, AllocationAuthorization, + BatchAllocation, + BatchAllocationAuthorization, ALLOCATION_TYPEHASH, ALLOCATION_AUTHORIZATION_TYPEHASH, + BATCH_ALLOCATION_TYPEHASH, + BATCH_ALLOCATION_AUTHORIZATION_TYPEHASH, TRANSFER_AUTHORIZATION_TYPEHASH, DELEGATED_TRANSFER_TYPEHASH, WITHDRAWAL_AUTHORIZATION_TYPEHASH, @@ -22,6 +28,7 @@ import { } from "./types/EIP712Types.sol"; import { IOracle } from "./interfaces/IOracle.sol"; import { IAllocator } from "./interfaces/IAllocator.sol"; +import { MetadataRenderer } from "./lib/MetadataRenderer.sol"; /** * @title The Compact @@ -35,10 +42,13 @@ contract TheCompact is ERC6909 { using IdLib for uint256; using IdLib for address; using IdLib for Lock; + using MetadataLib for address; using ConsumerLib for uint256; using SafeTransferLib for address; using SignatureCheckerLib for address; using FixedPointMathLib for uint256; + using EfficiencyLib for bool; + using EfficiencyLib for uint256; event Deposit( address indexed depositor, @@ -48,8 +58,8 @@ contract TheCompact is ERC6909 { ); event Claim( address indexed provider, - address indexed allocator, address indexed claimant, + uint256 indexed id, bytes32 allocationHash, uint256 claimAmount ); @@ -76,6 +86,7 @@ contract TheCompact is ERC6909 { error InvalidAmountReduction(uint256 amount, uint256 amountReduction); error UnallocatedTransfer(address from, address to, uint256 id, uint256 amount); error CallerNotClaimant(); + error InvalidBatchAllocation(); IPermit2 private constant _PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3); @@ -106,12 +117,14 @@ contract TheCompact is ERC6909 { uint256 private immutable _INITIAL_CHAIN_ID; bytes32 private immutable _INITIAL_DOMAIN_SEPARATOR; + MetadataRenderer private immutable _METADATA_RENDERER; constructor() { _INITIAL_CHAIN_ID = block.chainid; _INITIAL_DOMAIN_SEPARATOR = keccak256( abi.encode(_DOMAIN_TYPEHASH, _NAME_HASH, _VERSION_HASH, block.chainid, address(this)) ); + _METADATA_RENDERER = new MetadataRenderer(); } /// @dev Returns the name for the contract. @@ -136,7 +149,7 @@ contract TheCompact is ERC6909 { /// @dev Returns the Uniform Resource Identifier (URI) for token `id`. function tokenURI(uint256 id) public view virtual override returns (string memory) { - return id.toURI(); + return _METADATA_RENDERER.uri(id.toLock(), id); } function deposit(address allocator, uint48 resetPeriod, address recipient) @@ -230,6 +243,32 @@ contract TheCompact is ERC6909 { _release(allocation.owner, claimant, allocation.id, claimAmount); } + function claim( + BatchAllocation calldata batchAllocation, + BatchAllocationAuthorization calldata batchAllocationAuthorization, + bytes calldata oracleVariableData, + bytes calldata ownerSignature, + bytes calldata allocatorSignature + ) external returns (address claimant, uint256[] memory claimAmounts) { + (claimant, claimAmounts) = _processBatchClaim( + batchAllocation, + batchAllocationAuthorization, + oracleVariableData, + ownerSignature, + allocatorSignature + ); + + + uint256 totalIds = batchAllocation.ids.length; + address owner = batchAllocation.owner; + unchecked { + for (uint256 i = 0; i < totalIds; ++i) { + // TODO: skip bounds checks on array accesses + _release(owner, claimant, batchAllocation.ids[i], claimAmounts[i]); + } + } + } + // Note: this can be frontrun since anyone can call claim function claimAndWithdraw( Allocation calldata allocation, @@ -290,7 +329,61 @@ contract TheCompact is ERC6909 { oracleClaimAmount.min(allocation.amount - allocationAuthorization.amountReduction); } - emit Claim(allocation.owner, allocator, claimant, allocationMessageHash, claimAmount); + emit Claim(allocation.owner, claimant, allocation.id, allocationMessageHash, claimAmount); + } + + function _processBatchClaim( + BatchAllocation calldata batchAllocation, + BatchAllocationAuthorization calldata batchAllocationAuthorization, + bytes calldata oracleVariableData, + bytes calldata ownerSignature, + bytes calldata allocatorSignature + ) internal returns (address claimant, uint256[] memory claimAmounts) { + _assertValidTime(batchAllocation.startTime, batchAllocation.endTime); + _assertValidTime(batchAllocationAuthorization.startTime, batchAllocationAuthorization.endTime); + + uint256 totalIds = batchAllocation.ids.length; + if ( + (totalIds == 0).or(totalIds != batchAllocation.amounts.length).or(totalIds != batchAllocationAuthorization.amountReductions.length) + ) { + revert InvalidBatchAllocation(); + } + + // TODO: skip the bounds check on this array access + uint256 allocatorIndex = batchAllocation.ids[0].toAllocatorIndex(); + claimAmounts = new uint256[](totalIds); + + address allocator = allocatorIndex.toRegisteredAllocator(); + batchAllocation.nonce.consumeNonce(allocator); + bytes32 batchAllocationMessageHash = _getBatchAllocationMessageHash(batchAllocation); + _assertValidSignature(batchAllocationMessageHash, ownerSignature, batchAllocation.owner); + bytes32 batchAllocationAuthorizationMessageHash = + _getBatchAllocationAuthorizationMessageHash(batchAllocationAuthorization, batchAllocationMessageHash); + _assertValidSignature(batchAllocationAuthorizationMessageHash, allocatorSignature, allocator); + (address oracleClaimant, uint256[] memory oracleClaimAmounts) = IOracle(batchAllocation.oracle).attestBatch( + batchAllocationMessageHash, batchAllocation.oracleFixedData, oracleVariableData + ); + + claimant = + _deriveClaimant(batchAllocation.claimant, batchAllocationAuthorization.claimant, oracleClaimant); + + // TODO: many of the bounds checks on these array accesses can be skipped as an optimization + uint256 errorBuffer = (batchAllocationAuthorization.amountReductions[0] >= batchAllocation.amounts[0]).or(totalIds != oracleClaimAmounts.length).asUint256(); + unchecked { + for (uint256 i = 1; i < totalIds; ++i) { + uint256 id = batchAllocation.ids[i]; + uint256 originalAmount = batchAllocation.amounts[i]; + uint256 amountReduction = batchAllocationAuthorization.amountReductions[i]; + errorBuffer |= (amountReduction >= originalAmount).or(id.toAllocatorIndex() != allocatorIndex).asUint256(); + uint256 claimAmount = oracleClaimAmounts[i].min(originalAmount - amountReduction); + claimAmounts[i] = claimAmount; + emit Claim(batchAllocation.owner, claimant, id, batchAllocationMessageHash, claimAmount); + } + } + if (errorBuffer.asBool()) { + // TODO: extract more informative error by deriving the reason for the failure + revert InvalidBatchAllocation(); + } } function enableForcedWithdrawal(uint256 id) external returns (uint256 withdrawableAt) { @@ -317,7 +410,7 @@ contract TheCompact is ERC6909 { { uint256 withdrawableAt = cutoffTime[msg.sender][id]; - if (withdrawableAt == 0 || withdrawableAt > block.timestamp) { + if ((withdrawableAt == 0).or(withdrawableAt > block.timestamp)) { revert PrematureWithdrawal(id); } @@ -440,7 +533,7 @@ contract TheCompact is ERC6909 { } function getAllocatorByIndex(uint256 index) external view returns (address) { - return index.registeredAllocatorByIndex(); + return index.toRegisteredAllocator(); } function getAllocatorIndex(address allocator) external view returns (uint256) { @@ -732,6 +825,27 @@ contract TheCompact is ERC6909 { ); } + function _getBatchAllocationMessageHash(BatchAllocation memory batchAllocation) + internal + pure + returns (bytes32 messageHash) + { + messageHash = keccak256( + abi.encode( + BATCH_ALLOCATION_TYPEHASH, + batchAllocation.owner, + batchAllocation.startTime, + batchAllocation.endTime, + batchAllocation.nonce, + keccak256(abi.encode(batchAllocation.ids)), + keccak256(abi.encode(batchAllocation.amounts)), + batchAllocation.claimant, + batchAllocation.oracle, + keccak256(batchAllocation.oracleFixedData) + ) + ); + } + function _getAllocationAuthorizationMessageHash( AllocationAuthorization memory allocationAuthorization, bytes32 allocationMessageHash @@ -748,6 +862,22 @@ contract TheCompact is ERC6909 { ); } + function _getBatchAllocationAuthorizationMessageHash( + BatchAllocationAuthorization memory batchAllocationAuthorization, + bytes32 batchAllocationMessageHash + ) internal pure returns (bytes32 messageHash) { + messageHash = keccak256( + abi.encode( + BATCH_ALLOCATION_AUTHORIZATION_TYPEHASH, + batchAllocationMessageHash, + batchAllocationAuthorization.startTime, + batchAllocationAuthorization.endTime, + batchAllocationAuthorization.claimant, + keccak256(abi.encode(batchAllocationAuthorization.amountReductions)) + ) + ); + } + function _getAuthorizedTransferMessageHash( uint256 expiration, uint256 nonce, diff --git a/src/interfaces/IOracle.sol b/src/interfaces/IOracle.sol index d3e7a11..61d8568 100644 --- a/src/interfaces/IOracle.sol +++ b/src/interfaces/IOracle.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.27; interface IOracle { + // Called on claims referencing a single allocated token. function attest(bytes32 allocationHash, bytes calldata fixedData, bytes calldata variableData) external returns (address claimant, uint256 claimAmount); + + // Called on claims referencing an array of allocated tokens. + function attestBatch(bytes32 allocationHash, bytes calldata fixedData, bytes calldata variableData) + external + returns (address claimant, uint256[] memory claimAmounts); } diff --git a/src/lib/EfficiencyLib.sol b/src/lib/EfficiencyLib.sol new file mode 100644 index 0000000..3de4c4e --- /dev/null +++ b/src/lib/EfficiencyLib.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +library EfficiencyLib { + // NOTE: this function is only safe if the supplied booleans are known to not + // have any dirty bits set (i.e. they are either 0 or 1). It is meant to get + // around the fact that solidity only evaluates both expressions in an && if + // the first expression evaluates to true, which requires a conditional jump. + function and(bool a, bool b) internal pure returns (bool c) { + assembly { + c := and(a, b) + } + } + + // NOTE: this function is only safe if the supplied booleans are known to not + // have any dirty bits set (i.e. they are either 0 or 1). It is meant to get + // around the fact that solidity only evaluates both expressions in an || if + // the first expression evaluates to false, which requires a conditional jump. + function or(bool a, bool b) internal pure returns (bool c) { + assembly { + c := or(a, b) + } + } + + // NOTE: this function is only safe if the supplied uint256 is known to not + // have any dirty bits set (i.e. it is either 0 or 1). + function asBool(uint256 a) internal pure returns (bool b) { + assembly { + b := a + } + } + + function asUint256(bool a) internal pure returns (uint256 b) { + assembly { + b := a + } + } +} \ No newline at end of file diff --git a/src/lib/IdLib.sol b/src/lib/IdLib.sol index 32c5cce..eacc5d0 100644 --- a/src/lib/IdLib.sol +++ b/src/lib/IdLib.sol @@ -2,14 +2,12 @@ pragma solidity ^0.8.27; import { Lock } from "../types/Lock.sol"; -import { LibString } from "solady/utils/LibString.sol"; -import { MetadataReaderLib } from "solady/utils/MetadataReaderLib.sol"; +import { MetadataLib } from "./MetadataLib.sol"; library IdLib { - using LibString for uint256; - using LibString for address; - using MetadataReaderLib for address; using IdLib for address; + using IdLib for uint256; + using MetadataLib for Lock; event AllocatorRegistered(uint256 index, address allocator); @@ -30,97 +28,12 @@ library IdLib { return (id << 0x30) >> 0xd0; } - function toAllocator(uint256 id) internal view returns (address allocator) { - allocator = registeredAllocatorByIndex(id >> 0xd0); - } - - function toURI(uint256 id) internal view returns (string memory uri) { - Lock memory lock = toLock(id); - string memory tokenAddress = - lock.token == address(0) ? "Native Token" : lock.token.toHexStringChecksummed(); - string memory allocator = lock.allocator.toHexStringChecksummed(); - string memory resetPeriod = string.concat(lock.resetPeriod.toString(), " seconds"); - string memory tokenName = lock.token.readNameWithDefaultValue(); - string memory tokenSymbol = lock.token.readSymbolWithDefaultValue(); - string memory tokenDecimals = uint256(lock.token.readDecimals()).toString(); - - string memory name = string.concat("{\"name\": \"Compact ", tokenSymbol, "\","); - string memory description = string.concat( - "\"description\": \"Compact ", - tokenName, - " (", - tokenAddress, - ") with allocator ", - allocator, - " and reset period of ", - resetPeriod, - "\"," - ); - string memory attributes = string.concat( - "\"attributes\": [", - toAttributeString("Token Address", tokenAddress, false), - toAttributeString("Token Name", tokenName, false), - toAttributeString("Token Symbol", tokenSymbol, false), - toAttributeString("Token Decimals", tokenDecimals, false), - toAttributeString("Allocator", allocator, false), - toAttributeString("Reset Period", resetPeriod, true), - "]}" - ); - - // Note: this just returns a default image; replace with a dynamic image based on attributes - string memory image = - "\"image\": \"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuNSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDkyIDQ5MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDkyIDQ5MjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgaWQ9Ik1hc3Rlcl9MYXllcl8yIj4NCjwvZz4NCjxnIGlkPSJMYXllcl8xIj4NCgk8Zz4NCgkJPGc+DQoJCQk8Zz4NCgkJCQk8Zz4NCgkJCQkJPHBhdGggc3R5bGU9ImZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIxMjEyMTsiIGQ9Ik0zMjEuMzA4LDI5NC44NjRjNS4zNTIsNS4zMjgsOS40NTYsMTIuMTQ0LDE1Ljc5Miw4LjgzMg0KCQkJCQkJYzIuNDQ4LTEuMjcyLDUuMDY0LTMuMDk2LDcuMzItNS4yNTZjMy43NDQtMy41NzYsOC4yNTYtOS41MjgsNC42NTYtMTQuMjhjLTEyLjQ1Ni0xMS45NzYtMzYuMzg0LTMyLjExMi0zNi40NTYtMzIuMTYNCgkJCQkJCWw3LjU2LTguNTY4YzAuMDI0LDAuMDI0LDUuMTYsNC41MzYsMTEuODMyLDEwLjgyNGM4LjY4OCw4LjIwOCwyMC44NTYsMTYuMiwyNi43MzYsMjQuNDA4DQoJCQkJCQljMy4zMTIsNC42MDgsMi42MTYsMTIuNzQ0LDAuODY0LDE3LjUyYy0xLjM5MiwzLjg0LTQuMTA0LDcuNDY0LTcuMzIsMTAuNTM2Yy0zLjAyNCwyLjkwNC02LjYsNS40LTkuOTYsNy4xMjgNCgkJCQkJCWMtMy4zODQsMS43NTItNi43OTIsMi43Ni05LjY5NiwyLjc4NGMtMC4wOTYsMC40NTYtMC4yMTYsMC45MzYtMC4zMzYsMS4zOTJjLTAuOTYsMy4yNC0zLjAyNCw2LjA3Mi01LjYxNiw4LjQNCgkJCQkJCWMtMi4zMjgsMi4wODgtNS4xMzYsMy44MTYtNy45NDQsNS4wNjRjLTMuMDcyLDEuMzQ0LTYuMjg4LDIuMTEyLTkuMTY4LDIuMTZjLTAuMDk2LDAuOTM2LTAuMjg4LDEuODQ4LTAuNTUyLDIuNzYNCgkJCQkJCWMtMC45NiwzLjI0LTMuMDI0LDYuMDcyLTUuNjE2LDguNGMtMi4zMjgsMi4wODgtNS4xMzYsMy44MTYtNy45NDQsNS4wNjRjLTQuMTI4LDEuODI0LTguNTQ0LDIuNTY4LTEyLDEuOTY4DQoJCQkJCQljLTAuMTIsMS4yMjQtMC4zNiwyLjQtMC42OTYsMy41MDR2MC4wMjRjLTEuMDMyLDMuMzg0LTMsNi4yNC01LjUyLDguMzUyYy0yLjUyLDIuMTEyLTUuNTkyLDMuNDgtOC44NTYsMy45MzYNCgkJCQkJCWMtMy45NiwwLjU1Mi04LjE2LTAuMjQtMTEuOTA0LTIuNjg4Yy0xLjAzMi0wLjY3Mi0yLjE2LTEuNTM2LTMuNDgtMi41OTJsLTAuNzQ0LTAuNTc2bC0xMS4xNi04LjYxNmw2Ljk2LTkuMDI0bDExLjE2LDguNjE2DQoJCQkJCQlsMC43NDQsMC41NzZjMS4wMzIsMC43OTIsMS44OTYsMS40ODgsMi43ODQsMi4wNGMxLjI5NiwwLjg2NCwyLjczNiwxLjEyOCw0LjA4LDAuOTZjMS4xMjgtMC4xNjgsMi4xODQtMC42NDgsMy4wNzItMS4zOTINCgkJCQkJCWMwLjg2NC0wLjcyLDEuNTYtMS43MjgsMS45Mi0yLjkwNGwwLDBjMC40NTYtMS41NiwwLjM4NC0zLjUwNC0wLjQ1Ni01Ljc2Yy05LjUyOC0xMy4yOTYtMjkuNDQ4LTI5LjQyNC0yOS40OTYtMjkuNDcyDQoJCQkJCQlsNy4yLTguODU2YzAuMDQ4LDAuMDI0LDguMTEyLDYuNTc2LDE2Ljc1MiwxNS4wMjRjMi4zMDQsMi4yNTYsNC44NDgsNC43NTIsNy41MTIsNy4xMjhjMC40OCwwLjQzMiwwLjk4NCwwLjg2NCwxLjQ2NCwxLjI5Ng0KCQkJCQkJbDAsMGwwLDBjMC4wOTYsMC4wOTYsMC4yMTYsMC4xOTIsMC4zMTIsMC4yODhjMC42MjQsMC41NTIsMS4yNDgsMS4xMjgsMS44NzIsMS43MDRjMi4xMTIsMS44OTYsNC4yLDMuODE2LDYuMzg0LDUuNDk2DQoJCQkJCQljMi41OTIsMS44NDgsMi41NDQsMi4yMzIsNS40OTYsMS4zNDRjMC42MjQtMC4xOTIsMS4yOTYtMC41MjgsMi4wMTYtMC44NGMxLjc3Ni0wLjc2OCwzLjUwNC0xLjg0OCw0Ljg5Ni0zLjA5Ng0KCQkJCQkJYzEuMTI4LTEuMDMyLDEuOTkyLTIuMTEyLDIuMzA0LTMuMTY4YzAuMjQtMC44NCwwLjA3Mi0xLjg0OC0wLjc0NC0yLjk3NmMtOS41NzYtMTMuMzItMzUuOTA0LTM2LjQ1Ni0zNS45NzYtMzYuNTI4bDcuNTYtOC41NjgNCgkJCQkJCWMwLjA0OCwwLjA0OCwxNC42ODgsMTIuOTEyLDI2LjYxNiwyNS40ODhjMy4yNCwzLjE5Miw4LjA2NCw3LjU2LDExLjU0NCwxMC4yNzJjMS4yNzIsMC45MTIsMi4xNiwyLjA4OCw0LjA4LDEuNDE2DQoJCQkJCQljMC44MTYtMC4yODgsMS44NDgtMC42OTYsMy0xLjJjMS43NzYtMC43NjgsMy41MDQtMS44NDgsNC44OTYtMy4wOTZjMS4xMjgtMS4wMDgsMS45OTItMi4xMTIsMi4zMDQtMy4xNjgNCgkJCQkJCWMwLjI0LTAuODQsMC4wNzItMS44NDgtMC43NDQtM2MtOS41NzYtMTMuMzItMzUuOTA0LTM2LjQ1Ni0zNS45NzYtMzYuNTI4bDcuNTYtOC41NjgNCgkJCQkJCUMyOTIuMjIsMjY2LjY4OCwzMDkuMDQ0LDI4MS40OTYsMzIxLjMwOCwyOTQuODY0eiIvPg0KCQkJCTwvZz4NCgkJCQk8Zz4NCgkJCQkJPHBhdGggc3R5bGU9ImZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIxMjEyMTsiIGQ9Ik00MjkuMDIsMjU0LjQyNEwzOTMuNjkyLDEyOS43MmwtMS41MzYtNS40NDhsLTUuNDQ4LDEuNDg4DQoJCQkJCQlsLTQ1LjIxNiwxMi40MDhsLTUuNTY4LDEuNTM2bDEuNTYsNS41MmwyLjEzNiw3LjUzNmMtMjEuNjk2LDEuOTY4LTQyLjg0LTIuNjY0LTYyLjU2OC02Ljk2DQoJCQkJCQljLTM5LjI2NC04LjU2OC03My4yOTYtMTUuOTg0LTk5LjU3NiwyNS44OTZsMCwwYy03LjEwNCwxMS4zNTItMTQuODU2LDI0Ljg0LTE2LjY1NiwzNS4xNg0KCQkJCQkJYy0yLjQ3MiwxNC4wNCwzLjAyNCwyMy4wNCwyNS4yNDgsMTguOTZjMTMuNjU2LTIuNDk2LDIyLjA4LTkuMzYsMjkuOTI4LTE1Ljc2OGM4Ljg4LTcuMjQ4LDE2Ljg3Mi0xMy43NTIsMzIuMzc2LTkuMTQ0DQoJCQkJCQljOC4xMzYsMy4zNiw4Ljg4LDMuNjcyLDE1LjI0LDkuMDI0YzIxLjE0NCwxNy43MzYsNzEuNCw2MS41MzYsNzIsNjIuMDRsMCwwbDEwLjQxNiw5LjE2OGwyLjkwNCwyLjU0NGwzLjQzMi0xLjc1Mg0KCQkJCQkJbDIwLjg4LTEwLjYwOGwxLjI3Miw0LjQ4OGw1LjQ3Mi0xLjU2bDQ1LjA5Ni0xMi43NjhsNS40OTYtMS41Nkw0MjkuMDIsMjU0LjQyNEw0MjkuMDIsMjU0LjQyNHogTTM1MC42MzYsMjY5Ljk3NmwtNy41MTItNi42DQoJCQkJCQlIMzQzLjFjLTAuMTQ0LTAuMTItNTEuNjI0LTQ1LTcyLjE5Mi02Mi4yMzJjLTcuNzA0LTYuNDU2LTguNTY4LTYuODE2LTE4LjM2LTEwLjg3MmwtMC4yNC0wLjA5NmwtMC41MjgtMC4xOTINCgkJCQkJCWMtMjEuMzYtNi40NTYtMzEuNjA4LDEuOTItNDIuOTg0LDExLjIwOGMtNi43NjgsNS41Mi0xMy45OTIsMTEuNDI0LTI0Ljc2OCwxMy4zOTJjLTEwLjA4LDEuODQ4LTEyLjc2OC0xLjAzMi0xMS45MjgtNS43ODQNCgkJCQkJCWMxLjQ4OC04LjQ3Miw4LjU0NC0yMC42NjQsMTUuMDQ4LTMxLjA1NnYtMC4wMjRjMjEuOTYtMzUuMDY0LDUyLjM5Mi0yOC40NCw4Ny40OC0yMC43ODQNCgkJCQkJCWMyMS4xOTIsNC42MDgsNDMuOTQ0LDkuNTc2LDY4LjE2LDYuOTM2bDI3LjI2NCw5Ni4yNEwzNTAuNjM2LDI2OS45NzZMMzUwLjYzNiwyNjkuOTc2eiBNMzgyLjM2NCwyNjEuNjk2TDM1MC4wNiwxNDcuNjI1DQoJCQkJCQlsMzQuMi05LjM4NGwzMi4yMzIsMTEzLjc4NEwzODIuMzY0LDI2MS42OTZ6Ii8+DQoJCQkJPC9nPg0KCQkJCTxnPg0KCQkJCQk8cGF0aCBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojMjEyMTIxOyIgZD0iTTE1NS4wMTMsMTQ1LjJsLTIuMjgsOC4wMTYNCgkJCQkJCWMxMC4yMjQsMC4yMTYsMjkuNTkyLDAuMDQ4LDQ1LjcyLTMuNmwyLjQ5NiwxMS4xMzZjLTE4Ljk2LDQuMjk2LTQxLjgwOCw0LjEwNC01MS40MDgsMy43OTJsLTI1LjQ4OCw4OS45NzYNCgkJCQkJCWM5LjY3MiwzLjA0OCwyNy44ODgsMTAuOTY4LDI5LjM1MiwyNy43MmwtMTEuNCwwLjk4NGMtMC44ODgtMTAuMTUyLTEzLjcyOC0xNS41MDQtMjEuMDcyLTE3Ljc2bC0xLjM2OCw0LjgyNGwtMS41Niw1LjQ5Ng0KCQkJCQkJbC01LjQ3Mi0xLjU2bC00NS4wOTYtMTIuNzY4bC01LjQ5Ni0xLjU2bDEuNTYtNS40NzJsMzUuMzI4LTEyNC43MDRsMS41MzYtNS40NDhsNS40NDgsMS40ODhsNDUuMjE2LDEyLjQwOGw1LjU2OCwxLjUzNg0KCQkJCQkJTDE1NS4wMTMsMTQ1LjJMMTU1LjAxMywxNDUuMkwxNTUuMDEzLDE0NS4yeiBNMTEwLjE1NywyNjEuNjk2bDMyLjMwNC0xMTQuMDcybC0zNC4yLTkuMzg0TDc2LjAyOSwyNTIuMDI0TDExMC4xNTcsMjYxLjY5NnoiLz4NCgkJCQk8L2c+DQoJCQkJPGc+DQoJCQkJCTxwYXRoIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsOiMyMTIxMjE7IiBkPSJNMjQwLjc2NCwzMzYuNjcyTDI0MC43NjQsMzM2LjY3Mg0KCQkJCQkJYy0xLjEwNC0wLjgxNi0yLjQ0OC0xLjA4LTMuNzQ0LTAuODg4cy0yLjQ5NiwwLjg2NC0zLjMxMiwxLjk0NGwtOC44MzIsMTEuOTc2aDAuMDI0Yy0wLjgxNiwxLjEwNC0xLjEwNCwyLjQ3Mi0wLjkxMiwzLjc0NA0KCQkJCQkJYzAuMTkyLDEuMjcyLDAuODY0LDIuNDcyLDEuOTQ0LDMuMjg4bDAuMTY4LDAuMTQ0YzEuMDU2LDAuNzIsMi4zNTIsMC45NiwzLjU3NiwwLjc2OGMxLjI5Ni0wLjE5MiwyLjQ5Ni0wLjg2NCwzLjMxMi0xLjk0NA0KCQkJCQkJbDguODU2LTEyYzAuODE2LTEuMTA0LDEuMDgtMi40NDgsMC44ODgtMy43NDRDMjQyLjUxNiwzMzguNjg4LDI0MS44NDQsMzM3LjQ4OCwyNDAuNzY0LDMzNi42NzJMMjQwLjc2NCwzMzYuNjcyDQoJCQkJCQlMMjQwLjc2NCwzMzYuNjcyeiBNMTc2LjQyMSwyNjYuMjhjNC4yMjQsMy4xMiw2LjgxNiw3LjY4LDcuNTM2LDEyLjUwNGMwLjMxMiwyLjA2NCwwLjI4OCw0LjE3Ni0wLjA5Niw2LjI0DQoJCQkJCQljMS44OTYtMC45NiwzLjkzNi0xLjYwOCw2LjAyNC0xLjkyYzUuMDE2LTAuNzQ0LDEwLjI5NiwwLjM4NCwxNC42ODgsMy42MjR2MC4wMjRjNC40MTYsMy4yNCw3LjA4LDcuOTY4LDcuODI0LDEyLjk4NA0KCQkJCQkJYzAuMzEyLDEuOTkyLDAuMjg4LDQuMDMyLTAuMDI0LDYuMDQ4YzAuNi0wLjE0NCwxLjE3Ni0wLjI2NCwxLjc3Ni0wLjM2YzQuNTM2LTAuNjcyLDkuMzM2LDAuMzYsMTMuMjk2LDMuMjg4bDAuMjg4LDAuMjQNCgkJCQkJCWMzLjgxNiwyLjkyOCw2LjE0NCw3LjEyOCw2LjgxNiwxMS41MmMwLjIxNiwxLjM2OCwwLjI2NCwyLjc2LDAuMTQ0LDQuMTUyYzAuMjE2LTAuMDQ4LDAuNDA4LTAuMDcyLDAuNjI0LTAuMDk2DQoJCQkJCQljNC4xMjgtMC42MjQsOC41NDQsMC4zMzYsMTIuMTkyLDMuMDI0bDAsMGMzLjY3MiwyLjcxMiw1Ljg4LDYuNjI0LDYuNTA0LDEwLjc3NmMwLjYyNCw0LjEyOC0wLjMzNiw4LjU0NC0zLjA0OCwxMi4xOTINCgkJCQkJCWwtOC44NTYsMTJjLTIuNzEyLDMuNjcyLTYuNjI0LDUuODgtMTAuNzc2LDYuNTA0Yy00LjEyOCwwLjYyNC04LjUyLTAuMzM2LTEyLjE5Mi0zLjAyNHYwLjAyNA0KCQkJCQkJYy0zLjY0OC0yLjY4OC01Ljg4LTYuNjI0LTYuNTA0LTEwLjhjLTAuMDcyLTAuNDgtMC4xMi0wLjk2LTAuMTQ0LTEuNDRjLTEuMDA4LDAuMzM2LTIuMDQsMC42LTMuMDcyLDAuNzQ0DQoJCQkJCQljLTQuNTEyLDAuNjcyLTkuMzEyLTAuMzYtMTMuMjk2LTMuMzEybDAsMGMtMy45ODQtMi45NTItNi40MDgtNy4yMjQtNy4wOC0xMS43MzZjLTAuMTQ0LTAuOTEyLTAuMjE2LTEuODI0LTAuMTkyLTIuNzYNCgkJCQkJCWMtMS41MTIsMC42MjQtMy4wNzIsMS4wOC00LjY4LDEuMzJjLTUuMDE2LDAuNzQ0LTEwLjI5Ni0wLjM4NC0xNC42ODgtMy42MjRsMCwwYy00LjM5Mi0zLjI0LTcuMDgtNy45OTItNy44MjQtMTMuMDA4DQoJCQkJCQljLTAuMzg0LTIuNDcyLTAuMjg4LTUuMDE2LDAuMzEyLTcuNDg4Yy0xLjU4NCwwLjcyLTMuMjY0LDEuMi00Ljk2OCwxLjQ2NGMtNC44MjQsMC43Mi05LjkxMi0wLjM4NC0xNC4xMzYtMy40OA0KCQkJCQkJYy00LjIyNC0zLjEyLTYuODE2LTcuNjgtNy41MzYtMTIuNTA0czAuMzg0LTkuOTEyLDMuNDgtMTQuMTM2aDAuMDI0bDEwLjk5Mi0xNC45MDRjMy4xMi00LjI0OCw3LjY4LTYuODE2LDEyLjQ4LTcuNTM2DQoJCQkJCQlDMTY3LjA4NSwyNjIuMDU2LDE3Mi4xNzMsMjYzLjE2LDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4eiBNMTcyLjY1MywyODAuNDY0DQoJCQkJCQljLTAuMjg4LTEuOTQ0LTEuMzItMy43NjgtMi45NzYtNC45OTJ2LTAuMDI0Yy0xLjY4LTEuMjI0LTMuNzItMS42NTYtNS42ODgtMS4zNjhjLTEuOTY4LDAuMjg4LTMuNzkyLDEuMzItNS4wMTYsMi45NzYNCgkJCQkJCWwtMTAuOTkyLDE0Ljg4aDAuMDI0Yy0xLjI0OCwxLjY4LTEuNjgsMy43NDQtMS4zOTIsNS42ODhjMC4yODgsMS45NDQsMS4zMiwzLjc2OCwyLjk3Niw0Ljk5Mg0KCQkJCQkJYzEuNjgsMS4yNDgsMy43NDQsMS42OCw1LjY4OCwxLjM5MnMzLjc2OC0xLjMyLDQuOTkyLTIuOTc2bDAuMDI0LDBsMTAuOTkyLTE0Ljg4aC0wLjAyNA0KCQkJCQkJQzE3Mi40ODUsMjg0LjQ3MiwxNzIuOTQxLDI4Mi40MDgsMTcyLjY1MywyODAuNDY0TDE3Mi42NTMsMjgwLjQ2NEwxNzIuNjUzLDI4MC40NjR6IE0yMDEuMDkyLDMwMS40MTYNCgkJCQkJCWMtMC4zMTItMi4xMzYtMS40NC00LjE1Mi0zLjI2NC01LjQ5NnYwLjAyNGMtMS44NDgtMS4zNjgtNC4xMDQtMS44NDgtNi4yNC0xLjUzNmMtMi4xNiwwLjMzNi00LjE1MiwxLjQ0LTUuNTIsMy4yNjQNCgkJCQkJCWwtMTEuNjE2LDE1Ljc0NGMtMS4zNDQsMS44NDgtMS44MjQsNC4xMDQtMS41MTIsNi4yNGMwLjMxMiwyLjEzNiwxLjQ0LDQuMTI4LDMuMjY0LDUuNDk2bDAsMA0KCQkJCQkJYzEuODQ4LDEuMzY4LDQuMTA0LDEuODQ4LDYuMjQsMS41MzZjMi4xNi0wLjMxMiw0LjE1Mi0xLjQ0LDUuNTItMy4yNjRsMCwwbDExLjYxNi0xNS43NDQNCgkJCQkJCUMyMDAuOTQ4LDMwNS44MzIsMjAxLjQyOCwzMDMuNTc2LDIwMS4wOTIsMzAxLjQxNkwyMDEuMDkyLDMwMS40MTZMMjAxLjA5MiwzMDEuNDE2eiBNMjIzLjI0NCwzMjIuMTUyDQoJCQkJCQljLTAuMjQtMS42OC0xLjEwNC0zLjI0LTIuNTItNC4yNzJ2MC4wMjRjLTEuNDQtMS4wNTYtMy4xOTItMS40NC00Ljg0OC0xLjE3NmMtMS42NTYsMC4yNC0zLjIxNiwxLjEwNC00LjI0OCwyLjU0NGwtMC4xOTIsMC4yNA0KCQkJCQkJbC05Ljg4OCwxMy40MTZ2MC4wMjRjLTEuMDU2LDEuNDE2LTEuNDE2LDMuMTY4LTEuMTUyLDQuODI0YzAuMjY0LDEuNjgsMS4xMjgsMy4yNCwyLjU0NCw0LjI3MmwwLDANCgkJCQkJCWMxLjQxNiwxLjA1NiwzLjE2OCwxLjQxNiw0Ljg0OCwxLjE1MmMxLjY4LTAuMjY0LDMuMjQtMS4xMjgsNC4yNzItMi41NDRMMjIyLjE0MSwzMjcNCgkJCQkJCUMyMjMuMTI0LDMyNS41ODQsMjIzLjUwOCwzMjMuODMyLDIyMy4yNDQsMzIyLjE1MnoiLz4NCgkJCQk8L2c+DQoJCQk8L2c+DQoJCTwvZz4NCgk8L2c+DQo8L2c+DQo8L3N2Zz4NCg==\","; - - uri = string.concat(name, description, image, attributes); - } - - function readNameWithDefaultValue(address token) internal view returns (string memory name) { - // NOTE: this will not take into account the correct symbol on many chains - if (token == address(0)) { - return "Ether"; - } - - name = token.readName(); - if (bytes(name).length == 0) { - name = "unknown token"; - } - } - - function readSymbolWithDefaultValue(address token) - internal - view - returns (string memory symbol) - { - // NOTE: this will not take into account the correct symbol on many chains - if (token == address(0)) { - return "ETH"; - } - - symbol = token.readSymbol(); - if (bytes(symbol).length == 0) { - symbol = "???"; - } + function toAllocatorIndex(uint256 id) internal pure returns (uint256 index) { + index = id >> 0xd0; } - function readDecimalsWithDefaultValue(address token) - internal - view - returns (string memory decimals) - { - if (token == address(0)) { - return "18"; - } - return uint256(token.readDecimals()).toString(); - } - - function toAttributeString(string memory trait, string memory value, bool terminal) - internal - pure - returns (string memory attribute) - { - return string.concat( - "{\"trait_type\": \"", trait, "\", \"value\": \"", value, "\"}", terminal ? "" : "," - ); + function toAllocator(uint256 id) internal view returns (address allocator) { + allocator = id.toAllocatorIndex().toRegisteredAllocator(); } function toLock(address token, address allocator, uint48 resetPeriod) @@ -132,15 +45,22 @@ library IdLib { } function toLock(uint256 id) internal view returns (Lock memory lock) { - lock.token = toToken(id); - lock.resetPeriod = toResetPeriod(id); - lock.allocator = toAllocator(id); + lock.token = id.toToken(); + lock.resetPeriod = id.toResetPeriod(); + lock.allocator = id.toAllocator(); } function toId(Lock memory lock) internal returns (uint256 id) { id = ( uint256(uint160(lock.token)) | ((lock.resetPeriod << 0xd0) >> 0x30) - | toIndex(lock.allocator) << 0xd0 + | lock.allocator.toIndex() << 0xd0 + ); + } + + function toIdIfRegistered(Lock memory lock) internal view returns (uint256 id) { + id = ( + uint256(uint160(lock.token)) | ((lock.resetPeriod << 0xd0) >> 0x30) + | lock.allocator.toIndexIfRegistered() << 0xd0 ); } @@ -150,7 +70,7 @@ library IdLib { } } - function registeredAllocatorByIndex(uint256 index) internal view returns (address allocator) { + function toRegisteredAllocator(uint256 index) internal view returns (address allocator) { assembly { allocator := sload(or(_ALLOCATOR_BY_INDEX_SLOT_SEED, index)) diff --git a/src/lib/MetadataLib.sol b/src/lib/MetadataLib.sol new file mode 100644 index 0000000..77db604 --- /dev/null +++ b/src/lib/MetadataLib.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { Lock } from "../types/Lock.sol"; +import { IdLib } from "./IdLib.sol"; +import { LibString } from "solady/utils/LibString.sol"; +import { MetadataReaderLib } from "solady/utils/MetadataReaderLib.sol"; + +library MetadataLib { + using MetadataLib for address; + using MetadataLib for string; + using IdLib for Lock; + using LibString for uint256; + using LibString for address; + using MetadataReaderLib for address; + + function toURI(Lock memory lock, uint256 id) internal view returns (string memory uri) { + string memory tokenAddress = + lock.token == address(0) ? "Native Token" : lock.token.toHexStringChecksummed(); + string memory allocator = lock.allocator.toHexStringChecksummed(); + string memory resetPeriod = string.concat(lock.resetPeriod.toString(), " seconds"); + string memory tokenName = lock.token.readNameWithDefaultValue(); + string memory tokenSymbol = lock.token.readSymbolWithDefaultValue(); + string memory tokenDecimals = uint256(lock.token.readDecimals()).toString(); + + string memory name = string.concat("{\"name\": \"Compact ", tokenSymbol, "\","); + string memory description = string.concat( + "\"description\": \"Compact ", + tokenName, + " (", + tokenAddress, + ") with allocator ", + allocator, + " and reset period of ", + resetPeriod, + "\"," + ); + string memory attributes = string.concat( + "\"attributes\": [", + toAttributeString("ID", id.toString(), false), + toAttributeString("Token Address", tokenAddress, false), + toAttributeString("Token Name", tokenName, false), + toAttributeString("Token Symbol", tokenSymbol, false), + toAttributeString("Token Decimals", tokenDecimals, false), + toAttributeString("Allocator", allocator, false), + toAttributeString("Reset Period", resetPeriod, true), + "]}" + ); + + // Note: this just returns a default image; replace with a dynamic image based on attributes + string memory image = + "\"image\": \"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDIzLjAuNSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDkyIDQ5MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDkyIDQ5MjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGcgaWQ9Ik1hc3Rlcl9MYXllcl8yIj4NCjwvZz4NCjxnIGlkPSJMYXllcl8xIj4NCgk8Zz4NCgkJPGc+DQoJCQk8Zz4NCgkJCQk8Zz4NCgkJCQkJPHBhdGggc3R5bGU9ImZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIxMjEyMTsiIGQ9Ik0zMjEuMzA4LDI5NC44NjRjNS4zNTIsNS4zMjgsOS40NTYsMTIuMTQ0LDE1Ljc5Miw4LjgzMg0KCQkJCQkJYzIuNDQ4LTEuMjcyLDUuMDY0LTMuMDk2LDcuMzItNS4yNTZjMy43NDQtMy41NzYsOC4yNTYtOS41MjgsNC42NTYtMTQuMjhjLTEyLjQ1Ni0xMS45NzYtMzYuMzg0LTMyLjExMi0zNi40NTYtMzIuMTYNCgkJCQkJCWw3LjU2LTguNTY4YzAuMDI0LDAuMDI0LDUuMTYsNC41MzYsMTEuODMyLDEwLjgyNGM4LjY4OCw4LjIwOCwyMC44NTYsMTYuMiwyNi43MzYsMjQuNDA4DQoJCQkJCQljMy4zMTIsNC42MDgsMi42MTYsMTIuNzQ0LDAuODY0LDE3LjUyYy0xLjM5MiwzLjg0LTQuMTA0LDcuNDY0LTcuMzIsMTAuNTM2Yy0zLjAyNCwyLjkwNC02LjYsNS40LTkuOTYsNy4xMjgNCgkJCQkJCWMtMy4zODQsMS43NTItNi43OTIsMi43Ni05LjY5NiwyLjc4NGMtMC4wOTYsMC40NTYtMC4yMTYsMC45MzYtMC4zMzYsMS4zOTJjLTAuOTYsMy4yNC0zLjAyNCw2LjA3Mi01LjYxNiw4LjQNCgkJCQkJCWMtMi4zMjgsMi4wODgtNS4xMzYsMy44MTYtNy45NDQsNS4wNjRjLTMuMDcyLDEuMzQ0LTYuMjg4LDIuMTEyLTkuMTY4LDIuMTZjLTAuMDk2LDAuOTM2LTAuMjg4LDEuODQ4LTAuNTUyLDIuNzYNCgkJCQkJCWMtMC45NiwzLjI0LTMuMDI0LDYuMDcyLTUuNjE2LDguNGMtMi4zMjgsMi4wODgtNS4xMzYsMy44MTYtNy45NDQsNS4wNjRjLTQuMTI4LDEuODI0LTguNTQ0LDIuNTY4LTEyLDEuOTY4DQoJCQkJCQljLTAuMTIsMS4yMjQtMC4zNiwyLjQtMC42OTYsMy41MDR2MC4wMjRjLTEuMDMyLDMuMzg0LTMsNi4yNC01LjUyLDguMzUyYy0yLjUyLDIuMTEyLTUuNTkyLDMuNDgtOC44NTYsMy45MzYNCgkJCQkJCWMtMy45NiwwLjU1Mi04LjE2LTAuMjQtMTEuOTA0LTIuNjg4Yy0xLjAzMi0wLjY3Mi0yLjE2LTEuNTM2LTMuNDgtMi41OTJsLTAuNzQ0LTAuNTc2bC0xMS4xNi04LjYxNmw2Ljk2LTkuMDI0bDExLjE2LDguNjE2DQoJCQkJCQlsMC43NDQsMC41NzZjMS4wMzIsMC43OTIsMS44OTYsMS40ODgsMi43ODQsMi4wNGMxLjI5NiwwLjg2NCwyLjczNiwxLjEyOCw0LjA4LDAuOTZjMS4xMjgtMC4xNjgsMi4xODQtMC42NDgsMy4wNzItMS4zOTINCgkJCQkJCWMwLjg2NC0wLjcyLDEuNTYtMS43MjgsMS45Mi0yLjkwNGwwLDBjMC40NTYtMS41NiwwLjM4NC0zLjUwNC0wLjQ1Ni01Ljc2Yy05LjUyOC0xMy4yOTYtMjkuNDQ4LTI5LjQyNC0yOS40OTYtMjkuNDcyDQoJCQkJCQlsNy4yLTguODU2YzAuMDQ4LDAuMDI0LDguMTEyLDYuNTc2LDE2Ljc1MiwxNS4wMjRjMi4zMDQsMi4yNTYsNC44NDgsNC43NTIsNy41MTIsNy4xMjhjMC40OCwwLjQzMiwwLjk4NCwwLjg2NCwxLjQ2NCwxLjI5Ng0KCQkJCQkJbDAsMGwwLDBjMC4wOTYsMC4wOTYsMC4yMTYsMC4xOTIsMC4zMTIsMC4yODhjMC42MjQsMC41NTIsMS4yNDgsMS4xMjgsMS44NzIsMS43MDRjMi4xMTIsMS44OTYsNC4yLDMuODE2LDYuMzg0LDUuNDk2DQoJCQkJCQljMi41OTIsMS44NDgsMi41NDQsMi4yMzIsNS40OTYsMS4zNDRjMC42MjQtMC4xOTIsMS4yOTYtMC41MjgsMi4wMTYtMC44NGMxLjc3Ni0wLjc2OCwzLjUwNC0xLjg0OCw0Ljg5Ni0zLjA5Ng0KCQkJCQkJYzEuMTI4LTEuMDMyLDEuOTkyLTIuMTEyLDIuMzA0LTMuMTY4YzAuMjQtMC44NCwwLjA3Mi0xLjg0OC0wLjc0NC0yLjk3NmMtOS41NzYtMTMuMzItMzUuOTA0LTM2LjQ1Ni0zNS45NzYtMzYuNTI4bDcuNTYtOC41NjgNCgkJCQkJCWMwLjA0OCwwLjA0OCwxNC42ODgsMTIuOTEyLDI2LjYxNiwyNS40ODhjMy4yNCwzLjE5Miw4LjA2NCw3LjU2LDExLjU0NCwxMC4yNzJjMS4yNzIsMC45MTIsMi4xNiwyLjA4OCw0LjA4LDEuNDE2DQoJCQkJCQljMC44MTYtMC4yODgsMS44NDgtMC42OTYsMy0xLjJjMS43NzYtMC43NjgsMy41MDQtMS44NDgsNC44OTYtMy4wOTZjMS4xMjgtMS4wMDgsMS45OTItMi4xMTIsMi4zMDQtMy4xNjgNCgkJCQkJCWMwLjI0LTAuODQsMC4wNzItMS44NDgtMC43NDQtM2MtOS41NzYtMTMuMzItMzUuOTA0LTM2LjQ1Ni0zNS45NzYtMzYuNTI4bDcuNTYtOC41NjgNCgkJCQkJCUMyOTIuMjIsMjY2LjY4OCwzMDkuMDQ0LDI4MS40OTYsMzIxLjMwOCwyOTQuODY0eiIvPg0KCQkJCTwvZz4NCgkJCQk8Zz4NCgkJCQkJPHBhdGggc3R5bGU9ImZpbGwtcnVsZTpldmVub2RkO2NsaXAtcnVsZTpldmVub2RkO2ZpbGw6IzIxMjEyMTsiIGQ9Ik00MjkuMDIsMjU0LjQyNEwzOTMuNjkyLDEyOS43MmwtMS41MzYtNS40NDhsLTUuNDQ4LDEuNDg4DQoJCQkJCQlsLTQ1LjIxNiwxMi40MDhsLTUuNTY4LDEuNTM2bDEuNTYsNS41MmwyLjEzNiw3LjUzNmMtMjEuNjk2LDEuOTY4LTQyLjg0LTIuNjY0LTYyLjU2OC02Ljk2DQoJCQkJCQljLTM5LjI2NC04LjU2OC03My4yOTYtMTUuOTg0LTk5LjU3NiwyNS44OTZsMCwwYy03LjEwNCwxMS4zNTItMTQuODU2LDI0Ljg0LTE2LjY1NiwzNS4xNg0KCQkJCQkJYy0yLjQ3MiwxNC4wNCwzLjAyNCwyMy4wNCwyNS4yNDgsMTguOTZjMTMuNjU2LTIuNDk2LDIyLjA4LTkuMzYsMjkuOTI4LTE1Ljc2OGM4Ljg4LTcuMjQ4LDE2Ljg3Mi0xMy43NTIsMzIuMzc2LTkuMTQ0DQoJCQkJCQljOC4xMzYsMy4zNiw4Ljg4LDMuNjcyLDE1LjI0LDkuMDI0YzIxLjE0NCwxNy43MzYsNzEuNCw2MS41MzYsNzIsNjIuMDRsMCwwbDEwLjQxNiw5LjE2OGwyLjkwNCwyLjU0NGwzLjQzMi0xLjc1Mg0KCQkJCQkJbDIwLjg4LTEwLjYwOGwxLjI3Miw0LjQ4OGw1LjQ3Mi0xLjU2bDQ1LjA5Ni0xMi43NjhsNS40OTYtMS41Nkw0MjkuMDIsMjU0LjQyNEw0MjkuMDIsMjU0LjQyNHogTTM1MC42MzYsMjY5Ljk3NmwtNy41MTItNi42DQoJCQkJCQlIMzQzLjFjLTAuMTQ0LTAuMTItNTEuNjI0LTQ1LTcyLjE5Mi02Mi4yMzJjLTcuNzA0LTYuNDU2LTguNTY4LTYuODE2LTE4LjM2LTEwLjg3MmwtMC4yNC0wLjA5NmwtMC41MjgtMC4xOTINCgkJCQkJCWMtMjEuMzYtNi40NTYtMzEuNjA4LDEuOTItNDIuOTg0LDExLjIwOGMtNi43NjgsNS41Mi0xMy45OTIsMTEuNDI0LTI0Ljc2OCwxMy4zOTJjLTEwLjA4LDEuODQ4LTEyLjc2OC0xLjAzMi0xMS45MjgtNS43ODQNCgkJCQkJCWMxLjQ4OC04LjQ3Miw4LjU0NC0yMC42NjQsMTUuMDQ4LTMxLjA1NnYtMC4wMjRjMjEuOTYtMzUuMDY0LDUyLjM5Mi0yOC40NCw4Ny40OC0yMC43ODQNCgkJCQkJCWMyMS4xOTIsNC42MDgsNDMuOTQ0LDkuNTc2LDY4LjE2LDYuOTM2bDI3LjI2NCw5Ni4yNEwzNTAuNjM2LDI2OS45NzZMMzUwLjYzNiwyNjkuOTc2eiBNMzgyLjM2NCwyNjEuNjk2TDM1MC4wNiwxNDcuNjI1DQoJCQkJCQlsMzQuMi05LjM4NGwzMi4yMzIsMTEzLjc4NEwzODIuMzY0LDI2MS42OTZ6Ii8+DQoJCQkJPC9nPg0KCQkJCTxnPg0KCQkJCQk8cGF0aCBzdHlsZT0iZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7ZmlsbDojMjEyMTIxOyIgZD0iTTE1NS4wMTMsMTQ1LjJsLTIuMjgsOC4wMTYNCgkJCQkJCWMxMC4yMjQsMC4yMTYsMjkuNTkyLDAuMDQ4LDQ1LjcyLTMuNmwyLjQ5NiwxMS4xMzZjLTE4Ljk2LDQuMjk2LTQxLjgwOCw0LjEwNC01MS40MDgsMy43OTJsLTI1LjQ4OCw4OS45NzYNCgkJCQkJCWM5LjY3MiwzLjA0OCwyNy44ODgsMTAuOTY4LDI5LjM1MiwyNy43MmwtMTEuNCwwLjk4NGMtMC44ODgtMTAuMTUyLTEzLjcyOC0xNS41MDQtMjEuMDcyLTE3Ljc2bC0xLjM2OCw0LjgyNGwtMS41Niw1LjQ5Ng0KCQkJCQkJbC01LjQ3Mi0xLjU2bC00NS4wOTYtMTIuNzY4bC01LjQ5Ni0xLjU2bDEuNTYtNS40NzJsMzUuMzI4LTEyNC43MDRsMS41MzYtNS40NDhsNS40NDgsMS40ODhsNDUuMjE2LDEyLjQwOGw1LjU2OCwxLjUzNg0KCQkJCQkJTDE1NS4wMTMsMTQ1LjJMMTU1LjAxMywxNDUuMkwxNTUuMDEzLDE0NS4yeiBNMTEwLjE1NywyNjEuNjk2bDMyLjMwNC0xMTQuMDcybC0zNC4yLTkuMzg0TDc2LjAyOSwyNTIuMDI0TDExMC4xNTcsMjYxLjY5NnoiLz4NCgkJCQk8L2c+DQoJCQkJPGc+DQoJCQkJCTxwYXRoIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtmaWxsOiMyMTIxMjE7IiBkPSJNMjQwLjc2NCwzMzYuNjcyTDI0MC43NjQsMzM2LjY3Mg0KCQkJCQkJYy0xLjEwNC0wLjgxNi0yLjQ0OC0xLjA4LTMuNzQ0LTAuODg4cy0yLjQ5NiwwLjg2NC0zLjMxMiwxLjk0NGwtOC44MzIsMTEuOTc2aDAuMDI0Yy0wLjgxNiwxLjEwNC0xLjEwNCwyLjQ3Mi0wLjkxMiwzLjc0NA0KCQkJCQkJYzAuMTkyLDEuMjcyLDAuODY0LDIuNDcyLDEuOTQ0LDMuMjg4bDAuMTY4LDAuMTQ0YzEuMDU2LDAuNzIsMi4zNTIsMC45NiwzLjU3NiwwLjc2OGMxLjI5Ni0wLjE5MiwyLjQ5Ni0wLjg2NCwzLjMxMi0xLjk0NA0KCQkJCQkJbDguODU2LTEyYzAuODE2LTEuMTA0LDEuMDgtMi40NDgsMC44ODgtMy43NDRDMjQyLjUxNiwzMzguNjg4LDI0MS44NDQsMzM3LjQ4OCwyNDAuNzY0LDMzNi42NzJMMjQwLjc2NCwzMzYuNjcyDQoJCQkJCQlMMjQwLjc2NCwzMzYuNjcyeiBNMTc2LjQyMSwyNjYuMjhjNC4yMjQsMy4xMiw2LjgxNiw3LjY4LDcuNTM2LDEyLjUwNGMwLjMxMiwyLjA2NCwwLjI4OCw0LjE3Ni0wLjA5Niw2LjI0DQoJCQkJCQljMS44OTYtMC45NiwzLjkzNi0xLjYwOCw2LjAyNC0xLjkyYzUuMDE2LTAuNzQ0LDEwLjI5NiwwLjM4NCwxNC42ODgsMy42MjR2MC4wMjRjNC40MTYsMy4yNCw3LjA4LDcuOTY4LDcuODI0LDEyLjk4NA0KCQkJCQkJYzAuMzEyLDEuOTkyLDAuMjg4LDQuMDMyLTAuMDI0LDYuMDQ4YzAuNi0wLjE0NCwxLjE3Ni0wLjI2NCwxLjc3Ni0wLjM2YzQuNTM2LTAuNjcyLDkuMzM2LDAuMzYsMTMuMjk2LDMuMjg4bDAuMjg4LDAuMjQNCgkJCQkJCWMzLjgxNiwyLjkyOCw2LjE0NCw3LjEyOCw2LjgxNiwxMS41MmMwLjIxNiwxLjM2OCwwLjI2NCwyLjc2LDAuMTQ0LDQuMTUyYzAuMjE2LTAuMDQ4LDAuNDA4LTAuMDcyLDAuNjI0LTAuMDk2DQoJCQkJCQljNC4xMjgtMC42MjQsOC41NDQsMC4zMzYsMTIuMTkyLDMuMDI0bDAsMGMzLjY3MiwyLjcxMiw1Ljg4LDYuNjI0LDYuNTA0LDEwLjc3NmMwLjYyNCw0LjEyOC0wLjMzNiw4LjU0NC0zLjA0OCwxMi4xOTINCgkJCQkJCWwtOC44NTYsMTJjLTIuNzEyLDMuNjcyLTYuNjI0LDUuODgtMTAuNzc2LDYuNTA0Yy00LjEyOCwwLjYyNC04LjUyLTAuMzM2LTEyLjE5Mi0zLjAyNHYwLjAyNA0KCQkJCQkJYy0zLjY0OC0yLjY4OC01Ljg4LTYuNjI0LTYuNTA0LTEwLjhjLTAuMDcyLTAuNDgtMC4xMi0wLjk2LTAuMTQ0LTEuNDRjLTEuMDA4LDAuMzM2LTIuMDQsMC42LTMuMDcyLDAuNzQ0DQoJCQkJCQljLTQuNTEyLDAuNjcyLTkuMzEyLTAuMzYtMTMuMjk2LTMuMzEybDAsMGMtMy45ODQtMi45NTItNi40MDgtNy4yMjQtNy4wOC0xMS43MzZjLTAuMTQ0LTAuOTEyLTAuMjE2LTEuODI0LTAuMTkyLTIuNzYNCgkJCQkJCWMtMS41MTIsMC42MjQtMy4wNzIsMS4wOC00LjY4LDEuMzJjLTUuMDE2LDAuNzQ0LTEwLjI5Ni0wLjM4NC0xNC42ODgtMy42MjRsMCwwYy00LjM5Mi0zLjI0LTcuMDgtNy45OTItNy44MjQtMTMuMDA4DQoJCQkJCQljLTAuMzg0LTIuNDcyLTAuMjg4LTUuMDE2LDAuMzEyLTcuNDg4Yy0xLjU4NCwwLjcyLTMuMjY0LDEuMi00Ljk2OCwxLjQ2NGMtNC44MjQsMC43Mi05LjkxMi0wLjM4NC0xNC4xMzYtMy40OA0KCQkJCQkJYy00LjIyNC0zLjEyLTYuODE2LTcuNjgtNy41MzYtMTIuNTA0czAuMzg0LTkuOTEyLDMuNDgtMTQuMTM2aDAuMDI0bDEwLjk5Mi0xNC45MDRjMy4xMi00LjI0OCw3LjY4LTYuODE2LDEyLjQ4LTcuNTM2DQoJCQkJCQlDMTY3LjA4NSwyNjIuMDU2LDE3Mi4xNzMsMjYzLjE2LDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4TDE3Ni40MjEsMjY2LjI4eiBNMTcyLjY1MywyODAuNDY0DQoJCQkJCQljLTAuMjg4LTEuOTQ0LTEuMzItMy43NjgtMi45NzYtNC45OTJ2LTAuMDI0Yy0xLjY4LTEuMjI0LTMuNzItMS42NTYtNS42ODgtMS4zNjhjLTEuOTY4LDAuMjg4LTMuNzkyLDEuMzItNS4wMTYsMi45NzYNCgkJCQkJCWwtMTAuOTkyLDE0Ljg4aDAuMDI0Yy0xLjI0OCwxLjY4LTEuNjgsMy43NDQtMS4zOTIsNS42ODhjMC4yODgsMS45NDQsMS4zMiwzLjc2OCwyLjk3Niw0Ljk5Mg0KCQkJCQkJYzEuNjgsMS4yNDgsMy43NDQsMS42OCw1LjY4OCwxLjM5MnMzLjc2OC0xLjMyLDQuOTkyLTIuOTc2bDAuMDI0LDBsMTAuOTkyLTE0Ljg4aC0wLjAyNA0KCQkJCQkJQzE3Mi40ODUsMjg0LjQ3MiwxNzIuOTQxLDI4Mi40MDgsMTcyLjY1MywyODAuNDY0TDE3Mi42NTMsMjgwLjQ2NEwxNzIuNjUzLDI4MC40NjR6IE0yMDEuMDkyLDMwMS40MTYNCgkJCQkJCWMtMC4zMTItMi4xMzYtMS40NC00LjE1Mi0zLjI2NC01LjQ5NnYwLjAyNGMtMS44NDgtMS4zNjgtNC4xMDQtMS44NDgtNi4yNC0xLjUzNmMtMi4xNiwwLjMzNi00LjE1MiwxLjQ0LTUuNTIsMy4yNjQNCgkJCQkJCWwtMTEuNjE2LDE1Ljc0NGMtMS4zNDQsMS44NDgtMS44MjQsNC4xMDQtMS41MTIsNi4yNGMwLjMxMiwyLjEzNiwxLjQ0LDQuMTI4LDMuMjY0LDUuNDk2bDAsMA0KCQkJCQkJYzEuODQ4LDEuMzY4LDQuMTA0LDEuODQ4LDYuMjQsMS41MzZjMi4xNi0wLjMxMiw0LjE1Mi0xLjQ0LDUuNTItMy4yNjRsMCwwbDExLjYxNi0xNS43NDQNCgkJCQkJCUMyMDAuOTQ4LDMwNS44MzIsMjAxLjQyOCwzMDMuNTc2LDIwMS4wOTIsMzAxLjQxNkwyMDEuMDkyLDMwMS40MTZMMjAxLjA5MiwzMDEuNDE2eiBNMjIzLjI0NCwzMjIuMTUyDQoJCQkJCQljLTAuMjQtMS42OC0xLjEwNC0zLjI0LTIuNTItNC4yNzJ2MC4wMjRjLTEuNDQtMS4wNTYtMy4xOTItMS40NC00Ljg0OC0xLjE3NmMtMS42NTYsMC4yNC0zLjIxNiwxLjEwNC00LjI0OCwyLjU0NGwtMC4xOTIsMC4yNA0KCQkJCQkJbC05Ljg4OCwxMy40MTZ2MC4wMjRjLTEuMDU2LDEuNDE2LTEuNDE2LDMuMTY4LTEuMTUyLDQuODI0YzAuMjY0LDEuNjgsMS4xMjgsMy4yNCwyLjU0NCw0LjI3MmwwLDANCgkJCQkJCWMxLjQxNiwxLjA1NiwzLjE2OCwxLjQxNiw0Ljg0OCwxLjE1MmMxLjY4LTAuMjY0LDMuMjQtMS4xMjgsNC4yNzItMi41NDRMMjIyLjE0MSwzMjcNCgkJCQkJCUMyMjMuMTI0LDMyNS41ODQsMjIzLjUwOCwzMjMuODMyLDIyMy4yNDQsMzIyLjE1MnoiLz4NCgkJCQk8L2c+DQoJCQk8L2c+DQoJCTwvZz4NCgk8L2c+DQo8L2c+DQo8L3N2Zz4NCg==\","; + + uri = string.concat(name, description, image, attributes); + } + + function readNameWithDefaultValue(address token) internal view returns (string memory name) { + // NOTE: this will not take into account the correct symbol on many chains + if (token == address(0)) { + return "Ether"; + } + + name = token.readName(); + if (bytes(name).length == 0) { + name = "unknown token"; + } + } + + function readSymbolWithDefaultValue(address token) + internal + view + returns (string memory symbol) + { + // NOTE: this will not take into account the correct symbol on many chains + if (token == address(0)) { + return "ETH"; + } + + symbol = token.readSymbol(); + if (bytes(symbol).length == 0) { + symbol = "???"; + } + } + + function readDecimalsWithDefaultValue(address token) + internal + view + returns (string memory decimals) + { + if (token == address(0)) { + return "18"; + } + return uint256(token.readDecimals()).toString(); + } + + function toAttributeString(string memory trait, string memory value, bool terminal) + internal + pure + returns (string memory attribute) + { + return string.concat( + "{\"trait_type\": \"", trait, "\", \"value\": \"", value, "\"}", terminal ? "" : "," + ); + } +} \ No newline at end of file diff --git a/src/lib/MetadataRenderer.sol b/src/lib/MetadataRenderer.sol new file mode 100644 index 0000000..7f28ae5 --- /dev/null +++ b/src/lib/MetadataRenderer.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.27; + +import { MetadataLib } from "./MetadataLib.sol"; +import { Lock } from "../types/Lock.sol"; + +contract MetadataRenderer { + using MetadataLib for Lock; + + function uri(Lock memory lock, uint256 id) external view returns (string memory) { + return lock.toURI(id); + } +} \ No newline at end of file diff --git a/src/types/EIP712Types.sol b/src/types/EIP712Types.sol index 51e0bad..e4538d1 100644 --- a/src/types/EIP712Types.sol +++ b/src/types/EIP712Types.sol @@ -20,6 +20,26 @@ struct Allocation { bytes32 constant ALLOCATION_TYPEHASH = 0x332b96efcdc96931e9c671e47db4a873af3efa03557c9c2b93f4eb5f85587c15; + +// Message signed by the owner that specifies the conditions under which a set of +// tokens can be allocated; the specified oracle verifies that those conditions +// have been met, enabling an allocatee to claim the specified token amounts. +struct BatchAllocation { + address owner; // The account to source the allocation from. + uint256 startTime; // The time at which the allocation can be released. + uint256 endTime; // The time at which the allocation expires. + uint256 nonce; // A parameter to enforce replay protection, scoped to allocator. + uint256[] ids; // The token IDs to allocate, ordered sequentially by token address & sharing a single allocator. + uint256[] amounts; // The amounts of each ERC6909 token to allocate. + address claimant; // The allocation recipient (no address: any recipient) + address oracle; // The account enforcing whether to release allocated funds. + bytes oracleFixedData; // The fixed data payload provided to the oracle. +} + +// keccak256("BatchAllocation(address owner,uint256 startTime,uint256 endTime,uint256 nonce,uint256[] ids,uint256[] amounts,address claimant,address oracle,bytes oracleFixedData)") +bytes32 constant BATCH_ALLOCATION_TYPEHASH = + 0x4a861c73d7d12b2f376a12be983a2d1f530af97e12d5ff0b5f8e9d790ee09b93; + // Message signed by the allocator that confirms that a given allocation does // not result in an over-allocated state for the token owner, and that modifies // the conditions of the allocation where applicable. @@ -35,6 +55,21 @@ struct AllocationAuthorization { bytes32 constant ALLOCATION_AUTHORIZATION_TYPEHASH = 0x9d7957a907b00fac8de3a22c078f7f0409c40a085d5c51f7a371cf3291563692; +// Message signed by the allocator that confirms that a given allocation does +// not result in an over-allocated state for the token owner, and that modifies +// the conditions of the allocation where applicable. +struct BatchAllocationAuthorization { + // bytes32 allocationHash; // signed but not explicitly supplied as a parameter + uint256 startTime; // The time at which the allocation authorization becomes valid. + uint256 endTime; // The time at which the allocation authorization expires. + address claimant; // The allocation recipient (no address: any recipient) + uint256[] amountReductions; // The amounts by which each claimable token will be reduced. +} + +// keccak256("BatchAllocationAuthorization(bytes32 allocationHash,uint256 startTime,uint256 endTime,address claimant,uint256[] amountReductions)") +bytes32 constant BATCH_ALLOCATION_AUTHORIZATION_TYPEHASH = + 0x4c90cb61ec8a1c2fae08542cc7898d379b30b0aa4a2a31eb94f4942bf3b3e59a; + // Message signed by the allocator that confirms that a given withdrawal does // not result in an over-allocated state for the token owner, and that enables // the owner to directly withdraw their tokens to an arbitrary recipient. diff --git a/test/TheCompact.t.sol b/test/TheCompact.t.sol index 4dc78b1..aba43ca 100644 --- a/test/TheCompact.t.sol +++ b/test/TheCompact.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.13; import { Test, console } from "forge-std/Test.sol"; import { TheCompact } from "../src/TheCompact.sol"; import { MockERC20 } from "../lib/solady/test/utils/mocks/MockERC20.sol"; -import { Allocation, AllocationAuthorization } from "../src/types/EIP712Types.sol"; +import { Allocation, AllocationAuthorization, BatchAllocation, BatchAllocationAuthorization } from "../src/types/EIP712Types.sol"; interface EIP712 { function DOMAIN_SEPARATOR() external view returns (bytes32); @@ -13,12 +13,14 @@ interface EIP712 { contract TheCompactTest is Test { TheCompact public theCompact; MockERC20 public token; + MockERC20 public anotherToken; address permit2 = address(0x000000000022D473030F116dDEE9F6B43aC78BA3); uint256 swapperPrivateKey; address swapper; uint256 allocatorPrivateKey; address allocator; address dummyOracle; + address dummyBatchOracle; bytes32 compactEIP712DomainHash = keccak256( "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" ); @@ -27,6 +29,7 @@ contract TheCompactTest is Test { function setUp() public { address deployedDummyOracle; + address deployedDummyBatchOracle; assembly { // deploy a contract that always returns one word of 0's followed by one word of f's // minimal "constructor" 0x600b5981380380925939f3... (11 bytes) @@ -41,8 +44,15 @@ contract TheCompactTest is Test { // F3 RETURN [] (Returns 0x40 bytes from memory starting at 0x00) mstore(0, 0x600b5981380380925939f360403d3d19602052f3) deployedDummyOracle := create(0, 12, 20) + + // and this one returns [0, 0x40, 3, 0xfff, 0xfff, 0xfff] + mstore(0, 0x600b598138038092) + mstore(0x20, 0x5939f360c03d60406020600360403d1960603d1960803d1960a05252525252f3) + deployedDummyBatchOracle := create(0, 24, 40) } dummyOracle = deployedDummyOracle; + dummyBatchOracle = deployedDummyBatchOracle; + address permit2Deployer = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); address deployedPermit2Deployer; address permit2DeployerDeployer = address(0x3fAB184622Dc19b6109349B94811493BF2a45362); @@ -65,16 +75,20 @@ contract TheCompactTest is Test { theCompact = new TheCompact(); token = new MockERC20("Mock ERC20", "MOCK", 18); + anotherToken = new MockERC20("Another Mock ERC20", "MOCK2", 18); (swapper, swapperPrivateKey) = makeAddrAndKey("swapper"); (allocator, allocatorPrivateKey) = makeAddrAndKey("allocator"); vm.deal(swapper, 2e18); token.mint(swapper, 1e18); + anotherToken.mint(swapper, 1e18); vm.startPrank(swapper); token.approve(address(theCompact), 1e18); token.approve(permit2, 1e18); + anotherToken.approve(address(theCompact), 1e18); + anotherToken.approve(permit2, 1e18); vm.stopPrank(); } @@ -564,6 +578,8 @@ contract TheCompactTest is Test { address allocatorClaimant = claimant; uint256 amountReduction = 0; + uint256 anotherAmountReduction = 0; + uint256 aThirdAmountReduction = 0; vm.prank(swapper); uint256 id = theCompact.deposit{ value: amount }(allocator, resetPeriod, swapper); @@ -639,4 +655,126 @@ contract TheCompactTest is Test { assertEq(theCompact.balanceOf(claimant, id), 0); assertEq(theCompact.balanceOf(recipient, id), 0); } + + function test_batchClaim() public { + uint48 resetPeriod = 120; + uint256 amount = 1e18; + uint256 anotherAmount = 1e18; + uint256 aThirdAmount = 1e18; + uint256 nonce = 0; + uint256 startTime = block.timestamp; + uint256 endTime = block.timestamp + 1000; + address claimant = 0x1111111111111111111111111111111111111111; + address oracle = dummyBatchOracle; + bytes memory oracleFixedData; + bytes memory oracleVariableData; + + address allocatorClaimant = claimant; + uint256 amountReduction = 0; + uint256 anotherAmountReduction = 0; + uint256 aThirdAmountReduction = 0; + + vm.startPrank(swapper); + uint256 id = theCompact.deposit{ value: amount }(allocator, resetPeriod, swapper); + + uint256 anotherId = theCompact.deposit(address(token), allocator, resetPeriod, anotherAmount, swapper); + assertEq(theCompact.balanceOf(swapper, id), anotherAmount); + + uint256 aThirdId = theCompact.deposit(address(anotherToken), allocator, resetPeriod, aThirdAmount, swapper); + assertEq(theCompact.balanceOf(swapper, id), aThirdAmount); + + vm.stopPrank(); + + assertEq(theCompact.balanceOf(swapper, id), amount); + assertEq(theCompact.balanceOf(swapper, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(swapper, aThirdId), aThirdAmount); + + uint256[] memory ids = new uint256[](3); + ids[0] = id; + ids[1] = anotherId; + ids[2] = aThirdId; + + uint256[] memory amounts = new uint256[](3); + amounts[0] = amount; + amounts[1] = anotherAmount; + amounts[2] = aThirdAmount; + + uint256[] memory amountReductions = new uint256[](3); + amountReductions[0] = amountReduction; + amountReductions[1] = anotherAmountReduction; + amountReductions[2] = aThirdAmountReduction; + + bytes32 allocationHash = keccak256( + abi.encode( + keccak256( + "BatchAllocation(address owner,uint256 startTime,uint256 endTime,uint256 nonce,uint256[] ids,uint256[] amounts,address claimant,address oracle,bytes oracleFixedData)" + ), + swapper, + startTime, + endTime, + nonce, + keccak256(abi.encode(ids)), + keccak256(abi.encode(amounts)), + claimant, + oracle, + keccak256(oracleFixedData) + ) + ); + + bytes32 digest = keccak256( + abi.encodePacked(bytes2(0x1901), theCompact.DOMAIN_SEPARATOR(), allocationHash) + ); + + (bytes32 r, bytes32 vs) = vm.signCompact(swapperPrivateKey, digest); + bytes memory ownerSignature = abi.encodePacked(r, vs); + + digest = keccak256( + abi.encodePacked( + bytes2(0x1901), + theCompact.DOMAIN_SEPARATOR(), + keccak256( + abi.encode( + keccak256( + "BatchAllocationAuthorization(bytes32 allocationHash,uint256 startTime,uint256 endTime,address claimant,uint256[] amountReductions)" + ), + allocationHash, + startTime, + endTime, + allocatorClaimant, + keccak256(abi.encode(amountReductions)) + ) + ) + ) + ); + + (r, vs) = vm.signCompact(allocatorPrivateKey, digest); + bytes memory allocatorSignature = abi.encodePacked(r, vs); + + BatchAllocation memory allocation = BatchAllocation( + swapper, startTime, endTime, nonce, ids, amounts, claimant, oracle, oracleFixedData + ); + BatchAllocationAuthorization memory allocationAuthorization = + BatchAllocationAuthorization(startTime, endTime, allocatorClaimant, amountReductions); + + (address returnedClaimant, uint256[] memory returnedClaimAmounts) = theCompact.claim( + allocation, + allocationAuthorization, + oracleVariableData, + ownerSignature, + allocatorSignature + ); + assertEq(claimant, returnedClaimant); + + for (uint256 i = 0; i < ids.length; ++i) { + assertEq(amounts[i], returnedClaimAmounts[i]); + } + + assertEq(address(theCompact).balance, amount); + assertEq(token.balanceOf(address(theCompact)), anotherAmount); + assertEq(anotherToken.balanceOf(address(theCompact)), aThirdAmount); + + assertEq(theCompact.balanceOf(claimant, id), amount); + assertEq(theCompact.balanceOf(claimant, anotherId), anotherAmount); + assertEq(theCompact.balanceOf(claimant, aThirdId), aThirdAmount); + } }