Skip to content

Commit fe6249e

Browse files
Amxxcairoethernestognwarr00
authored
Bytes library and CAIP2/CAIP10 helpers (#5252)
Co-authored-by: cairo <cairoeth@protonmail.com> Co-authored-by: Ernesto García <ernestognw@gmail.com> Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com>
1 parent bd58895 commit fe6249e

File tree

10 files changed

+476
-0
lines changed

10 files changed

+476
-0
lines changed

.changeset/healthy-books-shout.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.

.changeset/proud-planes-arrive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`Bytes`: Add a library of common operation that operate on `bytes` objects.

contracts/mocks/Stateless.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {Arrays} from "../utils/Arrays.sol";
99
import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol";
1010
import {Base64} from "../utils/Base64.sol";
1111
import {BitMaps} from "../utils/structs/BitMaps.sol";
12+
import {Bytes} from "../utils/Bytes.sol";
13+
import {CAIP2} from "../utils/CAIP2.sol";
14+
import {CAIP10} from "../utils/CAIP10.sol";
1215
import {Checkpoints} from "../utils/structs/Checkpoints.sol";
1316
import {CircularBuffer} from "../utils/structs/CircularBuffer.sol";
1417
import {Clones} from "../proxy/Clones.sol";

contracts/utils/Bytes.sol

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {Math} from "./math/Math.sol";
6+
7+
/**
8+
* @dev Bytes operations.
9+
*/
10+
library Bytes {
11+
/**
12+
* @dev Forward search for `s` in `buffer`
13+
* * If `s` is present in the buffer, returns the index of the first instance
14+
* * If `s` is not present in the buffer, returns type(uint256).max
15+
*
16+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`]
17+
*/
18+
function indexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
19+
return indexOf(buffer, s, 0);
20+
}
21+
22+
/**
23+
* @dev Forward search for `s` in `buffer` starting at position `pos`
24+
* * If `s` is present in the buffer (at or after `pos`), returns the index of the next instance
25+
* * If `s` is not present in the buffer (at or after `pos`), returns type(uint256).max
26+
*
27+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`]
28+
*/
29+
function indexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
30+
unchecked {
31+
uint256 length = buffer.length;
32+
for (uint256 i = pos; i < length; ++i) {
33+
if (bytes1(_unsafeReadBytesOffset(buffer, i)) == s) {
34+
return i;
35+
}
36+
}
37+
return type(uint256).max;
38+
}
39+
}
40+
41+
/**
42+
* @dev Backward search for `s` in `buffer`
43+
* * If `s` is present in the buffer, returns the index of the last instance
44+
* * If `s` is not present in the buffer, returns type(uint256).max
45+
*
46+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`]
47+
*/
48+
function lastIndexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
49+
return lastIndexOf(buffer, s, type(uint256).max);
50+
}
51+
52+
/**
53+
* @dev Backward search for `s` in `buffer` starting at position `pos`
54+
* * If `s` is present in the buffer (at or before `pos`), returns the index of the previous instance
55+
* * If `s` is not present in the buffer (at or before `pos`), returns type(uint256).max
56+
*
57+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`]
58+
*/
59+
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
60+
unchecked {
61+
uint256 length = buffer.length;
62+
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
63+
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
64+
if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) {
65+
return i - 1;
66+
}
67+
}
68+
return type(uint256).max;
69+
}
70+
}
71+
72+
/**
73+
* @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in
74+
* memory.
75+
*
76+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`]
77+
*/
78+
function slice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) {
79+
return slice(buffer, start, buffer.length);
80+
}
81+
82+
/**
83+
* @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in
84+
* memory.
85+
*
86+
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`]
87+
*/
88+
function slice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) {
89+
// sanitize
90+
uint256 length = buffer.length;
91+
end = Math.min(end, length);
92+
start = Math.min(start, end);
93+
94+
// allocate and copy
95+
bytes memory result = new bytes(end - start);
96+
assembly ("memory-safe") {
97+
mcopy(add(result, 0x20), add(buffer, add(start, 0x20)), sub(end, start))
98+
}
99+
100+
return result;
101+
}
102+
103+
/**
104+
* @dev Reads a bytes32 from a bytes array without bounds checking.
105+
*
106+
* NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the
107+
* assembly block as such would prevent some optimizations.
108+
*/
109+
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
110+
// This is not memory safe in the general case, but all calls to this private function are within bounds.
111+
assembly ("memory-safe") {
112+
value := mload(add(buffer, add(0x20, offset)))
113+
}
114+
}
115+
}

