Repository: pcaversaccio/snekmate (language: python)

Issue:
💥 `Base64` Util
Provide a set of functions to operate with `Base64` strings. See also [here](https://github.com/horsefacts/commit-reveal/blob/main/contracts/Base64.vy).


Discussion / hints:

Fix patch:
diff --git a/.gas-snapshot b/.gas-snapshot
index be330023..a6b78bf0 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -101,61 +101,61 @@ ERC20Test:testTransferOwnershipToZeroAddress() (gas: 13705)
 ERC20Test:testTransferSuccess() (gas: 42042)
 ERC20Test:testTransferToZeroAddress() (gas: 14721)
 ERC20Test:testTransferZeroTokens() (gas: 24114)
-ERC721Test:testApproveClearingApprovalWithNoPriorApproval() (gas: 174628)
-ERC721Test:testApproveClearingApprovalWithPriorApproval() (gas: 182903)
-ERC721Test:testApproveFromApprovedAddress() (gas: 197003)
-ERC721Test:testApproveFromNonOwner() (gas: 169990)
-ERC721Test:testApproveFromOperatorAddress() (gas: 221443)
-ERC721Test:testApproveInvalidTokenId() (gas: 169187)
-ERC721Test:testApproveToOwner() (gas: 166869)
-ERC721Test:testApproveToZeroAddress() (gas: 182969)
-ERC721Test:testApproveWithNoPriorApproval() (gas: 194918)
-ERC721Test:testApproveWithPriorApprovalToDifferentAddress() (gas: 203234)
-ERC721Test:testApproveWithPriorApprovalToSameAddress() (gas: 202886)
-ERC721Test:testBalanceOfCase1() (gas: 327261)
+ERC721Test:testApproveClearingApprovalWithNoPriorApproval() (gas: 174531)
+ERC721Test:testApproveClearingApprovalWithPriorApproval() (gas: 182806)
+ERC721Test:testApproveFromApprovedAddress() (gas: 196906)
+ERC721Test:testApproveFromNonOwner() (gas: 169893)
+ERC721Test:testApproveFromOperatorAddress() (gas: 221346)
+ERC721Test:testApproveInvalidTokenId() (gas: 169090)
+ERC721Test:testApproveToOwner() (gas: 166772)
+ERC721Test:testApproveToZeroAddress() (gas: 182872)
+ERC721Test:testApproveWithNoPriorApproval() (gas: 194821)
+ERC721Test:testApproveWithPriorApprovalToDifferentAddress() (gas: 203137)
+ERC721Test:testApproveWithPriorApprovalToSameAddress() (gas: 202789)
+ERC721Test:testBalanceOfCase1() (gas: 327067)
 ERC721Test:testBalanceOfCase2() (gas: 10668)
 ERC721Test:testBalanceOfZeroAddress() (gas: 8936)
-ERC721Test:testBurnSuccess() (gas: 304702)
-ERC721Test:testBurnSuccessViaApproveAndSetApprovalForAll() (gas: 370086)
+ERC721Test:testBurnSuccess() (gas: 304547)
+ERC721Test:testBurnSuccessViaApproveAndSetApprovalForAll() (gas: 369931)
 ERC721Test:testCachedDomainSeparator() (gas: 8044)
 ERC721Test:testDomainSeparator() (gas: 9822)
-ERC721Test:testGetApprovedApprovedTokenId() (gas: 192477)
+ERC721Test:testGetApprovedApprovedTokenId() (gas: 192380)
 ERC721Test:testGetApprovedInvalidTokenId() (gas: 11145)
-ERC721Test:testGetApprovedNotApprovedTokenId() (gas: 167125)
+ERC721Test:testGetApprovedNotApprovedTokenId() (gas: 167028)
 ERC721Test:testHasOwner() (gas: 10166)
 ERC721Test:testInitialSetup() (gas: 18473)
-ERC721Test:testOwnerOf() (gas: 164838)
+ERC721Test:testOwnerOf() (gas: 164741)
 ERC721Test:testOwnerOfInvalidTokenId() (gas: 11077)
-ERC721Test:testPermitBadChainId() (gas: 196802)
-ERC721Test:testPermitBadNonce() (gas: 195863)
-ERC721Test:testPermitExpiredDeadline() (gas: 168830)
-ERC721Test:testPermitOtherSignature() (gas: 197186)
-ERC721Test:testPermitReplaySignature() (gas: 230653)
-ERC721Test:testPermitSuccess() (gas: 226196)
+ERC721Test:testPermitBadChainId() (gas: 196705)
+ERC721Test:testPermitBadNonce() (gas: 195766)
+ERC721Test:testPermitExpiredDeadline() (gas: 168733)
+ERC721Test:testPermitOtherSignature() (gas: 197089)
+ERC721Test:testPermitReplaySignature() (gas: 230556)
+ERC721Test:testPermitSuccess() (gas: 226099)
 ERC721Test:testRenounceOwnershipNonOwner() (gas: 11311)
 ERC721Test:testRenounceOwnershipSuccess() (gas: 18070)
 ERC721Test:testSafeMintNonMinter() (gas: 12264)
-ERC721Test:testSafeMintOverflow() (gas: 12584)
-ERC721Test:testSafeMintReceiverContract() (gas: 392847)
-ERC721Test:testSafeMintReceiverContractFunctionNotImplemented() (gas: 116900)
-ERC721Test:testSafeMintReceiverContractInvalidReturnIdentifier() (gas: 345831)
-ERC721Test:testSafeMintReceiverContractRevertsWithMessage() (gas: 343097)
-ERC721Test:testSafeMintReceiverContractRevertsWithPanic() (gas: 343344)
-ERC721Test:testSafeMintReceiverContractRevertsWithoutMessage() (gas: 342725)
-ERC721Test:testSafeMintSuccess() (gas: 515414)
-ERC721Test:testSafeMintToZeroAddress() (gas: 36709)
-ERC721Test:testSafeMintTokenAlreadyMinted() (gas: 354883)
-ERC721Test:testSafeTransferFromNoData() (gas: 1153794)
-ERC721Test:testSafeTransferFromReceiverFunctionNotImplemented() (gas: 181607)
-ERC721Test:testSafeTransferFromReceiverInvalidReturnIdentifier() (gas: 410629)
-ERC721Test:testSafeTransferFromReceiverRevertsWithMessage() (gas: 407878)
-ERC721Test:testSafeTransferFromReceiverRevertsWithPanic() (gas: 408148)
-ERC721Test:testSafeTransferFromReceiverRevertsWithoutMessage() (gas: 407528)
-ERC721Test:testSafeTransferFromWithData() (gas: 1163104)
-ERC721Test:testSetApprovalForAllOperatorIsOwner() (gas: 166396)
-ERC721Test:testSetApprovalForAllSuccessCase1() (gas: 193835)
-ERC721Test:testSetApprovalForAllSuccessCase2() (gas: 187606)
-ERC721Test:testSetApprovalForAllSuccessCase3() (gas: 200667)
+ERC721Test:testSafeMintOverflow() (gas: 12487)
+ERC721Test:testSafeMintReceiverContract() (gas: 392750)
+ERC721Test:testSafeMintReceiverContractFunctionNotImplemented() (gas: 116800)
+ERC721Test:testSafeMintReceiverContractInvalidReturnIdentifier() (gas: 345734)
+ERC721Test:testSafeMintReceiverContractRevertsWithMessage() (gas: 343000)
+ERC721Test:testSafeMintReceiverContractRevertsWithPanic() (gas: 343247)
+ERC721Test:testSafeMintReceiverContractRevertsWithoutMessage() (gas: 342628)
+ERC721Test:testSafeMintSuccess() (gas: 515026)
+ERC721Test:testSafeMintToZeroAddress() (gas: 36612)
+ERC721Test:testSafeMintTokenAlreadyMinted() (gas: 354592)
+ERC721Test:testSafeTransferFromNoData() (gas: 1153639)
+ERC721Test:testSafeTransferFromReceiverFunctionNotImplemented() (gas: 181510)
+ERC721Test:testSafeTransferFromReceiverInvalidReturnIdentifier() (gas: 410532)
+ERC721Test:testSafeTransferFromReceiverRevertsWithMessage() (gas: 407781)
+ERC721Test:testSafeTransferFromReceiverRevertsWithPanic() (gas: 408051)
+ERC721Test:testSafeTransferFromReceiverRevertsWithoutMessage() (gas: 407431)
+ERC721Test:testSafeTransferFromWithData() (gas: 1162949)
+ERC721Test:testSetApprovalForAllOperatorIsOwner() (gas: 166299)
+ERC721Test:testSetApprovalForAllSuccessCase1() (gas: 193738)
+ERC721Test:testSetApprovalForAllSuccessCase2() (gas: 187509)
+ERC721Test:testSetApprovalForAllSuccessCase3() (gas: 200570)
 ERC721Test:testSetMinterNonOwner() (gas: 11759)
 ERC721Test:testSetMinterRemoveOwnerAddress() (gas: 14130)
 ERC721Test:testSetMinterSuccess() (gas: 33472)
@@ -163,20 +163,35 @@ ERC721Test:testSetMinterToZeroAddress() (gas: 13917)
 ERC721Test:testSupportsInterfaceGasCost() (gas: 5754)
 ERC721Test:testSupportsInterfaceInvalidInterfaceId() (gas: 5641)
 ERC721Test:testSupportsInterfaceSuccess() (gas: 9386)
-ERC721Test:testTokenByIndex() (gas: 558031)
-ERC721Test:testTokenByIndexOutOfBounds() (gas: 329731)
-ERC721Test:testTokenOfOwnerByIndex() (gas: 577757)
-ERC721Test:testTokenOfOwnerByIndexReverts() (gas: 550248)
-ERC721Test:testTokenURIAfterBurning() (gas: 138060)
-ERC721Test:testTokenURIDefault() (gas: 167876)
+ERC721Test:testTokenByIndex() (gas: 557643)
+ERC721Test:testTokenByIndexOutOfBounds() (gas: 329537)
+ERC721Test:testTokenOfOwnerByIndex() (gas: 577369)
+ERC721Test:testTokenOfOwnerByIndexReverts() (gas: 549957)
+ERC721Test:testTokenURIAfterBurning() (gas: 137982)
+ERC721Test:testTokenURIDefault() (gas: 167794)
 ERC721Test:testTokenURIInvalidTokenId() (gas: 11585)
-ERC721Test:testTokenURINoBaseURI() (gas: 2522998)
-ERC721Test:testTokenURINoTokenUri() (gas: 124927)
-ERC721Test:testTotalSupply() (gas: 327358)
-ERC721Test:testTransferFrom() (gas: 554624)
+ERC721Test:testTokenURINoBaseURI() (gas: 2521716)
+ERC721Test:testTokenURINoTokenUri() (gas: 124830)
+ERC721Test:testTotalSupply() (gas: 327164)
+ERC721Test:testTransferFrom() (gas: 554469)
 ERC721Test:testTransferOwnershipNonOwner() (gas: 11785)
 ERC721Test:testTransferOwnershipSuccess() (gas: 21752)
 ERC721Test:testTransferOwnershipToZeroAddress() (gas: 13891)
+Base64Test:testDataLengthMismatch() (gas: 12503)
+Base64Test:testDecodeEmptyString() (gas: 7978)
+Base64Test:testDecodeSafeUrl() (gas: 321150)
+Base64Test:testDecodeSentence() (gas: 814472)
+Base64Test:testDecodeSingleCharacter() (gas: 153715)
+Base64Test:testDecodeWithDoublePadding() (gas: 226418)
+Base64Test:testDecodeWithNoPadding() (gas: 179634)
+Base64Test:testDecodeWithSinglePadding() (gas: 189896)
+Base64Test:testEncodeEmptyString() (gas: 9139)
+Base64Test:testEncodeSafeUrl() (gas: 83856)
+Base64Test:testEncodeSentence() (gas: 111820)
+Base64Test:testEncodeSingleCharacter() (gas: 65940)
+Base64Test:testEncodeWithDoublePadding() (gas: 71685)
+Base64Test:testEncodeWithNoPadding() (gas: 70338)
+Base64Test:testEncodeWithSinglePadding() (gas: 71788)
 BatchDistributorTest:testDistributeEtherMultipleAddressesSuccess() (gas: 145962)
 BatchDistributorTest:testDistributeEtherOneAddressSuccess() (gas: 74891)
 BatchDistributorTest:testDistributeEtherRevertWithInsufficientFunds() (gas: 76796)
@@ -211,27 +226,27 @@ CreateAddressTest:testComputeAddressSelfNonceUint64() (gas: 13353)
 CreateAddressTest:testComputeAddressSelfNonceUint8() (gas: 12954)
 CreateAddressTest:testComputeAddressSelfRevertTooHighNonce() (gas: 9168)
 ECDSATest:testEthSignedMessageHash() (gas: 5802)
-ECDSATest:testRecoverWith0x00Value() (gas: 13956)
-ECDSATest:testRecoverWithArbitraryMessage() (gas: 19318)
-ECDSATest:testRecoverWithCorrectVersion() (gas: 20088)
-ECDSATest:testRecoverWithInvalidSignature() (gas: 13541)
-ECDSATest:testRecoverWithTooHighSValue() (gas: 11696)
+ECDSATest:testRecoverWith0x00Value() (gas: 13971)
+ECDSATest:testRecoverWithArbitraryMessage() (gas: 19348)
+ECDSATest:testRecoverWithCorrectVersion() (gas: 20118)
+ECDSATest:testRecoverWithInvalidSignature() (gas: 13556)
+ECDSATest:testRecoverWithTooHighSValue() (gas: 11711)
 ECDSATest:testRecoverWithTooLongSignature() (gas: 9947)
-ECDSATest:testRecoverWithTooShortSignature() (gas: 9570)
-ECDSATest:testRecoverWithValidSignature() (gas: 19401)
-ECDSATest:testRecoverWithWrongMessage() (gas: 19396)
-ECDSATest:testRecoverWithWrongVersion() (gas: 13939)
+ECDSATest:testRecoverWithTooShortSignature() (gas: 9585)
+ECDSATest:testRecoverWithValidSignature() (gas: 19431)
+ECDSATest:testRecoverWithWrongMessage() (gas: 19426)
+ECDSATest:testRecoverWithWrongVersion() (gas: 13954)
 ECDSATest:testToTypedDataHash() (gas: 5882)
 EIP712DomainSeparatorTest:testCachedDomainSeparatorV4() (gas: 7575)
 EIP712DomainSeparatorTest:testDomainSeparatorV4() (gas: 9234)
 EIP712DomainSeparatorTest:testHashTypedDataV4() (gas: 10876)
-MerkleProofVerificationTest:testInvalidMerkleMultiProof() (gas: 412478916)
+MerkleProofVerificationTest:testInvalidMerkleMultiProof() (gas: 412478466)
 MerkleProofVerificationTest:testInvalidMerkleProof() (gas: 33970262)
 MerkleProofVerificationTest:testInvalidMerkleProofLength() (gas: 33972496)
 MerkleProofVerificationTest:testInvalidMultiProof() (gas: 909634320)
 MerkleProofVerificationTest:testMultiProofEdgeCase1() (gas: 412458074)
 MerkleProofVerificationTest:testMultiProofEdgeCase2() (gas: 412458157)
-MerkleProofVerificationTest:testMultiProofVerify() (gas: 412483025)
+MerkleProofVerificationTest:testMultiProofVerify() (gas: 412482395)
 MerkleProofVerificationTest:testVerify() (gas: 67939287)
 MulticallsTest:testMulticallRevert() (gas: 545361187)
 MulticallsTest:testMulticallSelfRevert() (gas: 1090159014)
@@ -245,12 +260,12 @@ MulticallsTest:testMulticallValueSelfSuccess() (gas: 1091257158)
 MulticallsTest:testMulticallValueSuccess() (gas: 545926196)
 MulticallsTest:testMultistaticcallRevert() (gas: 8937393460525250605)
 MulticallsTest:testMultistaticcallSuccess() (gas: 545336832)
-SignatureCheckerTest:testEIP1271WithInvalidSignature1() (gas: 25066)
-SignatureCheckerTest:testEIP1271WithInvalidSignature2() (gas: 15987)
-SignatureCheckerTest:testEIP1271WithInvalidSigner() (gas: 24995)
-SignatureCheckerTest:testEIP1271WithMaliciousWallet() (gas: 19035)
-SignatureCheckerTest:testEIP1271WithValidSignature() (gas: 25099)
-SignatureCheckerTest:testEOAWithInvalidSignature1() (gas: 16799)
-SignatureCheckerTest:testEOAWithInvalidSignature2() (gas: 14235)
-SignatureCheckerTest:testEOAWithInvalidSigner() (gas: 16810)
-SignatureCheckerTest:testEOAWithValidSignature() (gas: 13672)
+SignatureCheckerTest:testEIP1271WithInvalidSignature1() (gas: 25081)
+SignatureCheckerTest:testEIP1271WithInvalidSignature2() (gas: 16002)
+SignatureCheckerTest:testEIP1271WithInvalidSigner() (gas: 25010)
+SignatureCheckerTest:testEIP1271WithMaliciousWallet() (gas: 19050)
+SignatureCheckerTest:testEIP1271WithValidSignature() (gas: 25114)
+SignatureCheckerTest:testEOAWithInvalidSignature1() (gas: 16814)
+SignatureCheckerTest:testEOAWithInvalidSignature2() (gas: 14250)
+SignatureCheckerTest:testEOAWithInvalidSigner() (gas: 16825)
+SignatureCheckerTest:testEOAWithValidSignature() (gas: 13687)
diff --git a/.gitmodules b/.gitmodules
index c47670ed..0f1965e8 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -9,4 +9,7 @@
 	url = https://github.com/GNSPS/solidity-bytes-utils.git
 [submodule "lib/openzeppelin-contracts"]
 	path = lib/openzeppelin-contracts
-	url = https://github.com/OpenZeppelin/openzeppelin-contracts
+	url = https://github.com/OpenZeppelin/openzeppelin-contracts.git
+[submodule "lib/prb-test"]
+	path = lib/prb-test
+	url = https://github.com/paulrberg/prb-test.git
diff --git a/.prettierignore b/.prettierignore
index 32e01dfa..84eeb5e2 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1,6 +1,7 @@
 node_modules
-lib/create-util
+lib/prb-test
 lib/forge-std
+lib/create-util
 lib/solidity-bytes-utils
 lib/openzeppelin-contracts
 bin
diff --git a/.solhintignore b/.solhintignore
index 5f596cee..0d01aed5 100644
--- a/.solhintignore
+++ b/.solhintignore
@@ -1,5 +1,6 @@
 node_modules
-lib/create-util
+lib/prb-test
 lib/forge-std
+lib/create-util
 lib/solidity-bytes-utils
 lib/openzeppelin-contracts
diff --git a/README.md b/README.md
index 0eef462d..cbfa52ba 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,7 @@ src
    ├─ Multicall — "Multicall Functions"
    ├─ SignatureChecker — "ECDSA and EIP-1271 Signature Verification Function"
    ├─ BatchDistributor — "Batch Sending Both Native and ERC-20 Tokens"
+   ├─ Base64 — "Base64 Encoding and Decoding Functions"
 ```
 
 ## 🙏🏼 Acknowledgements
diff --git a/lib/create-util b/lib/create-util
index 259886ef..1dc296b6 160000
--- a/lib/create-util
+++ b/lib/create-util
@@ -1,1 +1,1 @@
-Subproject commit 259886ef110a3c5823088a61c61af812ebb1b33e
+Subproject commit 1dc296b690338a072095222b5bd971c81644d7ab
diff --git a/lib/forge-std b/lib/forge-std
index 76e89e59..53331f4c 160000
--- a/lib/forge-std
+++ b/lib/forge-std
@@ -1,1 +1,1 @@
-Subproject commit 76e89e592ac58df4f0d16202ee230bbf0b8098fc
+Subproject commit 53331f4cb2e313466f72440f3e73af048c454d02
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
index 2336bd3e..88754d0b 160000
--- a/lib/openzeppelin-contracts
+++ b/lib/openzeppelin-contracts
@@ -1,1 +1,1 @@
-Subproject commit 2336bd3e8e050d0e81caf30e89dcc9a9106e1a1f
+Subproject commit 88754d0b36cde4e6b1daab591058eaef449f5adb
diff --git a/remappings.txt b/remappings.txt
index c88acd2c..0466ab64 100644
--- a/remappings.txt
+++ b/remappings.txt
@@ -1,4 +1,5 @@
 utils/=lib/utils/
+prb/test/=lib/prb-test/src/
 forge-std/=lib/forge-std/src/
 create-util/=lib/create-util/contracts/
 openzeppelin/=lib/openzeppelin-contracts/contracts/
diff --git a/src/tokens/ERC721.vy b/src/tokens/ERC721.vy
index 0d5e4f59..d4ae60fc 100644
--- a/src/tokens/ERC721.vy
+++ b/src/tokens/ERC721.vy
@@ -453,8 +453,9 @@ def tokenURI(token_id: uint256) -> String[512]:
     self._require_minted(token_id)
     token_uri: String[432] = self._token_uris[token_id]
 
+    base_uri_length: uint256 = len(_BASE_URI)
     # If there is no base URI, return the token URI.
-    if (len(_BASE_URI) == empty(uint256)):
+    if (base_uri_length == empty(uint256)):
         return token_uri
 
     # If both are set, concatenate the base URI
@@ -464,7 +465,7 @@ def tokenURI(token_id: uint256) -> String[512]:
 
     # If there is no token URI but a base URI,
     # concatenate the base URI and token ID.
-    if (len(_BASE_URI) != empty(uint256)):
+    if (base_uri_length != empty(uint256)):
         return concat(_BASE_URI, uint2str(token_id))
     else:
         return ""
@@ -672,7 +673,7 @@ def _exists(token_id: uint256) -> bool:
             Tokens start existing when they are minted (`_mint`),
             and stop existing when they are burned (`_burn`).
     @param token_id The 32-byte identifier of the token.
-    @return The verification whether `token_id` exists
+    @return bool The verification whether `token_id` exists
             or not.
     """
     return self._owners[token_id] != empty(address)
diff --git a/src/utils/Base64.vy b/src/utils/Base64.vy
new file mode 100644
index 00000000..693babb0
--- /dev/null
+++ b/src/utils/Base64.vy
@@ -0,0 +1,272 @@
+# @version ^0.3.7
+"""
+@title Base64 Encoding and Decoding Functions
+@license GNU Affero General Public License v3.0
+@author pcaversaccio
+@notice These functions can be used to encode bytes or to decode strings
+        using the Base64 binary-to-text encoding scheme. For more details,
+        please refer to: https://www.rfc-editor.org/rfc/rfc4648#section-4.
+        The implementation is inspired by Brecht Devos' implementation here:
+        https://github.com/Brechtpd/base64/blob/main/base64.sol,
+        as well as by horsefacts' implementation here:
+        https://github.com/horsefacts/commit-reveal/blob/main/contracts/Base64.vy.
+"""
+
+
+# @dev Sets the maximum input and output length
+# allowed. For an n-byte input to be encoded, the
+# space required for the Base64-encoded content
+# (without line breaks) is "4 * ceil(n/3)" characters.
+_DATA_INPUT_BOUND: constant(uint256) = 1024
+_DATA_OUTPUT_BOUND: constant(uint256) = 1368
+
+
+# @dev Defines the Base64 encoding tables. For encoding
+# with a URL and filename-safe alphabet, please refer to:
+# https://www.rfc-editor.org/rfc/rfc4648#section-5.
+_TABLE_STD_CHARS: constant(String[65]) = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
+_TABLE_URL_CHARS: constant(String[65]) = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_="
+
+
+@external
+@pure
+def encode(data: Bytes[_DATA_INPUT_BOUND], base64_url: bool) -> DynArray[String[4], _DATA_OUTPUT_BOUND]:
+    """
+    @dev Encodes a `Bytes` array using the Base64
+         binary-to-text encoding scheme.
+    @notice Due to the Vyper design with fixed-size
+            string parameters, string concatenations
+            with itself in a loop can lead to length
+            mismatches (the underlying issue is that
+            Vyper does not support a mutable `Bytes`
+            type). To circumvent this issue, we choose
+            a dynamic array as the return type.
+    @param data The maximum 1024-byte data to be
+           Base64-encoded.
+    @param base64_url The Boolean variable that specifies
+           whether to use a URL and filename-safe alphabet
+           or not.
+    @return DynArray The maximum 4-character user-readable
+            string array that combined results in the Base64
+            encoding of `data`.
+    """
+    data_length: uint256 = len(data)
+    if (data_length == empty(uint256)):
+        return empty(DynArray[String[4], 1])
+
+    # If the length of the unencoded input is not
+    # a multiple of three, the encoded output must
+    # have padding added so that its length is a
+    # multiple of four.
+    padding: uint256 = data_length % 3
+    data_padded: Bytes[_DATA_INPUT_BOUND + 2] = b""
+    if (padding == 1):
+        data_padded = concat(data, b"\x00\x00")
+    elif (padding == 2):
+        data_padded = concat(data, b"\x00")
+    else:
+        data_padded = data
+
+    char_chunks: DynArray[String[4], _DATA_OUTPUT_BOUND] = []
+    idx: uint256 = 0
+    for _ in range(_DATA_INPUT_BOUND):
+        # For the Base64 encoding, three bytes (= chunk)
+        # of the bytestream (= 24 bits) are divided into
+        # four 6-bit blocks.
+        chunk: uint256 = convert(slice(data_padded, idx, 3), uint256)
+
+        # To write each character, we right shift the 3-byte
+        # chunk (= 24 bits) four times in blocks of six bits
+        # for each character (18, 12, 6, 0).
+        c1: uint256 = shift(chunk, -18) & 63
+        c2: uint256 = shift(chunk, -12) & 63
+        c3: uint256 = shift(chunk, -6) & 63
+        c4: uint256 = chunk & 63
+
+        # Base64 encoding with an URL and filename-safe
+        # alphabet.
+        if (base64_url):
+            char_chunks.append(concat(slice(_TABLE_URL_CHARS, c1, 1), slice(_TABLE_URL_CHARS, c2, 1), slice(_TABLE_URL_CHARS, c3, 1),\
+                                      slice(_TABLE_URL_CHARS, c4, 1)))
+        # Base64 encoding using the standard characters.
+        else:
+            char_chunks.append(concat(slice(_TABLE_STD_CHARS, c1, 1), slice(_TABLE_STD_CHARS, c2, 1), slice(_TABLE_STD_CHARS, c3, 1),\
+                                      slice(_TABLE_STD_CHARS, c4, 1)))
+
+        # The following line cannot overflow because we have
+        # limited the for loop by the `constant` parameter
+        # `_DATA_INPUT_BOUND`, which is bounded by the
+        # maximum value of `1024`.
+        idx = unsafe_add(idx, 3)
+
+        # We break the loop once we reach the end of `data`
+        # (including padding).
+        if (idx == len(data_padded)):
+            break
+
+    # Case 1: padding of "==" added.
+    if (padding == 1):
+        last_chunk: String[2] = slice(char_chunks.pop(), 0, 2)
+        char_chunks.append(concat(last_chunk, "=="))
+    # Case 2: padding of "=" added.
+    elif (padding == 2):
+        last_chunk: String[3] = slice(char_chunks.pop(), 0, 3)
+        char_chunks.append(concat(last_chunk, "="))
+
+    return char_chunks
+
+
+@external
+@pure
+def decode(data: String[_DATA_OUTPUT_BOUND], base64_url: bool) -> DynArray[Bytes[3], _DATA_INPUT_BOUND]:
+    """
+    @dev Decodes a `String` input using the Base64
+         binary-to-text encoding scheme.
+    @notice Due to the Vyper design with fixed-size
+            byte parameters, byte concatenations
+            with itself in a loop can lead to length
+            mismatches (the underlying issue is that
+            Vyper does not support a mutable `Bytes`
+            type). To circumvent this issue, we choose
+            a dynamic array as the return type. Note
+            that line breaks are not supported.
+    @param data The maximum 1368-byte data to be
+           Base64-decoded.
+    @param base64_url The Boolean variable that specifies
+           whether to use a URL and filename-safe alphabet
+           or not.
+    @return DynArray The maximum 3-byte array that combined
+            results in the Base64 decoding of `data`.
+    """
+    data_length: uint256 = len(data)
+    if (data_length == empty(uint256)):
+        return empty(DynArray[Bytes[3], _DATA_INPUT_BOUND])
+
+    # If the length of the encoded input is not a
+    # multiple of four, it is an invalid input.
+    assert data_length % 4 == 0, "Base64: length mismatch"
+
+    result: DynArray[Bytes[3], _DATA_INPUT_BOUND] = []
+    idx: uint256 = 0
+    for _ in range(_DATA_OUTPUT_BOUND):
+        # Each of these four characters represents
+        # a 6-bit index in the Base64 character list
+        # which, when concatenated, gives the 24-bit
+        # number for the original three characters.
+        chunk: String[4] = slice(data, idx, 4)
+
+        # Base64 encoding with an URL and filename-safe
+        # alphabet.
+        if (base64_url):
+            c1: uint256 = self._index_of(slice(chunk, 0, 1), True)
+            c2: uint256 = self._index_of(slice(chunk, 1, 1), True)
+            c3: uint256 = self._index_of(slice(chunk, 2, 1), True)
+            c4: uint256 = self._index_of(slice(chunk, 3, 1), True)
+
+            chunk_bytes: uint256 = shift(c1, 18) | shift(c2, 12) | shift(c3, 6) | c4
+
+            # We split the 24-bit number into the original
+            # three 8-bit characters.
+            b1: bytes1 = convert(convert(shift(chunk_bytes, -16) & 255, uint8), bytes1)
+            b2: bytes1 = convert(convert(shift(chunk_bytes, -8) & 255, uint8), bytes1)
+            b3: bytes1 = convert(convert(chunk_bytes & 255, uint8), bytes1)
+
+            # Case 1: padding of "=" as part of the
+            # encoded input.
+            if (c4 == 64):
+                result.append(concat(b1, b2, b"\x00"))
+            # Case 2: padding of "==" as part of the
+            # encoded input.
+            elif (c3 == 63):
+                result.append(concat(b1, b"\x00\x00"))
+            # Case 3: no padding as part of the encoded
+            # input.
+            else:
+                result.append(concat(b1, b2, b3))
+
+            # The following line cannot overflow because we have
+            # limited the for loop by the `constant` parameter
+            # `_DATA_OUTPUT_BOUND`, which is bounded by the
+            # maximum value of `1368`.
+            idx = unsafe_add(idx, 4)
+
+            # We break the loop once we reach the end of `data`.
+            if (idx == data_length):
+                break
+        # Base64 encoding using the standard characters.
+        else:
+            c1: uint256 = self._index_of(slice(chunk, 0, 1), False)
+            c2: uint256 = self._index_of(slice(chunk, 1, 1), False)
+            c3: uint256 = self._index_of(slice(chunk, 2, 1), False)
+            c4: uint256 = self._index_of(slice(chunk, 3, 1), False)
+
+            chunk_bytes: uint256 = shift(c1, 18) | shift(c2, 12) | shift(c3, 6) | c4
+
+            # We split the 24-bit number into the original
+            # three 8-bit characters.
+            b1: bytes1 = convert(convert(shift(chunk_bytes, -16) & 255, uint8), bytes1)
+            b2: bytes1 = convert(convert(shift(chunk_bytes, -8) & 255, uint8), bytes1)
+            b3: bytes1 = convert(convert(chunk_bytes & 255, uint8), bytes1)
+
+            # Case 1: padding of "=" as part of the
+            # encoded input.
+            if (c4 == 64):
+                result.append(concat(b1, b2, b"\x00"))
+            # Case 2: padding of "==" as part of the
+            # encoded input.
+            elif (c3 == 63):
+                result.append(concat(b1, b"\x00\x00"))
+            # Case 3: no padding as part of the encoded
+            # input.
+            else:
+                result.append(concat(b1, b2, b3))
+
+            # The following line cannot overflow because we have
+            # limited the for loop by the `constant` parameter
+            # `_DATA_OUTPUT_BOUND`, which is bounded by the
+            # maximum value of `1368`.
+            idx = unsafe_add(idx, 4)
+
+            # We break the loop once we reach the end of `data`.
+            if (idx == data_length):
+                break
+
+    return result
+
+
+@internal
+@pure
+def _index_of(char: String[1], base64_url: bool) -> uint256:
+    """
+    @dev Returns the index position of the string `char`
+         in the Base64 encoding table(s).
+    @param char The maximum 1-character user-readable string.
+    @param base64_url The Boolean variable that specifies
+           whether to use a URL and filename-safe alphabet
+           or not.
+    @return uint256 The 32-byte index position of the string
+            `char` in the Base64 encoding table.
+    """
+    pos: uint256 = 0
+    for _ in range(len(_TABLE_URL_CHARS)):
+        # Base64 encoding with an URL and filename-safe
+        # alphabet.
+        if (base64_url):
+            if (char == slice(_TABLE_URL_CHARS, pos, 1)):
+                break
+        # Base64 encoding using the standard characters.
+        else:
+            if (char == slice(_TABLE_STD_CHARS, pos, 1)):
+                break
+
+        # The following line cannot overflow because we have
+        # limited the for loop by the `constant` parameter
+        # `_TABLE_URL_CHARS`, which is bounded by the
+        # maximum value of `65`.
+        pos = unsafe_add(pos, 1)
+
+    # If no matching character is found, it is an
+    # invalid input.
+    assert pos < len(_TABLE_URL_CHARS), "Base64: invalid string"
+    
+    return pos
diff --git a/src/utils/ECDSA.vy b/src/utils/ECDSA.vy
index 251499e1..ec30665c 100644
--- a/src/utils/ECDSA.vy
+++ b/src/utils/ECDSA.vy
@@ -49,14 +49,15 @@ def recover_sig(hash: bytes32, signature: Bytes[65]) -> address:
     @param signature The secp256k1 64/65-byte signature of `hash`.
     @return address The recovered 20-byte signer address.
     """
+    sig_length: uint256 = len(signature)
     # 65-byte case: r,s,v standard signature.
-    if (len(signature) == 65):
+    if (sig_length == 65):
         r: uint256 = extract32(signature, 0, output_type=uint256)
         s: uint256 = extract32(signature, 32, output_type=uint256)
         v: uint256 = convert(slice(signature, 64, 1), uint256)
         return self._try_recover_vrs(hash, v, r, s)
     # 64-byte case: r,vs signature; see: https://eips.ethereum.org/EIPS/eip-2098.
-    elif (len(signature) == 64):
+    elif (sig_length == 64):
         r: uint256 = extract32(signature, 0, output_type=uint256)
         vs: uint256 = extract32(signature, 32, output_type=uint256)
         return self._try_recover_r_vs(hash, r, vs)
diff --git a/src/utils/MerkleProofVerification.vy b/src/utils/MerkleProofVerification.vy
index 661e4e8f..2a1f687f 100644
--- a/src/utils/MerkleProofVerification.vy
+++ b/src/utils/MerkleProofVerification.vy
@@ -138,15 +138,15 @@ def _process_multi_proof(proof: DynArray[bytes32, max_value(uint16)], proof_flag
     @return bytes32 The 32-byte recovered hash by using `leaves`
             and `proof` with a given set of `proof_flags`.
     """
-    leaves_len: uint256 = len(leaves)
+    leaves_length: uint256 = len(leaves)
     total_hashes: uint256 = len(proof_flags)
 
     # Checks the validity of the proof. We do not check for an
-    # overflow (nor underflow) as `leaves_len`, `proof`, and
+    # overflow (nor underflow) as `leaves_length`, `proof`, and
     # `total_hashes` are bounded by the value `max_value(uint16)`
     # and therefore cannot overflow the `uint256` type when they
     # are added together or incremented by 1.
-    assert unsafe_add(leaves_len, len(proof)) == unsafe_add(total_hashes, 1), "MerkleProof: invalid multiproof"
+    assert unsafe_add(leaves_length, len(proof)) == unsafe_add(total_hashes, 1), "MerkleProof: invalid multiproof"
 
     hashes: DynArray[bytes32, max_value(uint16)] = []
     leaf_pos: uint256 = empty(uint256)
@@ -161,14 +161,14 @@ def _process_multi_proof(proof: DynArray[bytes32, max_value(uint16)], proof_flag
     # - depending on the flag, either another value from the "main queue"
     #   (merging branches) or an element from the `proof` array.
     for flag in proof_flags:
-        if (leaf_pos < leaves_len):
+        if (leaf_pos < leaves_length):
             a = leaves[leaf_pos]
             leaf_pos = unsafe_add(leaf_pos, 1)
         else:
             a = hashes[hash_pos]
             hash_pos = unsafe_add(hash_pos, 1)
         if (flag):
-            if (leaf_pos < leaves_len):
+            if (leaf_pos < leaves_length):
                 b = leaves[leaf_pos]
                 leaf_pos = unsafe_add(leaf_pos, 1)
             else:
@@ -184,7 +184,7 @@ def _process_multi_proof(proof: DynArray[bytes32, max_value(uint16)], proof_flag
         # indexing and would revert in such a case. In any event,
         # the array index cannot become negative here by design.
         return hashes[unsafe_sub(total_hashes, 1)]
-    elif (leaves_len != empty(uint256)):
+    elif (leaves_length != empty(uint256)):
         return leaves[empty(uint256)]
     else:
         return proof[empty(uint256)]
diff --git a/src/utils/SignatureChecker.vy b/src/utils/SignatureChecker.vy
index 7e32983e..5e8bf39c 100644
--- a/src/utils/SignatureChecker.vy
+++ b/src/utils/SignatureChecker.vy
@@ -68,14 +68,15 @@ def _recover_sig(hash: bytes32, signature: Bytes[65]) -> address:
     @notice See {ECDSA-recover_sig} for the
             function docstring.
     """
+    sig_length: uint256 = len(signature)
     # 65-byte case: r,s,v standard signature.
-    if (len(signature) == 65):
+    if (sig_length == 65):
         r: uint256 = extract32(signature, 0, output_type=uint256)
         s: uint256 = extract32(signature, 32, output_type=uint256)
         v: uint256 = convert(slice(signature, 64, 1), uint256)
         return self._try_recover_vrs(hash, v, r, s)
     # 64-byte case: r,vs signature; see: https://eips.ethereum.org/EIPS/eip-2098.
-    elif (len(signature) == 64):
+    elif (sig_length == 64):
         r: uint256 = extract32(signature, 0, output_type=uint256)
         vs: uint256 = extract32(signature, 32, output_type=uint256)
         return self._try_recover_r_vs(hash, r, vs)


Test patch:
diff --git a/lib/prb-test b/lib/prb-test
new file mode 160000
index 00000000..b1f58b66
--- /dev/null
+++ b/lib/prb-test
@@ -0,0 +1,1 @@
+Subproject commit b1f58b66aeb6863c6a3de77d0d0744c01a0c5065
diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol
new file mode 100644
index 00000000..1c3711a6
--- /dev/null
+++ b/test/utils/Base64.t.sol
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.17;
+
+import {PRBTest} from "prb/test/PRBTest.sol";
+import {VyperDeployer} from "utils/VyperDeployer.sol";
+
+import {BytesLib} from "solidity-bytes-utils/BytesLib.sol";
+
+import {IBase64} from "./interfaces/IBase64.sol";
+
+/**
+ * @dev We make use of Paul's testing assertions (https://github.com/paulrberg/prb-test)
+ * in this test suite since it supports natively equality assertions for arrays.
+ */
+contract Base64Test is PRBTest {
+    using BytesLib for bytes;
+
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+
+    IBase64 private base64;
+
+    function setUp() public {
+        base64 = IBase64(vyperDeployer.deployContract("src/utils/", "Base64"));
+    }
+
+    function testEncodeEmptyString() public {
+        string[] memory emptyArray = new string[](0);
+        string[] memory outputStd = base64.encode("", false);
+        string[] memory outputUrl = base64.encode("", true);
+        assertEq(outputStd, emptyArray);
+        assertEq(outputUrl, emptyArray);
+    }
+
+    function testEncodeWithNoPadding() public {
+        string memory data = "test12";
+        string[] memory encoded = new string[](2);
+        encoded[0] = "dGVz";
+        encoded[1] = "dDEy";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encoded);
+        assertEq(outputUrl, encoded);
+    }
+
+    function testEncodeWithSinglePadding() public {
+        string memory data = "test1";
+        string[] memory encoded = new string[](2);
+        encoded[0] = "dGVz";
+        encoded[1] = "dDE=";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encoded);
+        assertEq(outputUrl, encoded);
+    }
+
+    function testEncodeWithDoublePadding() public {
+        string memory data = "test";
+        string[] memory encoded = new string[](2);
+        encoded[0] = "dGVz";
+        encoded[1] = "dA==";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encoded);
+        assertEq(outputUrl, encoded);
+    }
+
+    function testEncodeSingleCharacter() public {
+        string memory data = "M";
+        string[] memory encoded = new string[](1);
+        encoded[0] = "TQ==";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encoded);
+        assertEq(outputUrl, encoded);
+    }
+
+    function testEncodeSentence() public {
+        string memory data = "Snakes are great animals!";
+        string[] memory encoded = new string[](9);
+        encoded[0] = "U25h";
+        encoded[1] = "a2Vz";
+        encoded[2] = "IGFy";
+        encoded[3] = "ZSBn";
+        encoded[4] = "cmVh";
+        encoded[5] = "dCBh";
+        encoded[6] = "bmlt";
+        encoded[7] = "YWxz";
+        encoded[8] = "IQ==";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encoded);
+        assertEq(outputUrl, encoded);
+    }
+
+    function testEncodeSafeUrl() public {
+        string memory data = "[]c!~?[]~~";
+        string[] memory encodedStd = new string[](4);
+        string[] memory encodedUrl = new string[](4);
+        encodedStd[0] = "W11j";
+        encodedStd[1] = "IX4/";
+        encodedStd[2] = "W11+";
+        encodedStd[3] = "fg==";
+        encodedUrl[0] = "W11j";
+        encodedUrl[1] = "IX4_";
+        encodedUrl[2] = "W11-";
+        encodedUrl[3] = "fg==";
+        string[] memory outputStd = base64.encode(bytes(data), false);
+        string[] memory outputUrl = base64.encode(bytes(data), true);
+        assertEq(outputStd, encodedStd);
+        assertEq(outputUrl, encodedUrl);
+    }
+
+    function testDecodeEmptyString() public {
+        bytes[] memory outputStd = base64.decode("", false);
+        bytes[] memory outputUrl = base64.decode("", true);
+        assertEq(outputStd.length, 0);
+        assertEq(outputUrl.length, 0);
+    }
+
+    function testDecodeWithNoPadding() public {
+        string memory text = "test12";
+        string memory data = "dGVzdDEy";
+        bytes[] memory outputStd = base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        assertEq(string(bytes.concat(outputStd[0], outputStd[1])), text);
+        assertEq(string(bytes.concat(outputUrl[0], outputUrl[1])), text);
+    }
+
+    function testDecodeWithSinglePadding() public {
+        string memory text = "test1";
+        string memory data = "dGVzdDE=";
+        bytes[] memory outputStd = base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        bytes memory returnDataStd = bytes.concat(outputStd[0], outputStd[1]);
+        bytes memory returnDataUrl = bytes.concat(outputUrl[0], outputUrl[1]);
+        /**
+         * @dev We remove the one trailing zero byte that stems from
+         * the padding to ensure byte-level equality.
+         */
+        assertEq(
+            string(returnDataStd.slice(0, returnDataStd.length - 1)),
+            text
+        );
+        assertEq(
+            string(returnDataUrl.slice(0, returnDataUrl.length - 1)),
+            text
+        );
+    }
+
+    function testDecodeWithDoublePadding() public {
+        string memory text = "test";
+        string memory data = "dGVzdA==";
+        bytes[] memory outputStd = base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        bytes memory returnDataStd = bytes.concat(outputStd[0], outputStd[1]);
+        bytes memory returnDataUrl = bytes.concat(outputUrl[0], outputUrl[1]);
+        /**
+         * @dev We remove the two trailing zero bytes that stem from
+         * the padding to ensure byte-level equality.
+         */
+        assertEq(
+            string(returnDataStd.slice(0, returnDataStd.length - 2)),
+            text
+        );
+        assertEq(
+            string(returnDataUrl.slice(0, returnDataUrl.length - 2)),
+            text
+        );
+    }
+
+    function testDecodeSingleCharacter() public {
+        string memory text = "M";
+        string memory data = "TQ==";
+        bytes[] memory outputStd = base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        /**
+         * @dev We remove the two trailing zero bytes that stem from
+         * the padding to ensure byte-level equality.
+         */
+        assertEq(string(outputStd[0].slice(0, outputStd[0].length - 2)), text);
+        assertEq(string(outputUrl[0].slice(0, outputUrl[0].length - 2)), text);
+    }
+
+    function testDecodeSentence() public {
+        string memory text = "Snakes are great animals!";
+        string memory data = "U25ha2VzIGFyZSBncmVhdCBhbmltYWxzIQ==";
+        bytes[] memory outputStd = base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        bytes memory returnDataStd = bytes.concat(
+            outputStd[0],
+            outputStd[1],
+            outputStd[2],
+            outputStd[3],
+            outputStd[4],
+            outputStd[5],
+            outputStd[6],
+            outputStd[7],
+            outputStd[8]
+        );
+        bytes memory returnDataUrl = bytes.concat(
+            outputUrl[0],
+            outputUrl[1],
+            outputUrl[2],
+            outputUrl[3],
+            outputUrl[4],
+            outputUrl[5],
+            outputUrl[6],
+            outputUrl[7],
+            outputUrl[8]
+        );
+        /**
+         * @dev We remove the two trailing zero bytes that stem from
+         * the padding to ensure byte-level equality.
+         */
+        assertEq(
+            string(returnDataStd.slice(0, returnDataStd.length - 2)),
+            text
+        );
+        assertEq(
+            string(returnDataUrl.slice(0, returnDataUrl.length - 2)),
+            text
+        );
+    }
+
+    function testDecodeSafeUrl() public {
+        string memory text = "[]c!~?[]~";
+        string memory data = "W11jIX4_W11-";
+        vm.expectRevert(bytes("Base64: invalid string"));
+        base64.decode(data, false);
+        bytes[] memory outputUrl = base64.decode(data, true);
+        bytes memory returnDataUrl = bytes.concat(
+            outputUrl[0],
+            outputUrl[1],
+            outputUrl[2]
+        );
+        assertEq(string(returnDataUrl), text);
+    }
+
+    function testDataLengthMismatch() public {
+        string memory data = "W11jI";
+        vm.expectRevert(bytes("Base64: length mismatch"));
+        base64.decode(data, false);
+        vm.expectRevert(bytes("Base64: length mismatch"));
+        base64.decode(data, true);
+    }
+}
diff --git a/test/utils/interfaces/IBase64.sol b/test/utils/interfaces/IBase64.sol
new file mode 100644
index 00000000..57b6a3f7
--- /dev/null
+++ b/test/utils/interfaces/IBase64.sol
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.17;
+
+interface IBase64 {
+    function encode(
+        bytes memory data,
+        bool base64Url
+    ) external pure returns (string[] memory);
+
+    function decode(
+        string memory data,
+        bool base64Url
+    ) external pure returns (bytes[] memory);
+}