// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.0; /** * @title Library for encoding and decoding ancillary data for DVM price requests. * @notice We assume that on-chain ancillary data can be formatted directly from bytes to utf8 encoding via * web3.utils.hexToUtf8, and that clients will parse the utf8-encoded ancillary data as a comma-delimitted key-value * dictionary. Therefore, this library provides internal methods that aid appending to ancillary data from Solidity * smart contracts. More details on UMA's ancillary data guidelines below: * https://docs.google.com/document/d/1zhKKjgY1BupBGPPrY_WOJvui0B6DMcd-xDR8-9-SPDw/edit */ library AncillaryData { // This converts the bottom half of a bytes32 input to hex in a highly gas-optimized way. // Source: the brilliant implementation at https://gitter.im/ethereum/solidity?at=5840d23416207f7b0ed08c9b. function toUtf8Bytes32Bottom(bytes32 bytesIn) private pure returns (bytes32) { unchecked { uint256 x = uint256(bytesIn); // Nibble interleave x = x & 0x00000000000000000000000000000000ffffffffffffffffffffffffffffffff; x = (x | (x * 2**64)) & 0x0000000000000000ffffffffffffffff0000000000000000ffffffffffffffff; x = (x | (x * 2**32)) & 0x00000000ffffffff00000000ffffffff00000000ffffffff00000000ffffffff; x = (x | (x * 2**16)) & 0x0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff0000ffff; x = (x | (x * 2**8)) & 0x00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff00ff; x = (x | (x * 2**4)) & 0x0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f; // Hex encode uint256 h = (x & 0x0808080808080808080808080808080808080808080808080808080808080808) / 8; uint256 i = (x & 0x0404040404040404040404040404040404040404040404040404040404040404) / 4; uint256 j = (x & 0x0202020202020202020202020202020202020202020202020202020202020202) / 2; x = x + (h & (i | j)) * 0x27 + 0x3030303030303030303030303030303030303030303030303030303030303030; // Return the result. return bytes32(x); } } /** * @notice Returns utf8-encoded bytes32 string that can be read via web3.utils.hexToUtf8. * @dev Will return bytes32 in all lower case hex characters and without the leading 0x. * This has minor changes from the toUtf8BytesAddress to control for the size of the input. * @param bytesIn bytes32 to encode. * @return utf8 encoded bytes32. */ function toUtf8Bytes(bytes32 bytesIn) internal pure returns (bytes memory) { return abi.encodePacked(toUtf8Bytes32Bottom(bytesIn >> 128), toUtf8Bytes32Bottom(bytesIn)); } /** * @notice Returns utf8-encoded address that can be read via web3.utils.hexToUtf8. * Source: https://ethereum.stackexchange.com/questions/8346/convert-address-to-string/8447#8447 * @dev Will return address in all lower case characters and without the leading 0x. * @param x address to encode. * @return utf8 encoded address bytes. */ function toUtf8BytesAddress(address x) internal pure returns (bytes memory) { return abi.encodePacked(toUtf8Bytes32Bottom(bytes32(bytes20(x)) >> 128), bytes8(toUtf8Bytes32Bottom(bytes20(x)))); } /** * @notice Converts a uint into a base-10, UTF-8 representation stored in a `string` type. * @dev This method is based off of this code: https://stackoverflow.com/a/65707309. */ function toUtf8BytesUint(uint256 x) internal pure returns (bytes memory) { if (x == 0) { return "0"; } uint256 j = x; uint256 len; while (j != 0) { len++; j /= 10; } bytes memory bstr = new bytes(len); uint256 k = len; while (x != 0) { k = k - 1; uint8 temp = (48 + uint8(x - (x / 10) * 10)); bytes1 b1 = bytes1(temp); bstr[k] = b1; x /= 10; } return bstr; } function appendKeyValueBytes32( bytes memory currentAncillaryData, bytes memory key, bytes32 value ) internal pure returns (bytes memory) { bytes memory prefix = constructPrefix(currentAncillaryData, key); return abi.encodePacked(currentAncillaryData, prefix, toUtf8Bytes(value)); } /** * @notice Adds "key:value" to `currentAncillaryData` where `value` is an address that first needs to be converted * to utf8 bytes. For example, if `utf8(currentAncillaryData)="k1:v1"`, then this function will return * `utf8(k1:v1,key:value)`, and if `currentAncillaryData` is blank, then this will return `utf8(key:value)`. * @param currentAncillaryData This bytes data should ideally be able to be utf8-decoded, but its OK if not. * @param key Again, this bytes data should ideally be able to be utf8-decoded, but its OK if not. * @param value An address to set as the value in the key:value pair to append to `currentAncillaryData`. * @return Newly appended ancillary data. */ function appendKeyValueAddress( bytes memory currentAncillaryData, bytes memory key, address value ) internal pure returns (bytes memory) { bytes memory prefix = constructPrefix(currentAncillaryData, key); return abi.encodePacked(currentAncillaryData, prefix, toUtf8BytesAddress(value)); } /** * @notice Adds "key:value" to `currentAncillaryData` where `value` is a uint that first needs to be converted * to utf8 bytes. For example, if `utf8(currentAncillaryData)="k1:v1"`, then this function will return * `utf8(k1:v1,key:value)`, and if `currentAncillaryData` is blank, then this will return `utf8(key:value)`. * @param currentAncillaryData This bytes data should ideally be able to be utf8-decoded, but its OK if not. * @param key Again, this bytes data should ideally be able to be utf8-decoded, but its OK if not. * @param value A uint to set as the value in the key:value pair to append to `currentAncillaryData`. * @return Newly appended ancillary data. */ function appendKeyValueUint( bytes memory currentAncillaryData, bytes memory key, uint256 value ) internal pure returns (bytes memory) { bytes memory prefix = constructPrefix(currentAncillaryData, key); return abi.encodePacked(currentAncillaryData, prefix, toUtf8BytesUint(value)); } /** * @notice Helper method that returns the left hand side of a "key:value" pair plus the colon ":" and a leading * comma "," if the `currentAncillaryData` is not empty. The return value is intended to be prepended as a prefix to * some utf8 value that is ultimately added to a comma-delimited, key-value dictionary. */ function constructPrefix(bytes memory currentAncillaryData, bytes memory key) internal pure returns (bytes memory) { if (currentAncillaryData.length > 0) { return abi.encodePacked(",", key, ":"); } else { return abi.encodePacked(key, ":"); } } }