contracts/utils/CAIP10.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {SafeCast} from "./math/SafeCast.sol";
6+
import {Bytes} from "./Bytes.sol";
7+
import {CAIP2} from "./CAIP2.sol";
8+
import {Strings} from "./Strings.sol";
9+
10+
/**
11+
* @dev Helper library to format and parse CAIP-10 identifiers
12+
*
13+
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md[CAIP-10] defines account identifiers as:
14+
* account_id: chain_id + ":" + account_address
15+
* chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} (See {CAIP2})
16+
* account_address: [-.%a-zA-Z0-9]{1,128}
17+
*/
18+
library CAIP10 {
19+
using SafeCast for uint256;
20+
using Strings for address;
21+
using Bytes for bytes;
22+
23+
/// @dev Return the CAIP-10 identifier for an account on the current (local) chain.
24+
function local(address account) internal view returns (string memory) {
25+
return format(CAIP2.local(), account.toChecksumHexString());
26+
}
27+
28+
/**
29+
* @dev Return the CAIP-10 identifier for a given caip2 chain and account.
30+
*
31+
* NOTE: This function does not verify that the inputs are properly formatted.
32+
*/
33+
function format(string memory caip2, string memory account) internal pure returns (string memory) {
34+
return string.concat(caip2, ":", account);
35+
}
36+
37+
/**
38+
* @dev Parse a CAIP-10 identifier into its components.
39+
*
40+
* NOTE: This function does not verify that the CAIP-10 input is properly formatted. The `caip2` return can be
41+
* parsed using the {CAIP2} library.
42+
*/
43+
function parse(string memory caip10) internal pure returns (string memory caip2, string memory account) {
44+
bytes memory buffer = bytes(caip10);
45+
46+
uint256 pos = buffer.lastIndexOf(":");
47+
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
48+
}
49+
}

contracts/utils/CAIP2.sol

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {SafeCast} from "./math/SafeCast.sol";
6+
import {Bytes} from "./Bytes.sol";
7+
import {Strings} from "./Strings.sol";
8+
9+
/**
10+
* @dev Helper library to format and parse CAIP-2 identifiers
11+
*
12+
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] defines chain identifiers as:
13+
* chain_id: namespace + ":" + reference
14+
* namespace: [-a-z0-9]{3,8}
15+
* reference: [-_a-zA-Z0-9]{1,32}
16+
*/
17+
library CAIP2 {
18+
using SafeCast for uint256;
19+
using Strings for uint256;
20+
using Bytes for bytes;
21+
22+
/// @dev Return the CAIP-2 identifier for the current (local) chain.
23+
function local() internal view returns (string memory) {
24+
return format("eip155", block.chainid.toString());
25+
}
26+
27+
/**
28+
* @dev Return the CAIP-2 identifier for a given namespace and reference.
29+
*
30+
* NOTE: This function does not verify that the inputs are properly formatted.
31+
*/
32+
function format(string memory namespace, string memory ref) internal pure returns (string memory) {
33+
return string.concat(namespace, ":", ref);
34+
}
35+
36+
/**
37+
* @dev Parse a CAIP-2 identifier into its components.
38+
*
39+
* NOTE: This function does not verify that the CAIP-2 input is properly formatted.
40+
*/
41+
function parse(string memory caip2) internal pure returns (string memory namespace, string memory ref) {
42+
bytes memory buffer = bytes(caip2);
43+
44+
uint256 pos = buffer.indexOf(":");
45+
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
46+
}
47+
}

