144 lines
7.1 KiB
Solidity
144 lines
7.1 KiB
Solidity
// 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, ":");
|
|
}
|
|
}
|
|
} |