diff --git a/README.md b/README.md index 2b763e5f59..b7719d8e85 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ tokens ├─ ERC721 — "Simple ERC721 implementation with storage hitchhiking" ├─ WETH — "Simple Wrapped Ether implementation" utils +├─ Base58 — "Library for Base58 encoding and decoding" ├─ Base64 — "Library for Base64 encoding and decoding" ├─ CallContextChecker — "Call context checker mixin" ├─ CREATE3 — "Deterministic deployments agnostic to the initialization code" diff --git a/docs/utils/base58.md b/docs/utils/base58.md new file mode 100644 index 0000000000..cc55a2312c --- /dev/null +++ b/docs/utils/base58.md @@ -0,0 +1,44 @@ +# Base58 + +Library to encode strings in Base58. + + + + + + + + +## Custom Errors + +### Base58DecodingError() + +```solidity +error Base58DecodingError() +``` + +An unrecognized character was encountered during decoding. + +## Encoding / Decoding + +### encode(bytes) + +```solidity +function encode(bytes memory data) + internal + pure + returns (string memory result) +``` + +Encodes `data` into a Base58 string. + +### decode(string) + +```solidity +function decode(string memory encoded) + internal + pure + returns (bytes memory result) +``` + +Decodes `encoded`, a Base58 string, into the original bytes. \ No newline at end of file diff --git a/src/Milady.sol b/src/Milady.sol index 2e14615553..60da0907ac 100644 --- a/src/Milady.sol +++ b/src/Milady.sol @@ -23,6 +23,7 @@ import "./tokens/ERC4626.sol"; import "./tokens/ERC6909.sol"; import "./tokens/ERC721.sol"; import "./tokens/WETH.sol"; +import "./utils/Base58.sol"; import "./utils/Base64.sol"; import "./utils/CREATE3.sol"; import "./utils/CallContextChecker.sol"; diff --git a/src/utils/Base58.sol b/src/utils/Base58.sol new file mode 100644 index 0000000000..fcbe89b4f0 --- /dev/null +++ b/src/utils/Base58.sol @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +/// @notice Library to encode strings in Base58. +/// @author Solady (https://github.com/vectorized/solady/blob/main/src/utils/Base58.sol) +/// @author Modified from OpenZeppelin (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Base58.sol) +library Base58 { + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* CUSTOM ERRORS */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev An unrecognized character was encountered during decoding. + error Base58DecodingError(); + + /*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ + /* ENCODING / DECODING */ + /*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ + + /// @dev Encodes `data` into a Base58 string. + function encode(bytes memory data) internal pure returns (string memory result) { + uint256 l = data.length; + if (l == uint256(0)) return result; + /// @solidity memory-safe-assembly + assembly { + let b := add(data, 0x20) // Start of `data` bytes. + let z := 0 // Number of leading zero bytes in `data`. + // Count leading zero bytes. + for {} lt(byte(0, mload(add(b, z))), lt(z, l)) {} { z := add(1, z) } + + // Start the output offset by an over-estimate of the length. + let o := add(add(mload(0x40), 0x20), add(z, add(1, div(mul(sub(l, z), 8351), 6115)))) + let e := o + + let limbs := o + let limbsEnd := limbs + // Populate the uint248 limbs. + for { + let i := mod(l, 31) + if i { + mstore(limbsEnd, shr(shl(3, add(1, sub(31, i))), mload(b))) + limbsEnd := add(limbsEnd, 0x20) + } + } lt(i, l) { i := add(i, 31) } { + mstore(limbsEnd, shr(8, mload(add(b, i)))) + limbsEnd := add(limbsEnd, 0x20) + } + + // Use the extended scratch space for the lookup. We'll restore 0x40 later. + mstore(0x1f, "123456789ABCDEFGHJKLMNPQRSTUVWXY") + mstore(0x3f, "Zabcdefghijkmnopqrstuvwxyz") + + let w := not(0) // -1. + for {} 1 {} { + let i := limbs + for {} 1 {} { + if mload(i) { break } + i := add(i, 0x20) + if eq(i, limbsEnd) { break } + } + if eq(i, limbsEnd) { break } + + let carry := 0 + for { i := limbs } 1 {} { + let acc := add(shl(248, carry), mload(i)) + mstore(i, div(acc, 58)) + carry := mod(acc, 58) + i := add(i, 0x20) + if eq(i, limbsEnd) { break } + } + o := add(o, w) + mstore8(o, mload(carry)) + } + // We probably can optimize this more by writing 32 bytes at a time. + for {} z { z := add(z, w) } { + o := add(o, w) + mstore8(o, 49) // '1' in ASCII. + } + + let n := sub(e, o) // Compute the final length. + result := sub(o, 0x20) // Move back one word for the length. + mstore(result, n) // Store the length. + mstore(add(add(result, 0x20), n), 0) // Zeroize the slot after the bytes. + mstore(0x40, add(add(result, 0x40), n)) // Allocate memory. + } + } + + /// @dev Decodes `encoded`, a Base58 string, into the original bytes. + function decode(string memory encoded) internal pure returns (bytes memory result) { + uint256 n = bytes(encoded).length; + if (n == uint256(0)) return result; + /// @solidity memory-safe-assembly + assembly { + let s := add(encoded, 0x20) + let z := 0 // Number of leading '1' in `data`. + // Count leading '1'. + for {} and(eq(49, byte(0, mload(add(s, z)))), lt(z, n)) {} { z := add(1, z) } + + // Start the output offset by an over-estimate of the length. + let o := add(add(mload(0x40), 0x20), add(z, add(1, div(mul(sub(n, z), 7736), 10000)))) + let e := o + let limbs := o + let limbsEnd := limbs + let limbMask := shr(8, not(0)) + // Use the extended scratch space for the lookup. We'll restore 0x40 later. + mstore(0x2a, 0x30313233343536373839) + mstore(0x20, 0x1718191a1b1c1d1e1f20ffffffffffff2122232425262728292a2bff2c2d2e2f) + mstore(0x00, 0x000102030405060708ffffffffffffff090a0b0c0d0e0f10ff1112131415ff16) + + for { let j := 0 } 1 {} { + let c := sub(byte(0, mload(add(s, j))), 49) + // Check if the input character is valid. + if iszero(and(shl(c, 1), 0x3fff7ff03ffbeff01ff)) { + mstore(0x00, 0xe8fad793) // `Base58DecodingError()`. + revert(0x1c, 0x04) + } + let carry := byte(0, mload(c)) + for { let i := limbs } iszero(eq(i, limbsEnd)) { i := add(i, 0x20) } { + let acc := add(carry, mul(58, mload(i))) + mstore(i, and(limbMask, acc)) + carry := shr(248, acc) + } + // Carry will always be < 58. + if carry { + mstore(limbsEnd, carry) + limbsEnd := add(limbsEnd, 0x20) + } + j := add(j, 1) + if eq(j, n) { break } + } + // Copy and compact the uint248 limbs. + for { let i := limbs } iszero(eq(i, limbsEnd)) { i := add(i, 0x20) } { + o := sub(o, 31) + mstore(sub(o, 1), mload(i)) + } + // Strip any leading zeros from the limbs. + for {} lt(byte(0, mload(o)), lt(o, e)) {} { o := add(o, 1) } + o := sub(o, z) // Move back for the leading zero bytes. + calldatacopy(o, calldatasize(), z) // Fill the leading zero bytes. + + let l := sub(e, o) // Compute the final length. + result := sub(o, 0x20) // Move back one word for the length. + mstore(result, l) // Store the length. + mstore(add(add(result, 0x20), l), 0) // Zeroize the slot after the bytes. + mstore(0x40, add(add(result, 0x40), l)) // Allocate memory. + } + } +} diff --git a/test/Base58.t.sol b/test/Base58.t.sol new file mode 100644 index 0000000000..920623c027 --- /dev/null +++ b/test/Base58.t.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "./utils/SoladyTest.sol"; +import {Base58} from "../src/utils/Base58.sol"; +import {LibString} from "../src/utils/LibString.sol"; + +contract Base58Test is SoladyTest { + function testBase58EncodeDecode(bytes memory data, uint256 r) public { + if (r & 0x00f == 0) { + _brutalizeMemory(); + } + if (r & 0x0f0 == 0) { + _misalignFreeMemoryPointer(); + } + if (r & 0xf00 == 0) { + data = abi.encodePacked(new bytes(_bound(_random(), 0, 128)), data); + } + + uint256 h; + uint256 m; + /// @solidity memory-safe-assembly + assembly { + // Since `encode` writes memory backwards, we do some extra checks to ensure + // that the initial length overestimate is sufficient. + mstore(0x00, r) + mstore(0x20, "hehe") + h := keccak256(0x00, 0x40) + m := mload(0x40) + mstore(m, h) + mstore(0x40, add(m, 0x20)) + } + string memory encoded = Base58.encode(data); + /// @solidity memory-safe-assembly + assembly { + if iszero(eq(mload(m), h)) { invalid() } + } + + _checkMemory(encoded); + if (r & 0x00f000 == 0) { + _brutalizeMemory(); + } + if (r & 0x0f0000 == 0) { + _misalignFreeMemoryPointer(); + } + + /// @solidity memory-safe-assembly + assembly { + // Since `decode` writes memory backwards, we do some extra checks to ensure + // that the initial length overestimate is sufficient. + mstore(0x00, r) + mstore(0x20, "haha") + h := keccak256(0x00, 0x40) + m := mload(0x40) + mstore(m, h) + mstore(0x40, add(m, 0x20)) + } + bytes memory decoded = Base58.decode(encoded); + /// @solidity memory-safe-assembly + assembly { + if iszero(eq(mload(m), h)) { invalid() } + } + + _checkMemory(decoded); + assertEq(data, decoded); + } + + function testBase58EncodeDecode() public { + this._testBase58EncodeDecode(hex"", ""); + this._testBase58EncodeDecode(hex"0d", "E"); + this._testBase58EncodeDecode(hex"000e", "1F"); + this._testBase58EncodeDecode(hex"00f3", "15C"); + this._testBase58EncodeDecode(hex"00", "1"); + this._testBase58EncodeDecode(hex"f2", "5B"); + this._testBase58EncodeDecode(hex"0002da", "1Db"); + this._testBase58EncodeDecode(hex"0027b9", "142L"); + this._testBase58EncodeDecode(hex"00d80f", "1HSe"); + this._testBase58EncodeDecode(hex"ce", "4Z"); + this._testBase58EncodeDecode(hex"7c", "39"); + this._testBase58EncodeDecode(hex"cd0b5dfe722552f609ce", "CX9VkoSqX63kbo"); + this._testBase58EncodeDecode( + hex"00598b3dc0966af86beb7898fc9921c2fbc38a19d52dee9dfed69e3d", + "1D6w66tNCxvikkpma3BXnRnABJQojACXjHxtdJ" + ); + this._testBase58EncodeDecode( + hex"09100a2fc14628f168c2c9b980fb840857fbb9fe031013c9bf7e218d5c", + "Qs1VMdvTSeZkZ5p4e4xQaLa8J3ptpJzJAcM1Mp7" + ); + this._testBase58EncodeDecode( + hex"001d85089c34888205378be7e8f9ff5e2f", "14eRVxHMi5hh14FM9Gpd1Ua" + ); + this._testBase58EncodeDecode( + hex"0090ccbb306b1cc8f226e905623d19604fd0ad73bd80b8b4712e", + "121GLNsu9Tdp147zdSjFvJudL1pp1Qv39myF" + ); + this._testBase58EncodeDecode(hex"012ee97bcab1", "bB9gNQp"); + this._testBase58EncodeDecode( + hex"00f91f623af2d76e8ee2abdbfe5e3671373ad4736d2433397c93e08e63e9ce1830", + "1HmUGpDZUwcvX5xPMNQ9oHoMz8nKQF6EWgqno2iSXQJc7" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000000000000000000000000000fb53beb02ab2ab6583638677b592b2b56f321d94972b38acfd6d4cd1202f77ff1fddf68b9d2c4bdb1b6ced6ef31e282e48790854ce9c0ab93435761d0f5db1e16817119e682391a23f633d9cdd6481a07585ec17d6aeca0849eab41f5895cfc4e9503f97345a364964d7e024c947ae7c238d1a4705", + "1111111111111111111111111111111QVs5qPAkBrBEtm1UXSAcNGHgA6cUYDn4oAXAxgEQ5jntH1aWoie6t7a1j2RTmP4E5uGFpWwTUj7zyeKcs7BKMXJBRXHuokJ13KmbHC6RLtAbUdobwBcjx2UjCK5rPwVBjABFvjgGAaFFwEZgnRPudGLqLqJdCx6G" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000031f89a3264997ab236bf9c5200a5353c4e04a134ad572583a140f9c3cc7d4f3f6331716c", + "11111111111111111111P1So9spX62PHGLRTG8SgU1Lm19f6SgCozbp8Use7LdcMGYAKD" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000013bc3b22341190c36a1cda2a0a1ed6f93a080447160b626b2f711c9a266bdc17622794eb9d", + "11111111111fN3TfvHm4xfWWJT7FqjTP9WXJiCoKJejeRgQmU6LcWUgJezrH2" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b1859e09c90d4ac3dd4c89c3c2452b4d22c92d0eee246e75d72d7209078d5230159b5e52249d5b6017e6992696028d390c61d26c0d42395072378ad89df7b94dbef0624cd0e1e091829c6292e9cf8303b43bec", + "1111111111111111111111111111111111111111111111111113sLfBS3DP3qb3hQoRZDt1DotgrCJU5N17jh4bFG6cs5KGz4Z1wqWJC6bHVbgKYXAUMVvoqFbZAzC5Rg95xvsbmhTLkuH3jbPkqiXuGcRm2wGmiom8f" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000322a08fd25e4a383cf093664c6a28531bdfa2e6d3a0afdddae9a58fe1074642f979d6cc1dd196e77d63fac03e9e52815bb211a760e37470b006e9682a8d432", + "11111111111111111111111111111111111111111111111111111111111EBQAEDu1wmQTkQGGtoxiwzZzxPp3q6jswQS8BoqBuKzxhmVg8ThLq8Z6HjrcBPX5BsGYWTA7sjHP9CeASeFEsj" + ); + this._testBase58EncodeDecode( + hex"00000000000000c305c2e9ca1fed1817ff8ddc60fb26b5665ce958f9cbeb3f907e6ae500d5917d24b3b30b0d9e382e9521eeb232c7f5d328f0e239cec44d21d49472727a1ec7555580c88f2776", + "11111119a4d2p5cardGk5zgtKRV5xmbugoNWw3fp8eWTq3sjbVTYr7aXiE3wGzqe5bGgusiYsBzvdPibo5BVNxaxudCXc6WkikU8xwP" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005fcee37b107ba13473fff385e60e48f7085c72ca9fa64af6c21a568a281161db2af9844f52867ea6048a502da3dd827158b199f1c7e330028b849e135d7f5418923e", + "11111111111111111111111111111111111111111111111111eKPzXLFs4zguFbvYgRSoJDb6mTyCPFhve4Yww4PNDs89z8Kxd72pkXqDix6FkN62WxgNfrNry43pbizFaUuzaExHCy" + ); + this._testBase58EncodeDecode( + hex"00000013dff1618a82531e334a62f0de8f17c074732abf4c59cd7c", + "1112p67UFfiuvQD92aDbWzpm1Z5EU6Q8TtpB" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000003fd220658684b1ccef6552", "11111111111GpuXVbXqt99pach" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000e8271fe396fe0cf6bcc1f7881c9cbae4d147bbb61e947fb129177acd9887c0dca8348ffd2a385ecdac852073b60d7daf003d6e188c1841ddc5b68c3b9e3eda4c090f3567c54bd372676ee0ff9fbfede9", + "111111111111111111111111113Xj8EySrK3wEvmYt59y4b1vxzjGwPc688wZA4jXsyGNgEh7ghaP9ohvqseUcNUUCeqLxtyzWDPfngB1iHzjsEeaN14UjSeSZdCoWUSVHAYVtX2" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000000000000000000000000000000000003c58cebe89322eee5505a37a842fb64cd726c8a8adaa8a20c34de8eaff08373abe2f5912c912dde618fcb0b4a8a14da4f3fbc7d6004684fcedd7f133af9abe9360793485dbe855f3875e631c96d8ee775240cac29f2c8640aa9485f8c6b6410c0ac7fc83", + "1111111111111111111111111111111111113M2hXzVT7xgwnteR7ZsprbP5xBdDBWkkQHpVUJScKLca3WAfPSCUQ59d3a4zGU4P2Q5Dvgmz9LVbf1erXBmxLsha5PEhFmHDBpyGW5ZFSJswPtGRYQeVPWKpr3cbQbexUFsNpqVea" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000060234520525c5c627553370b53eb6d76c7766490efc4dae6fd5c5940008b5110eb834a2168c9728d51840c4e571321d4f08391009a0c3785c6c6b9b14d774629995acf59bf07f88b2762b426ddd135516a24daf2", + "111111111111111111111111111111117rbt7xcf57aaNKwQwTxYHWqqtGYNiSM63bYVGhBKNExhvAubcT68EToxSShmawAr33vALSbua2s1xt9C6yPCXU5dGA8cr1B8GS6WXVCh24heoNrUbSR" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000003b4c61dee11d868b463c055521a78d6552daefdfdc3fc03216cd84667365c0346a2954fe099bdf4baea658b3cee9589c6d217f8b3642", + "11111111111111111111111111111111115nJEXtj9QijtXd2gqb9bgPvanHGcXLdhKX14UbmuitdejFDnZ5MiGpHALxfnMVvo4CbVKcPHhK" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000013ba6b7a5b28ee62e23e2f037169950bfaa76a49cd560bf283cb7a76eab12b0766f61979108cd0bed77d37", + "111111111111112THktPmYvuwnWuVqbsNgWfAaR1dxCTi6zHB6ggkAidRRhwrrWmL3PGGCJ2J" + ); + this._testBase58EncodeDecode( + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000e357a949bd269668402d9fe64611b55659fa5c077ed6896ba83c99f6362ff3ddfb145512bd7825d25d99b62f392d42c397191d91a85cb3aac3f5aba7a1f8c7c5098ea8d8e452eab896ce53510d58f64518509dc4c9a93f9feccb8040b18fc8065e9811e954c4c421264d528ae0817f4981a11bfe", + "1111111111111111111111111111111111111115pJo5spTW949hEVQhtDHsyhLcMfUC6gHCM9pPGYter8CD12cNuXbshZvtDZoFAjwjuCftRhweQ7TcuARE52aqNzC5He3rqs1YdEreCTYqoPuQDqufzeDjywYJQmkgwaLrDbhKcjUbxFVMertvBGeUkB7tDiSMBs" + ); + this._testBase58EncodeDecode( + hex"00000000000000000000000000000000000000af64ba6caafbbca128653eff8c51", + "111111111111111111127UskEy2WhgsTGAY4Qi4" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000f744c6b510384801d5d10035f00b562f05c585aac1fe57f27b640096aafc1cb28a859d7ece16ee8c6813708193047aafd18c4", + "111111111111qkJMpKZLaSweHBvLEBUxhcB7AnaBv6QRC7mkQS54QEuh8PYp7pn7VfueHTsD4W1gF7px3" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000000000000000a4ae531a4546129e810fe716bef089bf466eed25dd729688c82481c3eea5cec22219d7dfcccb814a141dd14b98677f905c5efbc38f65040216f27042dcdf31ac81eff75985176ed0a40ae16eadca11464e40f16ee8fc2fd06ec6629d098759365df73073e4d4124b6003457367a484dc278b2e", + "11111111111111111111nqeSEB5S7wspidSeWX3LCxrjoSxFcAEE3C9LFix6tQHFWPMUWMSoFf2rStXF5JxarB4Dhxvnhbanz6mLyMerUKxU6WFag1vQMgCTLeeNHcRiu1srGtHB3YrVgoz6h2mAHyhs1ukVrGYmSmmKRty75Jx4zJ661" + ); + this._testBase58EncodeDecode( + hex"000000451c0d1e6a9b414cd8ee1d1fa7f7805de82932a48991c9edaf814215d069d5f1fef3a63f931792b2d113ce0a309d5e22a1d9ede1cca2e7e358e9d2600498f2e9a0c8159cebffa293512fe5b0f3d9971bb2a07d1b7df5f81af612141e4693147428285c21621c8e772a627ca1a9", + "1119kd1Rja2XCCMQDiRBv7EEX5upePsrmhBRHL4kkK8oBR3dSvVE4yuRtUzZKmtvWiBUD6qFk3HCoWpDapRw7WhBi3qR94ZeLkFuAZbqN4D2N4V6AgLdh2n1JwM3Z8i9quwNPHY4TNU2B2NYwZ2tons2" + ); + this._testBase58EncodeDecode( + hex"0000000000000000000000000000cc50f101efa6b062341bdc59edc70e9776728db82f5779100210d5907ebbe56673ef72e013987a297d560ad9e2229c508ce3568e5dd0f61e671e15f21521f9206a435c634b1f0d254326965d6c6eac24aec4b7fecc84b753d76d4e1bca902d662deb23a678f6cc1811", + "111111111111114xLrKJa5yzRCpMhRUMMv6WoK9hkibnv9rVbDQgxq5oWvSUv3UBJUjQ5xeFapu4ZZ4nDoi1C34c115cirJd5f1LucLojD45CuiGWNbCFQsPcQPfpkheZe1b5xWGsBPZzvZHxUaik4YAtk5sEg" + ); + } + + function _testBase58EncodeDecode(bytes memory data, string memory expectedEncoded) public { + string memory encoded = Base58.encode(data); + assertEq(expectedEncoded, encoded); + bytes memory decoded = Base58.decode(encoded); + assertEq(data, decoded); + } + + function testCarryBoundsTrick(uint248 limb, uint8 carry) public pure { + if (carry < 58) { + uint256 acc = uint256(limb) * 58 + uint256(carry); + assert((acc >> 248) < 58); + } + } + + function check_CarryBoundsTrick(uint248 limb, uint8 carry) public pure { + testCarryBoundsTrick(limb, carry); + } +}