Skip to content

Commit

Permalink
Bytes library and CAIP2/CAIP10 helpers (#5252)
Browse files Browse the repository at this point in the history
Co-authored-by: cairo <[email protected]>
Co-authored-by: Ernesto García <[email protected]>
Co-authored-by: Arr00 <[email protected]>
  • Loading branch information
4 people authored Oct 14, 2024
1 parent bd58895 commit fe6249e
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/healthy-books-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
5 changes: 5 additions & 0 deletions .changeset/proud-planes-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Bytes`: Add a library of common operation that operate on `bytes` objects.
3 changes: 3 additions & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {Arrays} from "../utils/Arrays.sol";
import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol";
import {Base64} from "../utils/Base64.sol";
import {BitMaps} from "../utils/structs/BitMaps.sol";
import {Bytes} from "../utils/Bytes.sol";
import {CAIP2} from "../utils/CAIP2.sol";
import {CAIP10} from "../utils/CAIP10.sol";
import {Checkpoints} from "../utils/structs/Checkpoints.sol";
import {CircularBuffer} from "../utils/structs/CircularBuffer.sol";
import {Clones} from "../proxy/Clones.sol";
Expand Down
115 changes: 115 additions & 0 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {Math} from "./math/Math.sol";

/**
* @dev Bytes operations.
*/
library Bytes {
/**
* @dev Forward search for `s` in `buffer`
* * If `s` is present in the buffer, returns the index of the first instance
* * If `s` is not present in the buffer, returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`]
*/
function indexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
return indexOf(buffer, s, 0);
}

/**
* @dev Forward search for `s` in `buffer` starting at position `pos`
* * If `s` is present in the buffer (at or after `pos`), returns the index of the next instance
* * If `s` is not present in the buffer (at or after `pos`), returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`]
*/
function indexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
for (uint256 i = pos; i < length; ++i) {
if (bytes1(_unsafeReadBytesOffset(buffer, i)) == s) {
return i;
}
}
return type(uint256).max;
}
}

/**
* @dev Backward search for `s` in `buffer`
* * If `s` is present in the buffer, returns the index of the last instance
* * If `s` is not present in the buffer, returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`]
*/
function lastIndexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
return lastIndexOf(buffer, s, type(uint256).max);
}

/**
* @dev Backward search for `s` in `buffer` starting at position `pos`
* * If `s` is present in the buffer (at or before `pos`), returns the index of the previous instance
* * If `s` is not present in the buffer (at or before `pos`), returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`]
*/
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) {
return i - 1;
}
}
return type(uint256).max;
}
}

/**
* @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in
* memory.
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`]
*/
function slice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) {
return slice(buffer, start, buffer.length);
}

/**
* @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in
* memory.
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`]
*/
function slice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) {
// sanitize
uint256 length = buffer.length;
end = Math.min(end, length);
start = Math.min(start, end);

// allocate and copy
bytes memory result = new bytes(end - start);
assembly ("memory-safe") {
mcopy(add(result, 0x20), add(buffer, add(start, 0x20)), sub(end, start))
}

return result;
}

/**
* @dev Reads a bytes32 from a bytes array without bounds checking.
*
* NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the
* assembly block as such would prevent some optimizations.
*/
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
// This is not memory safe in the general case, but all calls to this private function are within bounds.
assembly ("memory-safe") {
value := mload(add(buffer, add(0x20, offset)))
}
}
}
49 changes: 49 additions & 0 deletions contracts/utils/CAIP10.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
import {CAIP2} from "./CAIP2.sol";
import {Strings} from "./Strings.sol";

/**
* @dev Helper library to format and parse CAIP-10 identifiers
*
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md[CAIP-10] defines account identifiers as:
* account_id: chain_id + ":" + account_address
* chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} (See {CAIP2})
* account_address: [-.%a-zA-Z0-9]{1,128}
*/
library CAIP10 {
using SafeCast for uint256;
using Strings for address;
using Bytes for bytes;

/// @dev Return the CAIP-10 identifier for an account on the current (local) chain.
function local(address account) internal view returns (string memory) {
return format(CAIP2.local(), account.toChecksumHexString());
}

/**
* @dev Return the CAIP-10 identifier for a given caip2 chain and account.
*
* NOTE: This function does not verify that the inputs are properly formatted.
*/
function format(string memory caip2, string memory account) internal pure returns (string memory) {
return string.concat(caip2, ":", account);
}

/**
* @dev Parse a CAIP-10 identifier into its components.
*
* NOTE: This function does not verify that the CAIP-10 input is properly formatted. The `caip2` return can be
* parsed using the {CAIP2} library.
*/
function parse(string memory caip10) internal pure returns (string memory caip2, string memory account) {
bytes memory buffer = bytes(caip10);

uint256 pos = buffer.lastIndexOf(":");
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
}
}
47 changes: 47 additions & 0 deletions contracts/utils/CAIP2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
import {Strings} from "./Strings.sol";