contracts/utils/README.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
3131
* {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type.
3232
* {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`].
3333
* {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648].
34+
* {Bytes}: Common operations on bytes objects.
3435
* {Strings}: Common operations for strings formatting.
3536
* {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.
3637
* {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays.
@@ -41,6 +42,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
4142
* {Packing}: A library for packing and unpacking multiple values into bytes32
4243
* {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].
4344
* {Comparators}: A library that contains comparator functions to use with with the {Heap} library.
45+
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
4446

4547
[NOTE]
4648
====

test/helpers/chains.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// NOTE: this file defines some examples of CAIP-2 and CAIP-10 identifiers.
2+
// The following listing does not pretend to be exhaustive or even accurate. It SHOULD NOT be used in production.
3+
4+
const { ethers } = require('hardhat');
5+
const { mapValues } = require('./iterate');
6+
7+
// EVM (https://axelarscan.io/resources/chains?type=evm)
8+
const ethereum = {
9+
Ethereum: '1',
10+
optimism: '10',
11+
binance: '56',
12+
Polygon: '137',
13+
Fantom: '250',
14+
fraxtal: '252',
15+
filecoin: '314',
16+
Moonbeam: '1284',
17+
centrifuge: '2031',
18+
kava: '2222',
19+
mantle: '5000',
20+
base: '8453',
21+
immutable: '13371',
22+
arbitrum: '42161',
23+
celo: '42220',
24+
Avalanche: '43114',
25+
linea: '59144',
26+
blast: '81457',
27+
scroll: '534352',
28+
aurora: '1313161554',
29+
};
30+
31+
// Cosmos (https://axelarscan.io/resources/chains?type=cosmos)
32+
const cosmos = {
33+
Axelarnet: 'axelar-dojo-1',
34+
osmosis: 'osmosis-1',
35+
cosmoshub: 'cosmoshub-4',
36+
juno: 'juno-1',
37+
'e-money': 'emoney-3',
38+
injective: 'injective-1',
39+
crescent: 'crescent-1',
40+
kujira: 'kaiyo-1',
41+
'secret-snip': 'secret-4',
42+
secret: 'secret-4',
43+
sei: 'pacific-1',
44+
stargaze: 'stargaze-1',
45+
assetmantle: 'mantle-1',
46+
fetch: 'fetchhub-4',
47+
ki: 'kichain-2',
48+
evmos: 'evmos_9001-2',
49+
aura: 'xstaxy-1',
50+
comdex: 'comdex-1',
51+
persistence: 'core-1',
52+
regen: 'regen-1',
53+
umee: 'umee-1',
54+
agoric: 'agoric-3',
55+
xpla: 'dimension_37-1',
56+
acre: 'acre_9052-1',
57+
stride: 'stride-1',
58+
carbon: 'carbon-1',
59+
sommelier: 'sommelier-3',
60+
neutron: 'neutron-1',
61+
rebus: 'reb_1111-1',
62+
archway: 'archway-1',
63+
provenance: 'pio-mainnet-1',
64+
ixo: 'ixo-5',
65+
migaloo: 'migaloo-1',
66+
teritori: 'teritori-1',
67+
haqq: 'haqq_11235-1',
68+
celestia: 'celestia',
69+
ojo: 'agamotto',
70+
chihuahua: 'chihuahua-1',
71+
saga: 'ssc-1',
72+
dymension: 'dymension_1100-1',
73+
fxcore: 'fxcore',
74+
c4e: 'perun-1',
75+
bitsong: 'bitsong-2b',
76+
nolus: 'pirin-1',
77+
lava: 'lava-mainnet-1',
78+
'terra-2': 'phoenix-1',
79+
terra: 'columbus-5',
80+
};
81+
82+
const makeCAIP = ({ namespace, reference, account }) => ({
83+
namespace,
84+
reference,
85+
account,
86+
caip2: `${namespace}:${reference}`,
87+
caip10: `${namespace}:${reference}:${account}`,
88+
toCaip10: other => `${namespace}:${reference}:${ethers.getAddress(other.target ?? other.address ?? other)}`,
89+
});
90+
91+
module.exports = {
92+
CHAINS: mapValues(
93+
Object.assign(
94+
mapValues(ethereum, reference => ({
95+
namespace: 'eip155',
96+
reference,
97+
account: ethers.Wallet.createRandom().address,
98+
})),
99+
mapValues(cosmos, reference => ({
100+
namespace: 'cosmos',
101+
reference,
102+
account: ethers.encodeBase58(ethers.randomBytes(32)),
103+
})),
104+
),
105+
makeCAIP,
106+
),
107+
getLocalCAIP: account =>
108+
ethers.provider.getNetwork().then(({ chainId }) => makeCAIP({ namespace: 'eip155', reference: chainId, account })),
109+
};

0 commit comments

Comments
 (0)