/**
* @dev Helper library to format and parse CAIP-2 identifiers
*
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] defines chain identifiers as:
* chain_id: namespace + ":" + reference
* namespace: [-a-z0-9]{3,8}
* reference: [-_a-zA-Z0-9]{1,32}
*/
library CAIP2 {
using SafeCast for uint256;
using Strings for uint256;
using Bytes for bytes;

/// @dev Return the CAIP-2 identifier for the current (local) chain.
function local() internal view returns (string memory) {
return format("eip155", block.chainid.toString());
}

/**
* @dev Return the CAIP-2 identifier for a given namespace and reference.
*
* NOTE: This function does not verify that the inputs are properly formatted.
*/
function format(string memory namespace, string memory ref) internal pure returns (string memory) {
return string.concat(namespace, ":", ref);
}

/**
* @dev Parse a CAIP-2 identifier into its components.
*
* NOTE: This function does not verify that the CAIP-2 input is properly formatted.
*/
function parse(string memory caip2) internal pure returns (string memory namespace, string memory ref) {
bytes memory buffer = bytes(caip2);

uint256 pos = buffer.indexOf(":");
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
}
}
2 changes: 2 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type.
* {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`].
* {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648].
* {Bytes}: Common operations on bytes objects.
* {Strings}: Common operations for strings formatting.
* {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters.
* {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays.
Expand All @@ -41,6 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Packing}: A library for packing and unpacking multiple values into bytes32
* {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes].
* {Comparators}: A library that contains comparator functions to use with with the {Heap} library.
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.

[NOTE]
====
Expand Down
109 changes: 109 additions & 0 deletions test/helpers/chains.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// NOTE: this file defines some examples of CAIP-2 and CAIP-10 identifiers.
// The following listing does not pretend to be exhaustive or even accurate. It SHOULD NOT be used in production.

const { ethers } = require('hardhat');
const { mapValues } = require('./iterate');

// EVM (https://axelarscan.io/resources/chains?type=evm)
const ethereum = {
Ethereum: '1',
optimism: '10',
binance: '56',
Polygon: '137',
Fantom: '250',
fraxtal: '252',
filecoin: '314',
Moonbeam: '1284',
centrifuge: '2031',
kava: '2222',
mantle: '5000',
base: '8453',
immutable: '13371',
arbitrum: '42161',
celo: '42220',
Avalanche: '43114',
linea: '59144',
blast: '81457',
scroll: '534352',
aurora: '1313161554',
};

// Cosmos (https://axelarscan.io/resources/chains?type=cosmos)
const cosmos = {
Axelarnet: 'axelar-dojo-1',
osmosis: 'osmosis-1',
cosmoshub: 'cosmoshub-4',
juno: 'juno-1',
'e-money': 'emoney-3',
injective: 'injective-1',
crescent: 'crescent-1',
kujira: 'kaiyo-1',
'secret-snip': 'secret-4',
secret: 'secret-4',
sei: 'pacific-1',
stargaze: 'stargaze-1',
assetmantle: 'mantle-1',
fetch: 'fetchhub-4',
ki: 'kichain-2',
evmos: 'evmos_9001-2',
aura: 'xstaxy-1',
comdex: 'comdex-1',
persistence: 'core-1',
regen: 'regen-1',
umee: 'umee-1',
agoric: 'agoric-3',
xpla: 'dimension_37-1',
acre: 'acre_9052-1',
stride: 'stride-1',
carbon: 'carbon-1',
sommelier: 'sommelier-3',
neutron: 'neutron-1',
rebus: 'reb_1111-1',
archway: 'archway-1',
provenance: 'pio-mainnet-1',
ixo: 'ixo-5',
migaloo: 'migaloo-1',
teritori: 'teritori-1',
haqq: 'haqq_11235-1',
celestia: 'celestia',
ojo: 'agamotto',
chihuahua: 'chihuahua-1',
saga: 'ssc-1',
dymension: 'dymension_1100-1',
fxcore: 'fxcore',
c4e: 'perun-1',
bitsong: 'bitsong-2b',
nolus: 'pirin-1',
lava: 'lava-mainnet-1',
'terra-2': 'phoenix-1',
terra: 'columbus-5',
};

const makeCAIP = ({ namespace, reference, account }) => ({
namespace,
reference,
account,
caip2: `${namespace}:${reference}`,
caip10: `${namespace}:${reference}:${account}`,
toCaip10: other => `${namespace}:${reference}:${ethers.getAddress(other.target ?? other.address ?? other)}`,
});

module.exports = {
CHAINS: mapValues(
Object.assign(
mapValues(ethereum, reference => ({
namespace: 'eip155',
reference,
account: ethers.Wallet.createRandom().address,
})),
mapValues(cosmos, reference => ({
namespace: 'cosmos',
reference,
account: ethers.encodeBase58(ethers.randomBytes(32)),
})),
),
makeCAIP,
),
getLocalCAIP: account =>
ethers.provider.getNetwork().then(({ chainId }) => makeCAIP({ namespace: 'eip155', reference: chainId, account })),
};
Loading

0 comments on commit fe6249e

Please sign in to comment.