Repository: pcaversaccio/snekmate (language: python)

Issue:
💥 ERC-4626 Tokenised Vault Implementation
[ERC-4626](https://eips.ethereum.org/EIPS/eip-4626) tokenised vault implementation.
🔁 Upgrade to Solidity Version `0.8.19`
https://blog.soliditylang.org/2023/02/22/solidity-0.8.19-release-announcement/


Discussion / hints:
~Rename `mixins` to `extensions`.~ DONE


Fix patch:
diff --git a/.eslintignore b/.eslintignore
index 2439e745..7e97ca09 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,6 +3,7 @@ lib/murky
 lib/prb-test
 lib/forge-std
 lib/create-util
+lib/erc4626-tests
 lib/solidity-bytes-utils
 lib/openzeppelin-contracts
 bin
diff --git a/.gas-snapshot b/.gas-snapshot
index 908eab3b..978f4106 100644
--- a/.gas-snapshot
+++ b/.gas-snapshot
@@ -59,8 +59,8 @@ BatchDistributorTest:testDistributeTokenMultipleAddressesSuccess() (gas: 764652)
 BatchDistributorTest:testDistributeTokenOneAddressSuccess() (gas: 727265)
 BatchDistributorTest:testDistributeTokenRevertWithInsufficientAllowance() (gas: 721109)
 BatchDistributorTest:testDistributeTokenRevertWithInsufficientBalance() (gas: 723840)
-BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 983113, ~: 924567)
-BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1465307, ~: 1430877)
+BatchDistributorTest:testFuzzDistributeEtherMultipleAddressesSuccess(((address,uint256)[]),uint256) (runs: 256, μ: 988402, ~: 925503)
+BatchDistributorTest:testFuzzDistributeTokenMultipleAddressesSuccess(((address,uint256)[]),address,uint256) (runs: 256, μ: 1465572, ~: 1430877)
 Create2AddressTest:testComputeAddress() (gas: 702836)
 Create2AddressTest:testComputeAddressSelf() (gas: 711578)
 Create2AddressTest:testFuzzComputeAddress(bytes32,address) (runs: 256, μ: 703369, ~: 703369)
@@ -86,32 +86,32 @@ CreateAddressTest:testComputeAddressSelfNonceUint56() (gas: 688873)
 CreateAddressTest:testComputeAddressSelfNonceUint64() (gas: 688969)
 CreateAddressTest:testComputeAddressSelfNonceUint8() (gas: 688618)
 CreateAddressTest:testComputeAddressSelfRevertTooHighNonce() (gas: 9122)
-CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 687670, ~: 687831)
+CreateAddressTest:testFuzzComputeAddressNonce0x7f(uint64,address) (runs: 256, μ: 687669, ~: 687831)
 CreateAddressTest:testFuzzComputeAddressNonceUint16(uint64,address) (runs: 256, μ: 687167, ~: 687294)
 CreateAddressTest:testFuzzComputeAddressNonceUint24(uint64,address) (runs: 256, μ: 687297, ~: 687398)
-CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 687294, ~: 687427)
-CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 687401, ~: 687497)
+CreateAddressTest:testFuzzComputeAddressNonceUint32(uint64,address) (runs: 256, μ: 687293, ~: 687427)
+CreateAddressTest:testFuzzComputeAddressNonceUint40(uint64,address) (runs: 256, μ: 687399, ~: 687497)
 CreateAddressTest:testFuzzComputeAddressNonceUint48(uint64,address) (runs: 256, μ: 687399, ~: 687503)
 CreateAddressTest:testFuzzComputeAddressNonceUint56(uint64,address) (runs: 256, μ: 687403, ~: 687510)
 CreateAddressTest:testFuzzComputeAddressNonceUint64(uint64,address) (runs: 256, μ: 687431, ~: 687614)
 CreateAddressTest:testFuzzComputeAddressNonceUint8(uint64,address) (runs: 256, μ: 687169, ~: 687272)
 CreateAddressTest:testFuzzComputeAddressRevertTooHighNonce(uint256,address) (runs: 256, μ: 13321, ~: 13275)
 CreateAddressTest:testFuzzComputeAddressSelfNonce0x7f(uint64) (runs: 256, μ: 693917, ~: 694033)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 693197, ~: 693106)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint16(uint64) (runs: 256, μ: 693196, ~: 693106)
 CreateAddressTest:testFuzzComputeAddressSelfNonceUint24(uint64) (runs: 256, μ: 693545, ~: 693631)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 693548, ~: 693639)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 693624, ~: 693710)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint32(uint64) (runs: 256, μ: 693545, ~: 693639)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint40(uint64) (runs: 256, μ: 693627, ~: 693710)
 CreateAddressTest:testFuzzComputeAddressSelfNonceUint48(uint64) (runs: 256, μ: 693587, ~: 693697)
 CreateAddressTest:testFuzzComputeAddressSelfNonceUint56(uint64) (runs: 256, μ: 693679, ~: 693767)
 CreateAddressTest:testFuzzComputeAddressSelfNonceUint64(uint64) (runs: 256, μ: 693746, ~: 693897)
-CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 693257, ~: 693334)
+CreateAddressTest:testFuzzComputeAddressSelfNonceUint8(uint64) (runs: 256, μ: 693258, ~: 693334)
 CreateAddressTest:testFuzzComputeAddressSelfRevertTooHighNonce(uint256) (runs: 256, μ: 13522, ~: 13480)
 ECDSATest:testEthSignedMessageHash() (gas: 5859)
 ECDSATest:testFuzzEthSignedMessageHash(string) (runs: 256, μ: 6452, ~: 6455)
 ECDSATest:testFuzzRecoverWithInvalidSignature(bytes,string) (runs: 256, μ: 15181, ~: 15182)
 ECDSATest:testFuzzRecoverWithTooLongSignature(bytes,string) (runs: 256, μ: 13144, ~: 13142)
-ECDSATest:testFuzzRecoverWithValidSignature(string,string) (runs: 256, μ: 21790, ~: 21796)
-ECDSATest:testFuzzRecoverWithWrongMessage(string,string,bytes32) (runs: 256, μ: 21778, ~: 21789)
+ECDSATest:testFuzzRecoverWithValidSignature(string,string) (runs: 256, μ: 21787, ~: 21796)
+ECDSATest:testFuzzRecoverWithWrongMessage(string,string,bytes32) (runs: 256, μ: 21775, ~: 21789)
 ECDSATest:testFuzzToDataWithIntendedValidatorHash(address,bytes) (runs: 256, μ: 7069, ~: 7057)
 ECDSATest:testFuzzToDataWithIntendedValidatorHashSelf(bytes) (runs: 256, μ: 10062, ~: 10045)
 ECDSATest:testFuzzToTypedDataHash(string,string) (runs: 256, μ: 7105, ~: 7108)
@@ -130,7 +130,7 @@ ECDSATest:testToDataWithIntendedValidatorHashSelf() (gas: 10220)
 ECDSATest:testToTypedDataHash() (gas: 5973)
 EIP712DomainSeparatorTest:testCachedDomainSeparatorV4() (gas: 7554)
 EIP712DomainSeparatorTest:testDomainSeparatorV4() (gas: 9332)
-EIP712DomainSeparatorTest:testFuzzDomainSeparatorV4(uint8) (runs: 256, μ: 9445, ~: 9473)
+EIP712DomainSeparatorTest:testFuzzDomainSeparatorV4(uint8) (runs: 256, μ: 9448, ~: 9473)
 EIP712DomainSeparatorTest:testFuzzHashTypedDataV4(address,address,uint256,uint256,uint64) (runs: 256, μ: 7971, ~: 7971)
 EIP712DomainSeparatorTest:testHashTypedDataV4() (gas: 13313)
 ERC1155Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3479)
@@ -160,18 +160,18 @@ ERC1155Test:testExistsAfterBatchMint() (gas: 143972)
 ERC1155Test:testExistsAfterSingleBurn() (gas: 129963)
 ERC1155Test:testExistsAfterSingleMint() (gas: 114087)
 ERC1155Test:testExistsBeforeMint() (gas: 7770)
-ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 133467, ~: 133501)
+ERC1155Test:testFuzzBurnBatchSuccess(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 133468, ~: 133501)
 ERC1155Test:testFuzzBurnSuccess(address,address,uint256) (runs: 256, μ: 132317, ~: 132327)
 ERC1155Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14270, ~: 14270)
 ERC1155Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49312, ~: 49296)
 ERC1155Test:testFuzzSafeBatchTransferFromByApprovedOperator(address,address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 225900, ~: 225862)
 ERC1155Test:testFuzzSafeBatchTransferFromEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 190892, ~: 190867)
 ERC1155Test:testFuzzSafeBatchTransferFromNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 571897, ~: 571907)
-ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 572378, ~: 572005)
+ERC1155Test:testFuzzSafeBatchTransferFromWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 572399, ~: 572005)
 ERC1155Test:testFuzzSafeMintBatchEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 153428, ~: 153421)
 ERC1155Test:testFuzzSafeMintBatchNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 528705, ~: 528715)
 ERC1155Test:testFuzzSafeMintBatchNonMinter(address) (runs: 256, μ: 41249, ~: 41249)
-ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 529114, ~: 528919)
+ERC1155Test:testFuzzSafeMintBatchWithData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 529125, ~: 528919)
 ERC1155Test:testFuzzSafeMintEOAReceiver(address,address,uint256,uint256,uint256,uint256,bytes) (runs: 256, μ: 146628, ~: 146603)
 ERC1155Test:testFuzzSafeMintNoData(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 522911, ~: 522921)
 ERC1155Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 33621, ~: 33621)
@@ -183,12 +183,12 @@ ERC1155Test:testFuzzSafeTransferFromWithData(address,uint256,uint256,uint256,uin
 ERC1155Test:testFuzzSetApprovalForAllRevoke(address,address) (runs: 256, μ: 32083, ~: 32052)
 ERC1155Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 45488, ~: 45488)
 ERC1155Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16147, ~: 16161)
-ERC1155Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34558, ~: 34552)
+ERC1155Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34559, ~: 34552)
 ERC1155Test:testFuzzSetUriNonMinter(address) (runs: 256, μ: 14555, ~: 14555)
-ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 127513, ~: 127547)
-ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 142054, ~: 142006)
+ERC1155Test:testFuzzTotalSupplyAfterBatchBurn(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 127514, ~: 127547)
+ERC1155Test:testFuzzTotalSupplyAfterBatchMint(address,uint256,uint256,uint256,uint256) (runs: 256, μ: 142056, ~: 142006)
 ERC1155Test:testFuzzTotalSupplyAfterSingleBurn(address,uint256,bytes) (runs: 256, μ: 130287, ~: 130252)
-ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 85661, ~: 114738)
+ERC1155Test:testFuzzTotalSupplyAfterSingleMint(uint256,uint256,bytes) (runs: 256, μ: 86361, ~: 114770)
 ERC1155Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14366, ~: 14366)
 ERC1155Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 76430, ~: 76400)
 ERC1155Test:testHasOwner() (gas: 10214)
@@ -262,8 +262,8 @@ ERC1155Test:testUriBaseAndTokenUriNotSet() (gas: 2993638)
 ERC1155Test:testUriBaseAndTokenUriSet() (gas: 66159)
 ERC1155Test:testUriNoBaseURI() (gas: 3044520)
 ERC1155Test:testUriNoTokenUri() (gas: 16929)
-ERC20Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3136)
-ERC20Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3136)
+ERC20Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3135)
+ERC20Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3135)
 ERC20Test:testApproveExceedingBalanceCase1() (gas: 40595)
 ERC20Test:testApproveExceedingBalanceCase2() (gas: 47293)
 ERC20Test:testApproveFromZeroAddress() (gas: 13278)
@@ -301,14 +301,14 @@ ERC20Test:testFuzzDecreaseAllowanceInvalidAmount(address,uint256,uint256) (runs:
 ERC20Test:testFuzzDecreaseAllowanceSuccess(address,uint256,uint256) (runs: 256, μ: 47507, ~: 48025)
 ERC20Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11916, ~: 11943)
 ERC20Test:testFuzzIncreaseAllowanceSuccess(address,uint256) (runs: 256, μ: 44377, ~: 44976)
-ERC20Test:testFuzzMintNonMinter(string,uint256) (runs: 256, μ: 13433, ~: 13448)
-ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 56116, ~: 56981)
-ERC20Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46374, ~: 46407)
-ERC20Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 72188, ~: 72220)
+ERC20Test:testFuzzMintNonMinter(string,uint256) (runs: 256, μ: 13431, ~: 13448)
+ERC20Test:testFuzzMintSuccess(string,uint256) (runs: 256, μ: 56114, ~: 56981)
+ERC20Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46367, ~: 46398)
+ERC20Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 72181, ~: 72220)
 ERC20Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14203, ~: 14203)
-ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49166, ~: 49150)
-ERC20Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16081, ~: 16095)
-ERC20Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34431, ~: 34424)
+ERC20Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49111, ~: 49150)
+ERC20Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16083, ~: 16095)
+ERC20Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34433, ~: 34424)
 ERC20Test:testFuzzTransferFromInsufficientAllowance(address,address,uint256,uint8) (runs: 256, μ: 178255, ~: 178641)
 ERC20Test:testFuzzTransferFromSuccess(address,address,uint256) (runs: 256, μ: 207081, ~: 207955)
 ERC20Test:testFuzzTransferInvalidAmount(address,address,uint256) (runs: 256, μ: 16898, ~: 16898)
@@ -355,6 +355,64 @@ ERC20Test:testTransferOwnershipToZeroAddress() (gas: 15837)
 ERC20Test:testTransferSuccess() (gas: 43220)
 ERC20Test:testTransferToZeroAddress() (gas: 16812)
 ERC20Test:testTransferZeroTokens() (gas: 25327)
+ERC4626VaultInvariants:invariantTotalAssets() (runs: 256, calls: 3840, reverts: 2978)
+ERC4626VaultInvariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 2978)
+ERC4626VaultTest:testCachedDomainSeparator() (gas: 7836)
+ERC4626VaultTest:testDepositInsufficientAllowance() (gas: 83276)
+ERC4626VaultTest:testDepositWithNoApproval() (gas: 23849)
+ERC4626VaultTest:testDepositZero() (gas: 42955)
+ERC4626VaultTest:testDomainSeparator() (gas: 11772)
+ERC4626VaultTest:testEmptyVaultDeposit() (gas: 573759)
+ERC4626VaultTest:testEmptyVaultMint() (gas: 575658)
+ERC4626VaultTest:testEmptyVaultRedeem() (gas: 213227)
+ERC4626VaultTest:testEmptyVaultwithdraw() (gas: 227501)
+ERC4626VaultTest:testFail_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 569774, ~: 572506)
+ERC4626VaultTest:testFail_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 571772, ~: 574644)
+ERC4626VaultTest:testFuzzDomainSeparator(uint8) (runs: 256, μ: 11784, ~: 11811)
+ERC4626VaultTest:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 46281, ~: 46309)
+ERC4626VaultTest:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 72494, ~: 72529)
+ERC4626VaultTest:testInitialSetup() (gas: 5841081)
+ERC4626VaultTest:testMintWithNoApproval() (gas: 24097)
+ERC4626VaultTest:testMintZero() (gas: 43223)
+ERC4626VaultTest:testMultipleMintDepositRedeemWithdraw() (gas: 414232)
+ERC4626VaultTest:testPermitBadChainId() (gas: 44638)
+ERC4626VaultTest:testPermitBadNonce() (gas: 41213)
+ERC4626VaultTest:testPermitExpiredDeadline() (gas: 18296)
+ERC4626VaultTest:testPermitOtherSignature() (gas: 42793)
+ERC4626VaultTest:testPermitReplaySignature() (gas: 74757)
+ERC4626VaultTest:testPermitSuccess() (gas: 71427)
+ERC4626VaultTest:testRedeemInsufficientShares() (gas: 135371)
+ERC4626VaultTest:testRedeemWithNoShares() (gas: 14547)
+ERC4626VaultTest:testSingleDepositWithdraw() (gas: 182973)
+ERC4626VaultTest:testSingleMintRedeem() (gas: 181810)
+ERC4626VaultTest:testVaultInteractionsForSomeoneElse() (gas: 231861)
+ERC4626VaultTest:testWithdrawInsufficientAllowance() (gas: 127014)
+ERC4626VaultTest:testWithdrawInsufficientAssets() (gas: 121468)
+ERC4626VaultTest:testWithdrawWithNoAssets() (gas: 22798)
+ERC4626VaultTest:test_RT_deposit_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 485497, ~: 487700)
+ERC4626VaultTest:test_RT_deposit_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 487331, ~: 489093)
+ERC4626VaultTest:test_RT_mint_redeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486124, ~: 487922)
+ERC4626VaultTest:test_RT_mint_withdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 487859, ~: 489280)
+ERC4626VaultTest:test_RT_redeem_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 485744, ~: 487732)
+ERC4626VaultTest:test_RT_redeem_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 486023, ~: 487953)
+ERC4626VaultTest:test_RT_withdraw_deposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 487515, ~: 489106)
+ERC4626VaultTest:test_RT_withdraw_mint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 488006, ~: 489364)
+ERC4626VaultTest:test_asset((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 434482, ~: 437296)
+ERC4626VaultTest:test_convertToAssets((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 440830, ~: 441922)
+ERC4626VaultTest:test_convertToShares((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 440680, ~: 441904)
+ERC4626VaultTest:test_deposit((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 482732, ~: 485122)
+ERC4626VaultTest:test_maxDeposit((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 434182, ~: 436996)
+ERC4626VaultTest:test_maxMint((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 434262, ~: 437076)
+ERC4626VaultTest:test_maxRedeem((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 434472, ~: 437286)
+ERC4626VaultTest:test_maxWithdraw((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 435967, ~: 438751)
+ERC4626VaultTest:test_mint((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 483316, ~: 485273)
+ERC4626VaultTest:test_previewDeposit((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 476916, ~: 478987)
+ERC4626VaultTest:test_previewMint((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 477565, ~: 479231)
+ERC4626VaultTest:test_previewRedeem((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 479458, ~: 481197)
+ERC4626VaultTest:test_previewWithdraw((address[4],uint256[4],uint256[4],int256),uint256) (runs: 256, μ: 480924, ~: 482607)
+ERC4626VaultTest:test_redeem((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 486286, ~: 487570)
+ERC4626VaultTest:test_totalAssets((address[4],uint256[4],uint256[4],int256)) (runs: 256, μ: 434641, ~: 437456)
+ERC4626VaultTest:test_withdraw((address[4],uint256[4],uint256[4],int256),uint256,uint256) (runs: 256, μ: 487619, ~: 488929)
 ERC721Invariants:invariantOwner() (runs: 256, calls: 3840, reverts: 3449)
 ERC721Invariants:invariantTotalSupply() (runs: 256, calls: 3840, reverts: 3449)
 ERC721Test:testApproveClearingApprovalWithNoPriorApproval() (gas: 179010)
@@ -382,20 +440,20 @@ ERC721Test:testFuzzApproveFromOperatorAddress(address,address,address) (runs: 25
 ERC721Test:testFuzzApproveWithNoPriorApproval(address,address) (runs: 256, μ: 198495, ~: 198505)
 ERC721Test:testFuzzApproveWithPriorApproval(address,address) (runs: 256, μ: 206528, ~: 206538)
 ERC721Test:testFuzzBurnSuccess(address) (runs: 256, μ: 308415, ~: 308407)
-ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 10076, ~: 10104)
+ERC721Test:testFuzzDomainSeparator(uint8) (runs: 256, μ: 10074, ~: 10104)
 ERC721Test:testFuzzGetApprovedApprovedTokenId(address,address) (runs: 256, μ: 196055, ~: 196065)
-ERC721Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 204301, ~: 204332)
-ERC721Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 230826, ~: 230864)
+ERC721Test:testFuzzPermitInvalid(string,string,uint16) (runs: 256, μ: 204295, ~: 204332)
+ERC721Test:testFuzzPermitSuccess(string,string,uint16) (runs: 256, μ: 230820, ~: 230864)
 ERC721Test:testFuzzRenounceOwnershipNonOwner(address) (runs: 256, μ: 14408, ~: 14408)
 ERC721Test:testFuzzRenounceOwnershipSuccess(address) (runs: 256, μ: 49498, ~: 49482)
 ERC721Test:testFuzzSafeMintNonMinter(address) (runs: 256, μ: 16197, ~: 16197)
-ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 21484809, ~: 21429329)
+ERC721Test:testFuzzSafeMintSuccess(address[]) (runs: 256, μ: 21484879, ~: 21429329)
 ERC721Test:testFuzzSafeTransferFromWithData(address,address,address,bytes) (runs: 256, μ: 1458209, ~: 1458913)
 ERC721Test:testFuzzSetApprovalForAllSuccess(address,address) (runs: 256, μ: 191219, ~: 191219)
-ERC721Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16192, ~: 16210)
+ERC721Test:testFuzzSetMinterNonOwner(address,string) (runs: 256, μ: 16195, ~: 16210)
 ERC721Test:testFuzzSetMinterSuccess(string) (runs: 256, μ: 34775, ~: 34772)
-ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 24213046, ~: 24635126)
-ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 23979788, ~: 24395829)
+ERC721Test:testFuzzTokenByIndex(address,string[]) (runs: 256, μ: 24213486, ~: 24701621)
+ERC721Test:testFuzzTotalSupply(address,string[]) (runs: 256, μ: 23980228, ~: 24462324)
 ERC721Test:testFuzzTransferFrom(address,address,address) (runs: 256, μ: 571678, ~: 571662)
 ERC721Test:testFuzzTransferOwnershipNonOwner(address,address) (runs: 256, μ: 14485, ~: 14485)
 ERC721Test:testFuzzTransferOwnershipSuccess(address,address) (runs: 256, μ: 77186, ~: 77167)
@@ -457,8 +515,16 @@ ERC721Test:testTransferFrom() (gas: 582560)
 ERC721Test:testTransferOwnershipNonOwner() (gas: 13015)
 ERC721Test:testTransferOwnershipSuccess() (gas: 55029)
 ERC721Test:testTransferOwnershipToZeroAddress() (gas: 16064)
-MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649977830, ~: 1649974618)
-MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135976485, ~: 135973244)
+MathTest:testFuzzMulDiv(uint256,uint256,uint256) (runs: 256, μ: 12841, ~: 12595)
+MathTest:testFuzzMulDivDomain(uint256,uint256,uint256) (runs: 256, μ: 10347, ~: 10430)
+MathTest:testMulDivDivisionByZero() (gas: 10214)
+MathTest:testMulDivOverflow() (gas: 10658)
+MathTest:testMulDivRoundDownLargeValues() (gas: 12568)
+MathTest:testMulDivRoundDownSmallValues() (gas: 7571)
+MathTest:testMulDivRoundUpLargeValues() (gas: 12719)
+MathTest:testMulDivRoundUpSmallValues() (gas: 7758)
+MerkleProofVerificationTest:testFuzzMultiProofVerifySingleLeaf(bytes32[],uint256) (runs: 256, μ: 1649977824, ~: 1649974667)
+MerkleProofVerificationTest:testFuzzVerify(bytes32[],uint256) (runs: 256, μ: 135976479, ~: 135973293)
 MerkleProofVerificationTest:testFuzzVerifyMultiProofMultipleLeaves(bool,bool,bool) (runs: 256, μ: 412472500, ~: 412472432)
 MerkleProofVerificationTest:testInvalidMerkleMultiProof() (gas: 412478563)
 MerkleProofVerificationTest:testInvalidMerkleProof() (gas: 33970453)
@@ -522,8 +588,8 @@ SignatureCheckerTest:testEOAWithInvalidSignature2() (gas: 20267)
 SignatureCheckerTest:testEOAWithInvalidSigner() (gas: 20496)
 SignatureCheckerTest:testEOAWithTooHighSValue() (gas: 17256)
 SignatureCheckerTest:testEOAWithValidSignature() (gas: 19591)
-SignatureCheckerTest:testFuzzEIP1271WithInvalidSigner(string,string) (runs: 256, μ: 35895, ~: 35902)
+SignatureCheckerTest:testFuzzEIP1271WithInvalidSigner(string,string) (runs: 256, μ: 35896, ~: 35902)
 SignatureCheckerTest:testFuzzEIP1271WithValidSignature(string) (runs: 256, μ: 34429, ~: 34431)
 SignatureCheckerTest:testFuzzEOAWithInvalidSignature(bytes,string) (runs: 256, μ: 16901, ~: 16896)
-SignatureCheckerTest:testFuzzEOAWithInvalidSigner(string,string) (runs: 256, μ: 21576, ~: 21583)
-SignatureCheckerTest:testFuzzEOAWithValidSignature(string,string) (runs: 256, μ: 20687, ~: 20694)
\ No newline at end of file
+SignatureCheckerTest:testFuzzEOAWithInvalidSigner(string,string) (runs: 256, μ: 21577, ~: 21583)
+SignatureCheckerTest:testFuzzEOAWithValidSignature(string,string) (runs: 256, μ: 20688, ~: 20694)
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
index 2cd296e6..687815b4 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -16,3 +16,6 @@
 [submodule "lib/murky"]
 	path = lib/murky
 	url = https://github.com/dmfxyz/murky.git
+[submodule "lib/erc4626-tests"]
+	path = lib/erc4626-tests
+	url = https://github.com/a16z/erc4626-tests.git
diff --git a/.prettierignore b/.prettierignore
index 2439e745..7e97ca09 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,6 +3,7 @@ lib/murky
 lib/prb-test
 lib/forge-std
 lib/create-util
+lib/erc4626-tests
 lib/solidity-bytes-utils
 lib/openzeppelin-contracts
 bin
diff --git a/.solhintignore b/.solhintignore
index 34eb1f1a..7e97ca09 100644
--- a/.solhintignore
+++ b/.solhintignore
@@ -3,5 +3,9 @@ lib/murky
 lib/prb-test
 lib/forge-std
 lib/create-util
+lib/erc4626-tests
 lib/solidity-bytes-utils
 lib/openzeppelin-contracts
+bin
+cache
+out
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 937738a5..9f511f39 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,8 @@
   - `Ownable`: Owner-based access control functions.
   - `Ownable2Step`: 2-step ownership transfer functions.
   - `AccessControl`: Multi-role-based access control functions.
+- **Extensions**
+  - `ERC4626`: Modern and gas-efficient ERC-4626 tokenised vault implementation. ([#74](https://github.com/pcaversaccio/snekmate/pull/74))
 - **Tokens**
   - `ERC20`: Modern and gas-efficient ERC-20 + EIP-2612 implementation. ([#17](https://github.com/pcaversaccio/snekmate/pull/17))
   - `ERC721`: Modern and gas-efficient ERC-721 + EIP-4494 implementation. ([#20](https://github.com/pcaversaccio/snekmate/pull/20))
@@ -20,5 +22,6 @@
   - `ECDSA`: Elliptic curve digital signature algorithm (ECDSA) functions.
   - `SignatureChecker`: ECDSA and EIP-1271 signature verification functions.
   - `EIP712DomainSeparator`: EIP-712 domain separator.
+  - `Math`: Standard mathematical utility functions. ([#74](https://github.com/pcaversaccio/snekmate/pull/74))
   - `MerkleProofVerification`: Merkle tree proof verification functions. ([#30](https://github.com/pcaversaccio/snekmate/pull/30))
   - `Multicall`: Multicall functions.
diff --git a/README.md b/README.md
index dab03898..c25889ad 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ src
 │  ├─ Ownable2Step — "2-Step Ownership Transfer Functions"
 │  ├─ AccessControl — "Multi-Role-Based Access Control Functions"
 ├─ extensions
-│  ├─ ERC4626 — "ERC-4626 Tokenised Vault Implementation (TBD)"
+│  ├─ ERC4626 — "Modern and Gas-Efficient ERC-4626 Tokenised Vault Implementation"
 ├─ tokens
 │  ├─ ERC20 — "Modern and Gas-Efficient ERC-20 + EIP-2612 Implementation"
 │  ├─ ERC721 — "Modern and Gas-Efficient ERC-721 + EIP-4494 Implementation"
@@ -29,6 +29,7 @@ src
    ├─ ECDSA — "Elliptic Curve Digital Signature Algorithm (ECDSA) Functions"
    ├─ SignatureChecker — "ECDSA and EIP-1271 Signature Verification Functions"
    ├─ EIP712DomainSeparator — "EIP-712 Domain Separator"
+   ├─ Math — "Standard Mathematical Utility Functions"
    ├─ MerkleProofVerification — "Merkle Tree Proof Verification Functions"
    ├─ Multicall — "Multicall Functions"
 ```
@@ -44,6 +45,7 @@ This repository contains [Foundry](https://github.com/foundry-rs/foundry)-based
 | `Ownable`                 |       ✅       |            ✅            |         ✅          |
 | `Ownable2Step`            |       ✅       |            ✅            |         ✅          |
 | `AccessControl`           |       ✅       |            ✅            |         ✅          |
+| `ERC4626`                 |       ✅       |            ✅            |         ✅          |
 | `ERC20`                   |       ✅       |            ✅            |         ✅          |
 | `ERC721`                  |       ✅       |            ✅            |         ✅          |
 | `ERC1155`                 |       ✅       |            ✅            |         ✅          |
@@ -54,6 +56,7 @@ This repository contains [Foundry](https://github.com/foundry-rs/foundry)-based
 | `ECDSA`                   |       ✅       |            ✅            |         ❌          |
 | `SignatureChecker`        |       ✅       |            ✅            |         ❌          |
 | `EIP712DomainSeparator`   |       ✅       |            ✅            |         ❌          |
+| `Math`                    |       ✅       |            ✅            |         ❌          |
 | `MerkleProofVerification` |       ✅       |            ✅            |         ❌          |
 | `Multicall`               |       ✅       |            ❌            |         ❌          |
 
diff --git a/foundry.toml b/foundry.toml
index 110b2a32..0bd855dc 100644
--- a/foundry.toml
+++ b/foundry.toml
@@ -7,7 +7,7 @@ libs = ['lib']                                              # a list of library
 remappings = []                                             # a list of remappings
 cache = true                                                # whether to cache builds or not
 cache_path = 'cache'                                        # where the cache is stored if enabled
-force = true                                                # whether to ignore the cache (clean build)
+force = false                                               # whether to ignore the cache (clean build)
 fuzz = { runs = 256 }                                       # the number of fuzz runs for tests
 invariant = { runs = 256 }                                  # the number of runs that must execute for each invariant test group
 ffi = true                                                  # whether to enable foreign function interface (ffi) cheatcodes or not
@@ -16,6 +16,7 @@ fs_permissions = [{ access = "read-write", path = "./"}]    # set read-write acc
 
 ## default overrides for the CI runs
 [profile.ci]
-fuzz = { runs = 10_000 }                                    # increase the number of fuzz runs for the tests
+force = true                                                # always perform a clean build
+fuzz = { runs = 10_000, max_test_rejects = 150_0000 }       # increase the number of fuzz runs and maximum number of combined inputs that may be rejected for the tests
 invariant = { runs = 5_000 }                                # increase the number of runs that must execute for each invariant test group
 verbosity = 4                                               # increase the verbosity of tests
diff --git a/lib/create-util b/lib/create-util
index e1ebc2e9..76738686 160000
--- a/lib/create-util
+++ b/lib/create-util
@@ -1,1 +1,1 @@
-Subproject commit e1ebc2e93df0e18b50a2cd3b7323bdc3373c5a8a
+Subproject commit 76738686b4e9b9c5967f216ee512ef5bc27e6044
diff --git a/lib/forge-std b/lib/forge-std
index 5bbb2aa8..fd86115e 160000
--- a/lib/forge-std
+++ b/lib/forge-std
@@ -1,1 +1,1 @@
-Subproject commit 5bbb2aa82d86456dbf03305c3704cbd4fa93473f
+Subproject commit fd86115ed6aba8e234ee0fb86c12fe35eff0b2a0
diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts
index adb861fb..dad73159 160000
--- a/lib/openzeppelin-contracts
+++ b/lib/openzeppelin-contracts
@@ -1,1 +1,1 @@
-Subproject commit adb861fb3bb4b4167f10307b8d091326115502f9
+Subproject commit dad73159df3d3053c72b5e430fa8164330f18068
diff --git a/lib/utils/VyperDeployer.sol b/lib/utils/VyperDeployer.sol
index 7b4595e0..7ab7fcd8 100644
--- a/lib/utils/VyperDeployer.sol
+++ b/lib/utils/VyperDeployer.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Create} from "create-util/Create.sol";
 
diff --git a/package.json b/package.json
index d52c38fb..465cf828 100644
--- a/package.json
+++ b/package.json
@@ -31,9 +31,9 @@
   },
   "devDependencies": {
     "@openzeppelin/merkle-tree": "^1.0.2",
-    "eslint": "^8.34.0",
+    "eslint": "^8.35.0",
     "eslint-config-prettier": "^8.6.0",
-    "ethers": "^6.0.5",
+    "ethers": "^6.0.8",
     "keccak256": "^1.0.6",
     "merkletreejs": "^0.3.9",
     "prettier": "^2.8.4",
diff --git a/remappings.txt b/remappings.txt
index ee96e929..edddc5cc 100644
--- a/remappings.txt
+++ b/remappings.txt
@@ -2,6 +2,7 @@ utils/=lib/utils/
 murky/=lib/murky/src/
 prb/test/=lib/prb-test/src/
 forge-std/=lib/forge-std/src/
+erc4626-tests/=lib/erc4626-tests/
 create-util/=lib/create-util/contracts/
 openzeppelin/=lib/openzeppelin-contracts/contracts/
 solidity-bytes-utils/=lib/solidity-bytes-utils/contracts/
diff --git a/src/extensions/ERC4626.vy b/src/extensions/ERC4626.vy
index d9b316c2..7ed18b70 100644
--- a/src/extensions/ERC4626.vy
+++ b/src/extensions/ERC4626.vy
@@ -1,7 +1,1025 @@
 # @version ^0.3.7
 """
-@title ERC-4626 Tokenised Vault Implementation
+@title Modern and Gas-Efficient ERC-4626 Tokenised Vault Implementation
 @license GNU Affero General Public License v3.0
 @author pcaversaccio
-@notice TBD
+@notice These functions implement the ERC-4626
+        standard interface:
+        - https://eips.ethereum.org/EIPS/eip-4626.
+        In addition, the following functions have
+        been added for convenience:
+        - `permit` (`external` function),
+        - `nonces` (`external` `view` function),
+        - `DOMAIN_SEPARATOR` (`external` `view` function).
+        The `permit` function implements approvals via
+        EIP-712 secp256k1 signatures:
+        https://eips.ethereum.org/EIPS/eip-2612.
+        The implementation is inspired by OpenZeppelin's
+        implementation here:
+        https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol,
+        as well as by fubuloubu's implementation here:
+        https://github.com/fubuloubu/ERC4626/blob/main/contracts/VyperVault.vy.
+@custom:security Most of the following security analysis was sourced from OpenZeppelin's
+                 implementation: This implementation uses virtual assets and shares to
+                 mitigate the risk of inflation attacks. The `internal` `immutable` variable
+                 `_DECIMALS_OFFSET` corresponds to an offset in the decimal representation
+                 between the underlying asset's decimals and the vault decimals. This offset
+                 also determines the rate of virtual shares to virtual assets in the vault,
+                 which itself determines the initial exchange rate. While not fully
+                 preventing the attack, analysis (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/docs/modules/ROOT/pages/erc4626.adoc#security-concern-inflation-attack)
+                 shows that a standard offset of `0` makes it non-profitable, as a result
+                 of the value being captured by the virtual shares (out of the attacker's
+                 donation) matching the attacker's expected gains. With a larger offset,
+                 the attack becomes orders of magnitude more expensive than it is profitable.
+                 More details about the underlying mathematics can be found here:
+                 https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/docs/modules/ROOT/pages/erc4626.adoc#security-concern-inflation-attack.
+
+                 Furthermore, another potential approach would be that vault deployers can
+                 protect against this attack by making an initial deposit of a non-trivial
+                 amount of the asset, such that price manipulation becomes infeasible. For
+                 the detailed discussion, please refer to:
+                 https://ethereum-magicians.org/t/address-eip-4626-inflation-attacks-with-virtual-shares-and-assets/12677.
+
+                 The drawback of the implemented approach is that the virtual shares do
+                 capture (a very small) part of the value being accrued to the vault. Also,
+                 if the vault experiences losses, the users try to exit the vault, the virtual
+                 shares and assets will cause the first user to exit to experience reduced losses
+                 in detriment to the last users that will experience bigger losses.
 """
+
+
+# @dev We import and implement the `ERC20` interface,
+# which is a built-in interface of the Vyper compiler.
+# @notice We do not import and implement the interface
+# `ERC20Detailed` (https://github.com/vyperlang/vyper/blob/master/vyper/builtins/interfaces/ERC20Detailed.py)
+# to be able to declare `name`, `symbol`, and `decimals`
+# as `immutable` variables. This is a known compiler bug
+# (https://github.com/vyperlang/vyper/issues/3130) and
+# we will import and implement the interface `ERC20Detailed`
+# once it is fixed.
+from vyper.interfaces import ERC20
+implements: ERC20
+
+
+# @dev We import and implement the `IERC20Permit`
+# interface, which is written using standard Vyper
+# syntax.
+import src.tokens.interfaces.IERC20Permit as IERC20Permit
+implements: IERC20Permit
+
+
+# @dev We import the `ERC4626` interface, which is a
+# built-in interface of the Vyper compiler.
+# @notice We do not implement the interface `ERC4626`
+# (https://github.com/vyperlang/vyper/blob/master/vyper/builtins/interfaces/ERC4626.py)
+# to be able to declare `asset` as an `immutable` variable.
+# This is a known compiler bug (https://github.com/vyperlang/vyper/issues/3130)
+# and we will implement the interface `ERC4626` once it
+# is fixed.
+from vyper.interfaces import ERC4626
+
+
+# @dev Constant used as part of the ECDSA recovery function.
+_MALLEABILITY_THRESHOLD: constant(bytes32) = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0
+
+
+# @dev The 32-byte type hash of the `permit` function.
+_PERMIT_TYPE_HASH: constant(bytes32) = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
+
+
+# @dev Returns the name of the token.
+# @notice If you declare a variable as `public`,
+# Vyper automatically generates an `external`
+# getter function for the variable. Furthermore,
+# to preserve consistency with the interface for
+# the optional metadata functions of the ERC-20
+# standard, we use lower case letters for the
+# `immutable` variables `name`, `symbol`, and
+# `decimals`.
+name: public(immutable(String[25]))
+
+
+# @dev Returns the symbol of the token.
+# @notice See comment on lower case letters
+# above at `name`.
+symbol: public(immutable(String[5]))
+
+
+# @dev Returns the decimals places of the token.
+# @notice See comment on lower case letters
+# above at `name`.
+decimals: public(immutable(uint8))
+
+
+# @dev Returns the address of the underlying token
+# used for the vault for accounting, depositing,
+# and withdrawing. To preserve consistency with the
+# ERC-4626 interface, we use lower case letters for
+# the `immutable` variable `name`.
+# @notice Vyper returns the `address` type for interface
+# types by default.
+asset: public(immutable(ERC20))
+
+
+# @dev Caches the domain separator as an `immutable`
+# value, but also stores the corresponding chain id
+# to invalidate the cached domain separator if the
+# chain id changes.
+_CACHED_CHAIN_ID: immutable(uint256)
+_CACHED_SELF: immutable(address)
+_CACHED_DOMAIN_SEPARATOR: immutable(bytes32)
+
+
+# @dev `immutable` variables to store the name,
+# version, and type hash during contract creation.
+_HASHED_NAME: immutable(bytes32)
+_HASHED_VERSION: immutable(bytes32)
+_TYPE_HASH: immutable(bytes32)
+
+
+# @dev An offset in the decimal representation between
+# the underlying asset's decimals and the vault decimals.
+# @notice While not fully preventing the attack, analysis
+# (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/docs/modules/ROOT/pages/erc4626.adoc#security-concern-inflation-attack)
+# shows that a standard offset of `0` makes an inflation
+# attack non-profitable.
+_DECIMALS_OFFSET: immutable(uint8)
+
+
+# @dev Caches the underlying asset's decimals.
+_UNDERLYING_DECIMALS: immutable(uint8)
+
+
+# @dev Returns the amount of tokens owned by an `address`.
+balanceOf: public(HashMap[address, uint256])
+
+
+# @dev Returns the remaining number of tokens that a
+# `spender` will be allowed to spend on behalf of
+# `owner` through `transferFrom`. This is zero by
+# default. This value changes when `approve`,
+# `increase_allowance`, `decrease_allowance`, or
+# `transferFrom` are called.
+allowance: public(HashMap[address, HashMap[address, uint256]])
+
+
+# @dev Returns the amount of tokens in existence.
+totalSupply: public(uint256)
+
+
+# @dev Returns the current on-chain tracked nonce
+# of `address`.
+nonces: public(HashMap[address, uint256])
+
+
+# @dev Emitted when `amount` tokens are moved
+# from one account (`owner`) to another (`to`).
+# Note that the parameter `amount` may be zero.
+event Transfer:
+    owner: indexed(address)
+    to: indexed(address)
+    amount: uint256
+
+
+# @dev Emitted when the allowance of a `spender`
+# for an `owner` is set by a call to `approve`.
+# The parameter `amount` is the new allowance.
+event Approval:
+    owner: indexed(address)
+    spender: indexed(address)
+    amount: uint256
+
+
+# @dev Emitted when `sender` has exchanged `assets`
+# for `shares`, and transferred those `shares`
+# to `owner`.
+event Deposit:
+    sender: indexed(address)
+    owner: indexed(address)
+    assets: uint256
+    shares: uint256
+
+
+# @dev Emitted when `sender` has exchanged `shares`,
+# owned by `owner`, for `assets`, and transferred
+# those `assets` to `receiver`.
+event Withdraw:
+    sender: indexed(address)
+    receiver: indexed(address)
+    owner: indexed(address)
+    assets: uint256
+    shares: uint256
+
+
+@external
+@payable
+def __init__(name_: String[25], symbol_: String[5], asset_: ERC20, decimals_offset_: uint8, name_eip712_: String[50], version_eip712_: String[20]):
+    """
+    @dev To omit the opcodes for checking the `msg.value`
+         in the creation-time EVM bytecode, the constructor
+         is declared as `payable`.
+    @param name_ The maximum 25-character user-readable
+           string name of the token.
+    @param symbol_ The maximum 5-character user-readable
+           string symbol of the token.
+    @param asset_ The ERC-20 compatible (i.e. ERC-777 is also viable)
+           underlying asset contract.
+    @param decimals_offset_ The 1-byte offset in the decimal
+           representation between the underlying asset's
+           decimals and the vault decimals. The recommended value to
+           mitigate the risk of an inflation attack is `0`.
+    @param name_eip712_ The maximum 50-character user-readable
+           string name of the signing domain, i.e. the name
+           of the dApp or protocol.
+    @param version_eip712_ The maximum 20-character current
+           main version of the signing domain. Signatures
+           from different versions are not compatible.
+    """
+    name = name_
+    symbol = symbol_
+    asset = asset_
+
+    success: bool = empty(bool)
+    decoded_decimals: uint8 = empty(uint8)
+    # Attempt to fetch the underlying's decimals. A return
+    # value of `False` indicates that the attempt failed in
+    # some way.
+    success, decoded_decimals = self._try_get_underlying_decimals(asset_)
+
+    underlying_decimals: uint8 = empty(uint8)
+    # Due to a known compiler bug (https://github.com/vyperlang/vyper/issues/3278),
+    # we use a local variable for the first value assignment.
+    if (success):
+        underlying_decimals = decoded_decimals
+    else:
+        underlying_decimals = 18
+
+    _UNDERLYING_DECIMALS = underlying_decimals
+    _DECIMALS_OFFSET = decimals_offset_
+    # The following line uses intentionally checked arithmetic
+    # to prevent a theoretically possible overflow.
+    decimals = _UNDERLYING_DECIMALS + _DECIMALS_OFFSET
+
+    hashed_name: bytes32 = keccak256(convert(name_eip712_, Bytes[50]))
+    hashed_version: bytes32 = keccak256(convert(version_eip712_, Bytes[20]))
+    type_hash: bytes32 = keccak256(convert("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)", Bytes[82]))
+    _HASHED_NAME = hashed_name
+    _HASHED_VERSION = hashed_version
+    _TYPE_HASH = type_hash
+    _CACHED_CHAIN_ID = chain.id
+    _CACHED_SELF = self
+    _CACHED_DOMAIN_SEPARATOR = self._build_domain_separator(type_hash, hashed_name, hashed_version)
+
+
+@external
+def transfer(to: address, amount: uint256) -> bool:
+    """
+    @dev Sourced from {ERC20-transfer}.
+    @notice See {ERC20-transfer} for the function
+            docstring.
+    """
+    self._transfer(msg.sender, to, amount)
+    return True
+
+
+@external
+def approve(spender: address, amount: uint256) -> bool:
+    """
+    @dev Sourced from {ERC20-approve}.
+    @notice See {ERC20-approve} for the function
+            docstring.
+    """
+    self._approve(msg.sender, spender, amount)
+    return True
+
+
+@external
+def transferFrom(owner: address, to: address, amount: uint256) -> bool:
+    """
+    @dev Sourced from {ERC20-transferFrom}.
+    @notice See {ERC20-transferFrom} for the function
+            docstring.
+    """
+    self._spend_allowance(owner, msg.sender, amount)
+    self._transfer(owner, to, amount)
+    return True
+
+
+@external
+def increase_allowance(spender: address, added_amount: uint256) -> bool:
+    """
+    @dev Sourced from {ERC20-increase_allowance}.
+    @notice See {ERC20-increase_allowance} for the
+            function docstring.
+    """
+    self._approve(msg.sender, spender, self.allowance[msg.sender][spender] + added_amount)
+    return True
+
+
+@external
+def decrease_allowance(spender: address, subtracted_amount: uint256) -> bool:
+    """
+    @dev Sourced from {ERC20-decrease_allowance}.
+    @notice See {ERC20-decrease_allowance} for the
+            function docstring.
+    """
+    current_allowance: uint256 = self.allowance[msg.sender][spender]
+    assert current_allowance >= subtracted_amount, "ERC20: decreased allowance below zero"
+    self._approve(msg.sender, spender, unsafe_sub(current_allowance, subtracted_amount))
+    return True
+
+
+@external
+def permit(owner: address, spender: address, amount: uint256, deadline: uint256, v: uint8, r: bytes32, s: bytes32):
+    """
+    @dev Sourced from {ERC20-permit}.
+    @notice See {ERC20-permit} for the function
+            docstring.
+    """
+    assert block.timestamp <= deadline, "ERC20Permit: expired deadline"
+
+    current_nonce: uint256 = self.nonces[owner]
+    self.nonces[owner] = unsafe_add(current_nonce, 1)
+
+    struct_hash: bytes32 = keccak256(_abi_encode(_PERMIT_TYPE_HASH, owner, spender, amount, current_nonce, deadline))
+    hash: bytes32  = self._hash_typed_data_v4(struct_hash)
+
+    signer: address = self._recover_vrs(hash, convert(v, uint256), convert(r, uint256), convert(s, uint256))
+    assert signer == owner, "ERC20Permit: invalid signature"
+
+    self._approve(owner, spender, amount)
+
+
+@external
+@view
+def DOMAIN_SEPARATOR() -> bytes32:
+    """
+    @dev Sourced from {ERC20-DOMAIN_SEPARATOR}.
+    @notice See {ERC20-DOMAIN_SEPARATOR} for the
+            function docstring.
+    """
+    return self._domain_separator_v4()
+
+
+@external
+@view
+def totalAssets() -> uint256:
+    """
+    @dev Returns the total amount of the underlying asset
+         that is managed by the vault.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#totalassets.
+    @return uint256 The 32-byte total managed assets.
+    """
+    return asset.balanceOf(self)
+
+
+@external
+@view
+def convertToShares(assets: uint256) -> uint256:
+    """
+    @dev Returns the amount of shares that the vault would
+         exchange for the amount of assets provided, in an
+         ideal scenario where all the conditions are met.
+    @notice Note that the conversion must round down to 0.
+            For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#converttoshares.
+    @param assets The 32-byte assets amount.
+    @return uint256 The converted 32-byte shares amount.
+    """
+    return self._convert_to_shares(assets, False)
+
+
+@external
+@view
+def convertToAssets(shares: uint256) -> uint256:
+    """
+    @dev Returns the amount of assets that the vault would
+         exchange for the amount of shares provided, in an
+         ideal scenario where all the conditions are met.
+    @notice Note that the conversion must round down to 0.
+            For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#converttoassets.
+    @param shares The 32-byte shares amount.
+    @return uint256 The converted 32-byte assets amount.
+    """
+    return self._convert_to_assets(shares, False)
+
+
+@external
+@view
+def maxDeposit(receiver: address) -> uint256:
+    """
+    @dev Returns the maximum amount of the underlying asset
+         that can be deposited into the vault for the `receiver`,
+         through a `deposit` call.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#maxdeposit.
+    @param receiver The 20-byte receiver address.
+    @return uint256 The 32-byte maximum deposit amount.
+    """
+    return max_value(uint256)
+
+
+@external
+@view
+def previewDeposit(assets: uint256) -> uint256:
+    """
+    @dev Allows an on-chain or off-chain user to simulate the
+         effects of their deposit at the current block, given
+         current on-chain conditions.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#previewdeposit.
+    @param assets The 32-byte assets amount.
+    @return uint256 The simulated 32-byte returning shares amount.
+    """
+    return self._convert_to_shares(assets, False)
+
+
+@external
+def deposit(assets: uint256, receiver: address) -> uint256:
+    """
+    @dev Mints `shares` vault shares to `receiver` by depositing
+         exactly `assets` of underlying tokens.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#deposit.
+    @param assets The 32-byte assets amount.
+    @param receiver The 20-byte receiver address.
+    @return uint256 The 32-byte shares amount to be created.
+    """
+    assert assets <= ERC4626(self).maxDeposit(receiver), "ERC4626: deposit more than maximum"
+    shares: uint256 = ERC4626(self).previewDeposit(assets)
+    self._deposit(msg.sender, receiver, assets, shares)
+    return shares
+
+
+@external
+@view
+def maxMint(receiver: address) -> uint256:
+    """
+    @dev Returns the maximum amount of shares that can be minted
+         from the vault for the `receiver`, through a `mint` call.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#maxmint.
+    @param receiver The 20-byte receiver address.
+    @return uint256 The 32-byte maximum mint amount.
+    """
+    return max_value(uint256)
+
+
+@external
+@view
+def previewMint(shares: uint256) -> uint256:
+    """
+    @dev Allows an on-chain or off-chain user to simulate the
+         effects of their `mint` at the current block, given
+         current on-chain conditions.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#previewmint.
+    @param shares The 32-byte shares amount.
+    @return uint256 The simulated 32-byte required assets amount.
+    """
+    return self._convert_to_assets(shares, True)
+
+
+@external
+def mint(shares: uint256, receiver:address) -> uint256:
+    """
+    @dev Mints exactly `shares` vault shares to `receiver` by
+         depositing `assets` of underlying tokens.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#mint.
+    @param shares The 32-byte shares amount to be created.
+    @param receiver The 20-byte receiver address.
+    @return uint256 The deposited 32-byte assets amount.
+    """
+    assert shares <= ERC4626(self).maxMint(receiver), "ERC4626: mint more than maximum"
+    assets: uint256 = ERC4626(self).previewMint(shares)
+    self._deposit(msg.sender, receiver, assets, shares)
+    return assets
+
+
+@external
+@view
+def maxWithdraw(owner: address) -> uint256:
+    """
+    @dev Returns the maximum amount of the underlying asset that
+         can be withdrawn from the owner balance in the vault,
+         through a `withdraw` call.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#maxwithdraw.
+    @param owner The 20-byte owner address.
+    @return uint256 The 32-byte maximum withdraw amount.
+    """
+    return self._convert_to_assets(self.balanceOf[owner], False)
+
+
+@external
+@view
+def previewWithdraw(assets: uint256) -> uint256:
+    """
+    @dev Allows an on-chain or off-chain user to simulate the
+         effects of their withdrawal at the current block, given
+         current on-chain conditions.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#previewwithdraw.
+    @param assets The 32-byte assets amount.
+    @return uint256 The simulated 32-byte burned shares amount.
+    """
+    return self._convert_to_shares(assets, True)
+
+
+@external
+def withdraw(assets: uint256, receiver: address, owner: address) -> uint256:
+    """
+    @dev Burns `shares` from `owner` and sends exactly `assets` of
+         underlying tokens to `receiver`.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#withdraw.
+    @param assets The 32-byte assets amount to be withdrawn.
+    @param receiver The 20-byte receiver address.
+    @param owner The 20-byte owner address.
+    @return uint256 The burned 32-byte shares amount.
+    """
+    assert assets <= ERC4626(self).maxWithdraw(receiver), "ERC4626: withdraw more than maximum"
+    shares: uint256 = ERC4626(self).previewWithdraw(assets)
+    self._withdraw(msg.sender, receiver, owner, assets, shares)
+    return shares
+
+
+@external
+@view
+def maxRedeem(owner: address) -> uint256:
+    """
+    @dev Maximum amount of vault shares that can be redeemed from
+         the `owner` balance in the vault, through a `redeem` call.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#maxredeem.
+    @param owner The 20-byte owner address.
+    @return uint256 The 32-byte maximum redeemable shares amount.
+    """
+    return self.balanceOf[owner]
+
+
+@external
+@view
+def previewRedeem(shares: uint256) -> uint256:
+    """
+    @dev Allows an on-chain or off-chain user to simulate the effects
+         of their redeemption at the current block, given current
+         on-chain conditions.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#previewredeem.
+    @param shares The 32-byte shares amount to be redeemed.
+    @return uint256 The simulated 32-byte returning assets amount.
+    """
+    return self._convert_to_assets(shares, False)
+
+
+@external
+def redeem(shares: uint256, receiver: address, owner: address) -> uint256:
+    """
+    @dev Burns exactly `shares` from `owner` and sends `assets` of
+         underlying tokens to `receiver`.
+    @notice For the to be fulfilled conditions, please refer to:
+            https://eips.ethereum.org/EIPS/eip-4626#redeem.
+    @param shares The 32-byte redeemed shares amount.
+    @param receiver The 20-byte receiver address.
+    @param owner The 20-byte owner address.
+    @return uint256 The returned 32-byte assets amount.
+    """
+    assert shares <= ERC4626(self).maxRedeem(owner), "ERC4626: redeem more than maximum"
+    assets: uint256 = ERC4626(self).previewRedeem(shares)
+    self._withdraw(msg.sender, receiver, owner, assets, shares)
+    return assets
+
+
+@internal
+def _transfer(owner: address, to: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_transfer}.
+    @notice See {ERC20-_transfer} for the function
+            docstring.
+    """
+    assert owner != empty(address), "ERC20: transfer from the zero address"
+    assert to != empty(address), "ERC20: transfer to the zero address"
+
+    self._before_token_transfer(owner, to, amount)
+
+    owner_balanceOf: uint256 = self.balanceOf[owner]
+    assert owner_balanceOf >= amount, "ERC20: transfer amount exceeds balance"
+    self.balanceOf[owner] = unsafe_sub(owner_balanceOf, amount)
+    self.balanceOf[to] = unsafe_add(self.balanceOf[to], amount)
+    log Transfer(owner, to, amount)
+
+    self._after_token_transfer(owner, to, amount)
+
+
+@internal
+def _mint(owner: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_mint}.
+    @notice See {ERC20-_mint} for the function
+            docstring.
+    """
+    assert owner != empty(address), "ERC20: mint to the zero address"
+
+    self._before_token_transfer(empty(address), owner, amount)
+
+    self.totalSupply += amount
+    self.balanceOf[owner] = unsafe_add(self.balanceOf[owner], amount)
+    log Transfer(empty(address), owner, amount)
+
+    self._after_token_transfer(empty(address), owner, amount)
+
+
+@internal
+def _burn(owner: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_burn}.
+    @notice See {ERC20-_burn} for the function
+            docstring.
+    """
+    assert owner != empty(address), "ERC20: burn from the zero address"
+
+    self._before_token_transfer(owner, empty(address), amount)
+
+    account_balance: uint256 = self.balanceOf[owner]
+    assert account_balance >= amount, "ERC20: burn amount exceeds balance"
+    self.balanceOf[owner] = unsafe_sub(account_balance, amount)
+    self.totalSupply = unsafe_sub(self.totalSupply, amount)
+    log Transfer(owner, empty(address), amount)
+
+    self._after_token_transfer(owner, empty(address), amount)
+
+
+@internal
+def _approve(owner: address, spender: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_approve}.
+    @notice See {ERC20-_approve} for the function
+            docstring.
+    """
+    assert owner != empty(address), "ERC20: approve from the zero address"
+    assert spender != empty(address), "ERC20: approve to the zero address"
+
+    self.allowance[owner][spender] = amount
+    log Approval(owner, spender, amount)
+
+
+@internal
+def _spend_allowance(owner: address, spender: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_approve}.
+    @notice See {ERC20-_spend_allowance} for the
+            function docstring.
+    """
+    current_allowance: uint256 = self.allowance[owner][spender]
+    if (current_allowance != max_value(uint256)):
+        # The following line allows the commonly known address
+        # poisoning attack, where `transferFrom` instructions
+        # are executed from arbitrary addresses with an `amount`
+        # of 0. However, this poisoning attack is not an on-chain
+        # vulnerability. All assets are safe. It is an off-chain
+        # log interpretation issue.
+        assert current_allowance >= amount, "ERC20: insufficient allowance"
+        self._approve(owner, spender, unsafe_sub(current_allowance, amount))
+
+
+@internal
+def _before_token_transfer(owner: address, to: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_before_token_transfer}.
+    @notice See {ERC20-_before_token_transfer} for
+            the function docstring.
+    """
+    pass
+
+
+@internal
+def _after_token_transfer(owner: address, to: address, amount: uint256):
+    """
+    @dev Sourced from {ERC20-_after_token_transfer}.
+    @notice See {ERC20-_after_token_transfer} for
+            the function docstring.
+    """
+    pass
+
+
+@internal
+@view
+def _domain_separator_v4() -> bytes32:
+    """
+    @dev Sourced from {EIP712DomainSeparator-domain_separator_v4}.
+    @notice See {EIP712DomainSeparator-domain_separator_v4}
+            for the function docstring.
+    """
+    if (self == _CACHED_SELF and chain.id == _CACHED_CHAIN_ID):
+        return _CACHED_DOMAIN_SEPARATOR
+    else:
+        return self._build_domain_separator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION)
+
+
+@internal
+@view
+def _build_domain_separator(type_hash: bytes32, name_hash: bytes32, version_hash: bytes32) -> bytes32:
+    """
+    @dev Sourced from {EIP712DomainSeparator-_build_domain_separator}.
+    @notice See {EIP712DomainSeparator-_build_domain_separator}
+            for the function docstring.
+    """
+    return keccak256(_abi_encode(type_hash, name_hash, version_hash, chain.id, self))
+
+
+@internal
+@view
+def _hash_typed_data_v4(struct_hash: bytes32) -> bytes32:
+    """
+    @dev Sourced from {EIP712DomainSeparator-hash_typed_data_v4}.
+    @notice See {EIP712DomainSeparator-hash_typed_data_v4}
+            for the function docstring.
+    """
+    return self._to_typed_data_hash(self._domain_separator_v4(), struct_hash)
+
+
+@internal
+@pure
+def _to_typed_data_hash(domain_separator: bytes32, struct_hash: bytes32) -> bytes32:
+    """
+    @dev Sourced from {ECDSA-to_typed_data_hash}.
+    @notice See {ECDSA-to_typed_data_hash} for the
+            function docstring.
+    """
+    return keccak256(concat(b"\x19\x01", domain_separator, struct_hash))
+
+
+@internal
+@pure
+def _recover_vrs(hash: bytes32, v: uint256, r: uint256, s: uint256) -> address:
+    """
+    @dev Sourced from {ECDSA-_recover_vrs}.
+    @notice See {ECDSA-_recover_vrs} for the
+            function docstring.
+    """
+    return self._try_recover_vrs(hash, v, r, s)
+
+
+@internal
+@pure
+def _try_recover_vrs(hash: bytes32, v: uint256, r: uint256, s: uint256) -> address:
+    """
+    @dev Sourced from {ECDSA-_try_recover_vrs}.
+    @notice See {ECDSA-_try_recover_vrs} for the
+            function docstring.
+    """
+    if (s > convert(_MALLEABILITY_THRESHOLD, uint256)):
+        raise "ECDSA: invalid signature 's' value"
+
+    signer: address = ecrecover(hash, v, r, s)
+    if (signer == empty(address)):
+        raise "ECDSA: invalid signature"
+    
+    return signer
+
+
+@internal
+@view
+def _try_get_underlying_decimals(underlying: ERC20) -> (bool, uint8):
+    """
+    @dev Attempts to fetch the underlying's decimals. A return
+         value of `False` indicates that the attempt failed in
+         some way.
+    @param underlying The ERC-20 compatible (i.e. ERC-777 is also viable)
+           underlying asset contract.
+    @return bool The verification whether the call succeeded or
+            failed.
+    @return uint8 The fetched underlying's decimals.
+    """
+    success: bool = empty(bool)
+    return_data: Bytes[32] = b""
+    # The following low-level call does not revert, but instead
+    # returns `False` if the callable contract does not implement
+    # the `decimals` function. Since we perform a length check of
+    # 32 bytes for the return data in the return expression at the
+    # end, we also return `False` for EOA wallets instead of reverting
+    # (remember that the EVM always considers a call to an EOA as
+    # successful with return data `0x`). Furthermore, it is important
+    # to note that an external call via `raw_call` does not perform an
+    # external code size check on the target address.
+    success, return_data = raw_call(underlying.address, method_id("decimals()"), max_outsize=32, is_static_call=True, revert_on_failure=False)
+    if (success and (len(return_data) == 32) and (convert(return_data, uint256) <= convert(max_value(uint8), uint256))):
+        return (True, convert(return_data, uint8))
+    return (False, empty(uint8))
+
+
+@internal
+@view
+def _convert_to_shares(assets: uint256, roundup: bool) -> uint256:
+    """
+    @dev An `internal` conversion function (from assets to shares)
+         with support for rounding direction.
+    @param assets The 32-byte assets amount.
+    @param roundup The Boolean variable that specifies whether
+           to round up or not. The default `False` is round down.
+    @return uint256 The converted 32-byte shares amount.
+    """
+    return self._mul_div(assets, self.totalSupply + 10 ** convert(_DECIMALS_OFFSET, uint256), ERC4626(self).totalAssets() + 1, roundup)
+
+
+@internal
+@view
+def _convert_to_assets(shares: uint256, roundup: bool) -> uint256:
+    """
+    @dev An `internal` conversion function (from shares to assets)
+         with support for rounding direction.
+    @param shares The 32-byte shares amount.
+    @param roundup The Boolean variable that specifies whether
+           to round up or not. The default `False` is round down.
+    @return uint256 The converted 32-byte assets amount.
+    """
+    return self._mul_div(shares, ERC4626(self).totalAssets() + 1, self.totalSupply + 10 ** convert(_DECIMALS_OFFSET, uint256), roundup)
+
+
+@internal
+def _deposit(sender: address, receiver: address, assets: uint256, shares: uint256):
+    """
+    @dev An `internal` function handling the `deposit` and `mint`
+         common workflow.
+    @param sender The 20-byte sender address.
+    @param receiver The 20-byte receiver address.
+    @param assets The 32-byte assets amount.
+    @param shares The 32-byte shares amount.
+    """
+    # If `asset` is an ERC-777, `transferFrom` can trigger a reentrancy
+    # before the transfer happens through the `tokensToSend` hook. On the
+    # other hand, the `tokenReceived` hook, that is triggered after the
+    # transfer, calls the vault which is assumed not to be malicious.
+    # Thus, we need to do the transfer before we mint so that any reentrancy
+    # would happen before the assets are transferred and before the shares
+    # are minted, which is a valid state.
+
+    # To deal with (potentially) non-compliant ERC-20 tokens that do have
+    # no return value, we use the kwarg `default_return_value` for external
+    # calls. This function was introduced in Vyper version 0.3.4. For more
+    # details see:
+    # - https://github.com/vyperlang/vyper/pull/2839,
+    # - https://github.com/vyperlang/vyper/issues/2812,
+    # - https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca.
+
+    # It is important to note that an external call via interface casting
+    # always performs an external code size check on the target address unless
+    # you add the kwarg `skip_contract_check=True`. If the check fails (i.e.
+    # the target address is an EOA), the call reverts.
+    assert asset.transferFrom(sender, self, assets, default_return_value=True), "ERC4626: transferFrom operation did not succeed"
+    self._mint(receiver, shares)
+    log Deposit(sender, receiver, assets, shares)
+
+
+@internal
+def _withdraw(sender: address, receiver: address, owner: address, assets: uint256, shares: uint256):
+    """
+    @dev An `internal` function handling the `withdraw` and `redeem`
+         common workflow.
+    @param sender The 20-byte sender address.
+    @param receiver The 20-byte receiver address.
+    @param owner The 20-byte owner address.
+    @param assets The 32-byte assets amount.
+    @param shares The 32-byte shares amount.
+    """
+    if (sender != owner):
+        self._spend_allowance(owner, sender, shares)
+
+    # If `asset` is an ERC-777, `transfer` can trigger a reentrancy
+    # after the transfer happens through the `tokensReceived` hook.
+    # On the other hand, the `tokensToSend` hook, that is triggered
+    # before the transfer, calls the vault which is assumed not to
+    # be malicious. Thus, we need to do the transfer after the burn
+    # so that any reentrancy would happen after the shares are burned
+    # and after the assets are transferred, which is a valid state.
+    self._burn(owner, shares)
+
+    # To deal with (potentially) non-compliant ERC-20 tokens that do have
+    # no return value, we use the kwarg `default_return_value` for external
+    # calls. This function was introduced in Vyper version 0.3.4. For more
+    # details see:
+    # - https://github.com/vyperlang/vyper/pull/2839,
+    # - https://github.com/vyperlang/vyper/issues/2812,
+    # - https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca.
+
+    # It is important to note that an external call via interface casting
+    # always performs an external code size check on the target address unless
+    # you add the kwarg `skip_contract_check=True`. If the check fails (i.e.
+    # the target address is an EOA), the call reverts.
+    assert asset.transfer(receiver, assets, default_return_value=True), "ERC4626: transfer operation did not succeed"
+    log Withdraw(sender, receiver, owner, assets, shares)
+
+
+@internal
+@pure
+def _mul_div(x: uint256, y: uint256, denominator: uint256, roundup: bool) -> uint256:
+    """
+    @dev Sourced from {Math-mul_div}.
+    @notice See {Math-mul_div} for the function
+            docstring.
+    """
+    # Handle division by zero.
+    assert denominator != empty(uint256), "Math: mul_div division by zero"
+
+    # 512-bit multiplication "[prod1 prod0] = x * y".
+    # Compute the product "mod 2**256" and "mod 2**256 - 1".
+    # Then use the Chinese Remainder theorem to reconstruct
+    # the 512-bit result. The result is stored in two 256-bit
+    # variables, where: "product = prod1 * 2**256 + prod0".
+    mm: uint256 = uint256_mulmod(x, y, max_value(uint256))
+    # The least significant 256 bits of the product.
+    prod0: uint256 = unsafe_mul(x, y)
+    # The most significant 256 bits of the product.
+    prod1: uint256 = empty(uint256)
+
+    if (mm < prod0):
+        prod1 = unsafe_sub(unsafe_sub(mm, prod0), 1)
+    else:
+        prod1 = unsafe_sub(mm, prod0)
+
+    # Handling of non-overflow cases, 256 by 256 division.
+    if (prod1 == empty(uint256)):
+        if (roundup and uint256_mulmod(x, y, denominator) != empty(uint256)):
+            # Calculate "ceil((x * y) / denominator)". The following
+            # line cannot overflow because we have the previous check
+            # "(x * y) % denominator != 0", which accordingly rules out
+            # the possibility of "x * y = 2**256 - 1" and `denominator == 1`.
+            return unsafe_add(unsafe_div(prod0, denominator), 1)
+        else:
+            return unsafe_div(prod0, denominator)
+
+    # Ensure that the result is less than 2**256. Also,
+    # prevents that `denominator == 0`.
+    assert denominator > prod1, "Math: mul_div overflow"
+
+    #######################
+    # 512 by 256 Division #
+    #######################
+
+    # Make division exact by subtracting the remainder
+    # from "[prod1 prod0]". First, compute remainder using
+    # the `uint256_mulmod` operation.
+    remainder: uint256 = uint256_mulmod(x, y, denominator)
+
+    # Second, subtract the 256-bit number from the 512-bit
+    # number.
+    if (remainder > prod0):
+        prod1 = unsafe_sub(prod1, 1)
+    prod0 = unsafe_sub(prod0, remainder)
+
+    # Factor powers of two out of the denominator and calculate
+    # the largest power of two divisor of denominator. Always `>= 1`,
+    # unless the denominator is zero (which is prevented above),
+    # in which case `twos` is zero. For more details, please refer to:
+    # https://cs.stackexchange.com/q/138556.
+
+    # The following line does not overflow because the denominator
+    # cannot be zero at this stage of the function.
+    twos: uint256 = denominator & (unsafe_add(~denominator, 1))
+    # Divide denominator by `twos`.
+    denominator_div: uint256 = unsafe_div(denominator, twos)
+    # Divide "[prod1 prod0]" by `twos`.
+    prod0 = unsafe_div(prod0, twos)
+    # Flip `twos` such that it is "2**256 / twos". If `twos` is zero,
+    # it becomes one.
+    twos = unsafe_add(unsafe_div(unsafe_sub(empty(uint256), twos), twos), 1)
+
+    # Shift bits from `prod1` to `prod0`.
+    prod0 |= unsafe_mul(prod1, twos)
+
+    # Invert the denominator "mod 2**256". Since the denominator is
+    # now an odd number, it has an inverse modulo 2**256, so we have:
+    # "denominator * inverse = 1 mod 2**256". Calculate the inverse by
+    # starting with a seed that is correct for four bits. That is,
+    # "denominator * inverse = 1 mod 2**4".
+    inverse: uint256 = unsafe_mul(3, denominator_div) ^ 2
+
+    # Use Newton-Raphson iteration to improve accuracy. Thanks to Hensel's
+    # lifting lemma, this also works in modular arithmetic by doubling the
+    # correct bits in each step.
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**8".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**16".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**32".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**64".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**128".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**256".
+
+    # Since the division is now exact, we can divide by multiplying
+    # with the modular inverse of the denominator. This returns the
+    # correct result modulo 2**256. Since the preconditions guarantee
+    # that the result is less than 2**256, this is the final result.
+    # We do not need to calculate the high bits of the result and
+    # `prod1` is no longer necessary.
+    result: uint256 = unsafe_mul(prod0, inverse)
+
+    if (roundup and uint256_mulmod(x, y, denominator) != empty(uint256)):
+        # Calculate "ceil((x * y) / denominator)". The following
+        # line uses intentionally checked arithmetic to prevent
+        # a theoretically possible overflow.
+        result += 1
+
+    return result
diff --git a/src/tokens/ERC20.vy b/src/tokens/ERC20.vy
index 18491917..0a6c512e 100644
--- a/src/tokens/ERC20.vy
+++ b/src/tokens/ERC20.vy
@@ -50,12 +50,12 @@
 
 # @dev We import and implement the `ERC20` interface,
 # which is a built-in interface of the Vyper compiler.
-# @notice We do not import the interface `ERC20Detailed`
-# (https://github.com/vyperlang/vyper/blob/master/vyper/builtin_interfaces/ERC20Detailed.py)
+# @notice We do not import and implement the interface
+# `ERC20Detailed` (https://github.com/vyperlang/vyper/blob/master/vyper/builtins/interfaces/ERC20Detailed.py)
 # to be able to declare `name`, `symbol`, and `decimals`
 # as `immutable` and `constant` variables. This is a
 # known compiler bug (https://github.com/vyperlang/vyper/issues/3130)
-# and we will import the interface `ERC20Detailed`
+# and we will import and implement the interface `ERC20Detailed`
 # once it is fixed.
 from vyper.interfaces import ERC20
 implements: ERC20
@@ -102,7 +102,7 @@ symbol: public(immutable(String[5]))
 
 
 # @dev Caches the domain separator as an `immutable`
-# value, but also store the corresponding chain id
+# value, but also stores the corresponding chain id
 # to invalidate the cached domain separator if the
 # chain id changes.
 _CACHED_CHAIN_ID: immutable(uint256)
@@ -430,7 +430,7 @@ def permit(owner: address, spender: address, amount: uint256, deadline: uint256,
 
     current_nonce: uint256 = self.nonces[owner]
     self.nonces[owner] = unsafe_add(current_nonce, 1)
-    
+
     struct_hash: bytes32 = keccak256(_abi_encode(_PERMIT_TYPE_HASH, owner, spender, amount, current_nonce, deadline))
     hash: bytes32  = self._hash_typed_data_v4(struct_hash)
 
diff --git a/src/tokens/ERC721.vy b/src/tokens/ERC721.vy
index ae5a8e59..29d19a58 100644
--- a/src/tokens/ERC721.vy
+++ b/src/tokens/ERC721.vy
@@ -126,7 +126,7 @@ _BASE_URI: immutable(String[80])
 
 
 # @dev Caches the domain separator as an `immutable`
-# value, but also store the corresponding chain id
+# value, but also stores the corresponding chain id
 # to invalidate the cached domain separator if the
 # chain id changes.
 _CACHED_CHAIN_ID: immutable(uint256)
diff --git a/src/utils/BatchDistributor.vy b/src/utils/BatchDistributor.vy
index 814d9318..b7e53fd1 100644
--- a/src/utils/BatchDistributor.vy
+++ b/src/utils/BatchDistributor.vy
@@ -47,6 +47,10 @@ def distribute_ether(data: Batch):
     @notice In the event that excessive ether is sent,
             the residual amount is returned back to the
             `msg.sender`.
+
+            Furthermore, it is important to note that an
+            external call via `raw_call` does not perform
+            an external code size check on the target address.
     @param data Nested struct object that contains an array
            of tuples that contain each a recipient address &
            ether amount in wei.
@@ -88,6 +92,10 @@ def distribute_token(token: ERC20, data: Batch):
     for txn in data.txns:
         total += txn.amount
 
+    # It is important to note that an external call via interface casting
+    # always performs an external code size check on the target address unless
+    # you add the kwarg `skip_contract_check=True`. If the check fails (i.e.
+    # the target address is an EOA), the call reverts.
     assert token.transferFrom(msg.sender, self, total, default_return_value=True), "BatchDistributor: transferFrom operation did not succeed"
 
     for txn in data.txns:
diff --git a/src/utils/EIP712DomainSeparator.vy b/src/utils/EIP712DomainSeparator.vy
index ef0ee772..044039ba 100644
--- a/src/utils/EIP712DomainSeparator.vy
+++ b/src/utils/EIP712DomainSeparator.vy
@@ -13,7 +13,7 @@
 
 
 # @dev Caches the domain separator as an `immutable`
-# value, but also store the corresponding chain id
+# value, but also stores the corresponding chain id
 # to invalidate the cached domain separator if the
 # chain id changes.
 _CACHED_CHAIN_ID: immutable(uint256)
diff --git a/src/utils/Math.vy b/src/utils/Math.vy
new file mode 100644
index 00000000..aaddb061
--- /dev/null
+++ b/src/utils/Math.vy
@@ -0,0 +1,132 @@
+# @version ^0.3.7
+"""
+@title Standard Mathematical Utility Functions
+@license GNU Affero General Public License v3.0
+@author pcaversaccio
+@notice These functions implement standard mathematical utility
+        functions that are missing in the Vyper language. If a
+        function is inspired by an existing implementation, it
+        is properly referenced in the function docstring.
+"""
+
+
+@external
+@pure
+def mul_div(x: uint256, y: uint256, denominator: uint256, roundup: bool) -> uint256:
+    """
+    @dev Calculates "(x * y) / denominator" in 512-bit precision,
+         following the selected rounding direction.
+    @notice The implementation is inspired by Remco Bloemen's
+            implementation under the MIT license here:
+            https://xn--2-umb.com/21/muldiv. Furthermore,
+            the rounding direction design pattern is inspired
+            by OpenZeppelin's implementation here:
+            https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/Math.sol.
+    @param x The 32-byte multiplicand.
+    @param y The 32-byte multiplier.
+    @param denominator The 32-byte divisor.
+    @param roundup The Boolean variable that specifies whether
+           to round up or not. The default `False` is round down.
+    @return uint256 The 32-byte calculation result.
+    """
+    # Handle division by zero.
+    assert denominator != empty(uint256), "Math: mul_div division by zero"
+
+    # 512-bit multiplication "[prod1 prod0] = x * y".
+    # Compute the product "mod 2**256" and "mod 2**256 - 1".
+    # Then use the Chinese Remainder theorem to reconstruct
+    # the 512-bit result. The result is stored in two 256-bit
+    # variables, where: "product = prod1 * 2**256 + prod0".
+    mm: uint256 = uint256_mulmod(x, y, max_value(uint256))
+    # The least significant 256 bits of the product.
+    prod0: uint256 = unsafe_mul(x, y)
+    # The most significant 256 bits of the product.
+    prod1: uint256 = empty(uint256)
+
+    if (mm < prod0):
+        prod1 = unsafe_sub(unsafe_sub(mm, prod0), 1)
+    else:
+        prod1 = unsafe_sub(mm, prod0)
+
+    # Handling of non-overflow cases, 256 by 256 division.
+    if (prod1 == empty(uint256)):
+        if (roundup and uint256_mulmod(x, y, denominator) != empty(uint256)):
+            # Calculate "ceil((x * y) / denominator)". The following
+            # line cannot overflow because we have the previous check
+            # "(x * y) % denominator != 0", which accordingly rules out
+            # the possibility of "x * y = 2**256 - 1" and `denominator == 1`.
+            return unsafe_add(unsafe_div(prod0, denominator), 1)
+        else:
+            return unsafe_div(prod0, denominator)
+
+    # Ensure that the result is less than 2**256. Also,
+    # prevents that `denominator == 0`.
+    assert denominator > prod1, "Math: mul_div overflow"
+
+    #######################
+    # 512 by 256 Division #
+    #######################
+
+    # Make division exact by subtracting the remainder
+    # from "[prod1 prod0]". First, compute remainder using
+    # the `uint256_mulmod` operation.
+    remainder: uint256 = uint256_mulmod(x, y, denominator)
+
+    # Second, subtract the 256-bit number from the 512-bit
+    # number.
+    if (remainder > prod0):
+        prod1 = unsafe_sub(prod1, 1)
+    prod0 = unsafe_sub(prod0, remainder)
+
+    # Factor powers of two out of the denominator and calculate
+    # the largest power of two divisor of denominator. Always `>= 1`,
+    # unless the denominator is zero (which is prevented above),
+    # in which case `twos` is zero. For more details, please refer to:
+    # https://cs.stackexchange.com/q/138556.
+
+    # The following line does not overflow because the denominator
+    # cannot be zero at this stage of the function.
+    twos: uint256 = denominator & (unsafe_add(~denominator, 1))
+    # Divide denominator by `twos`.
+    denominator_div: uint256 = unsafe_div(denominator, twos)
+    # Divide "[prod1 prod0]" by `twos`.
+    prod0 = unsafe_div(prod0, twos)
+    # Flip `twos` such that it is "2**256 / twos". If `twos` is zero,
+    # it becomes one.
+    twos = unsafe_add(unsafe_div(unsafe_sub(empty(uint256), twos), twos), 1)
+
+    # Shift bits from `prod1` to `prod0`.
+    prod0 |= unsafe_mul(prod1, twos)
+
+    # Invert the denominator "mod 2**256". Since the denominator is
+    # now an odd number, it has an inverse modulo 2**256, so we have:
+    # "denominator * inverse = 1 mod 2**256". Calculate the inverse by
+    # starting with a seed that is correct for four bits. That is,
+    # "denominator * inverse = 1 mod 2**4".
+    inverse: uint256 = unsafe_mul(3, denominator_div) ^ 2
+
+    # Use Newton-Raphson iteration to improve accuracy. Thanks to Hensel's
+    # lifting lemma, this also works in modular arithmetic by doubling the
+    # correct bits in each step.
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**8".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**16".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**32".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**64".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**128".
+    inverse = unsafe_mul(inverse, unsafe_sub(2, unsafe_mul(denominator_div, inverse))) # Inverse "mod 2**256".
+
+    # Since the division is now exact, we can divide by multiplying
+    # with the modular inverse of the denominator. This returns the
+    # correct result modulo 2**256. Since the preconditions guarantee
+    # that the result is less than 2**256, this is the final result.
+    # We do not need to calculate the high bits of the result and
+    # `prod1` is no longer necessary.
+    result: uint256 = unsafe_mul(prod0, inverse)
+
+    if (roundup and uint256_mulmod(x, y, denominator) != empty(uint256)):
+        # Calculate "ceil((x * y) / denominator)". The following
+        # line uses intentionally checked arithmetic to prevent
+        # a theoretically possible overflow.
+        result += 1
+
+    return result
diff --git a/src/utils/Multicall.vy b/src/utils/Multicall.vy
index 1cb9e341..30422970 100644
--- a/src/utils/Multicall.vy
+++ b/src/utils/Multicall.vy
@@ -69,6 +69,9 @@ def multicall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result, max_v
          function returns successfully if required.
          Since this function uses `CALL`, the `msg.sender`
          will be the multicall contract itself.
+    @notice It is important to note that an external call
+            via `raw_call` does not perform an external code
+            size check on the target address.
     @param data The array of `Batch` structs.
     @return DynArray The array of `Result` structs.
     """
@@ -96,6 +99,9 @@ def multicall_value(data: DynArray[BatchValue, max_value(uint8)]) -> DynArray[Re
          if required. Since this function uses `CALL`,
          the `msg.sender` will be the multicall contract
          itself.
+    @notice It is important to note that an external call
+            via `raw_call` does not perform an external code
+            size check on the target address.
     @param data The array of `BatchValue` structs.
     @return DynArray The array of `Result` structs.
     """
@@ -131,6 +137,10 @@ def multicall_self(data: DynArray[BatchSelf, max_value(uint8)]) -> DynArray[Resu
             calls in one transaction. Since the `msg.sender` is
             preserved, it's equivalent to sending multiple transactions
             from an EOA (externally-owned account, i.e. non-contract account).
+
+            Furthermore, it is important to note that an external
+            call via `raw_call` does not perform an external code
+            size check on the target address.
     @param data The array of `BatchSelf` structs.
     @return DynArray The array of `Result` structs.
     """
@@ -164,6 +174,10 @@ def multicall_value_self(data: DynArray[BatchValueSelf, max_value(uint8)]) -> Dy
             calls in one transaction. Since the `msg.sender` is
             preserved, it's equivalent to sending multiple transactions
             from an EOA (externally-owned account, i.e. non-contract account).
+
+            Furthermore, it is important to note that an external
+            call via `raw_call` does not perform an external code
+            size check on the target address.
     @param data The array of `BatchValueSelf` structs.
     @return DynArray The array of `Result` structs.
     """
@@ -192,6 +206,9 @@ def multistaticcall(data: DynArray[Batch, max_value(uint8)]) -> DynArray[Result,
     """
     @dev Aggregates static function calls, ensuring that each
          function returns successfully if required.
+    @notice It is important to note that an external call
+            via `raw_call` does not perform an external code
+            size check on the target address.
     @param data The array of `Batch` structs.
     @return DynArray The array of `Result` structs.
     """
diff --git a/src/utils/SignatureChecker.vy b/src/utils/SignatureChecker.vy
index 2254a52b..c3658851 100644
--- a/src/utils/SignatureChecker.vy
+++ b/src/utils/SignatureChecker.vy
@@ -104,7 +104,10 @@ def _is_valid_ERC1271_signature_now(signer: address, hash: bytes32, signature: B
     # check of 32 bytes for the return data in the return expression
     # at the end, we also return `False` for EOA wallets instead
     # of reverting (remember that the EVM always considers a call
-    # to an EOA as successful with return data `0x`).
+    # to an EOA as successful with return data `0x`). Furthermore,
+    # it is important to note that an external call via `raw_call`
+    # does not perform an external code size check on the target
+    # address.
     success, return_data = \
         raw_call(signer, _abi_encode(hash, signature, method_id=IERC1271_ISVALIDSIGNATURE_SELECTOR), max_outsize=32, is_static_call=True, revert_on_failure=False)
     return (success and (len(return_data) == 32) and (convert(return_data, bytes32) == convert(IERC1271_ISVALIDSIGNATURE_SELECTOR, bytes32)))
diff --git a/yarn.lock b/yarn.lock
index 1aca36ca..cc74999b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7,10 +7,10 @@
   resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.8.9.tgz#67b3acadebbb551669c9a1da15fac951db795b85"
   integrity sha512-93OmGCV0vO8+JQ3FHG+gZk/MPHzzMPDRiCiFcCQNTCnHaaxsacO3ScTPGlu2wX2dOtgfalbchPcw1cOYYjHCYQ==
 
-"@eslint/eslintrc@^1.4.1":
-  version "1.4.1"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e"
-  integrity sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==
+"@eslint/eslintrc@^2.0.0":
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.0.tgz#943309d8697c52fc82c076e90c1c74fbbe69dbff"
+  integrity sha512-fluIaaV+GyV24CCu/ggiHdV+j4RNh85yQnAYS/G2mZODZgGmmlrgCydjUcV3YvxCm9x8nMAfThsqTni4KiXT4A==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
@@ -22,6 +22,11 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"
 
+"@eslint/js@8.35.0":
+  version "8.35.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.35.0.tgz#b7569632b0b788a0ca0e438235154e45d42813a7"
+  integrity sha512-JXdzbRiWclLVoD8sNUjR443VVlYqiYmDVT6rGUEIEHU5YJW0gaVZwV2xgM7D4arkvASqD0IlLUVjHiFuxaftRw==
+
 "@ethersproject/abi@^5.7.0":
   version "5.7.0"
   resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449"
@@ -299,9 +304,9 @@
     "@types/node" "*"
 
 "@types/node@*":
-  version "18.13.0"
-  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850"
-  integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==
+  version "18.14.1"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.1.tgz#90dad8476f1e42797c49d6f8b69aaf9f876fc69f"
+  integrity sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==
 
 "@types/pbkdf2@^3.0.0":
   version "3.1.0"
@@ -607,12 +612,13 @@ eslint-visitor-keys@^3.3.0:
   resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
   integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
 
-eslint@^8.34.0:
-  version "8.34.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.34.0.tgz#fe0ab0ef478104c1f9ebc5537e303d25a8fb22d6"
-  integrity sha512-1Z8iFsucw+7kSqXNZVslXS8Ioa4u2KM7GPwuKtkTFAqZ/cHMcEaR+1+Br0wLlot49cNxIiZk5wp8EAbPcYZxTg==
+eslint@^8.35.0:
+  version "8.35.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.35.0.tgz#fffad7c7e326bae606f0e8f436a6158566d42323"
+  integrity sha512-BxAf1fVL7w+JLRQhWl2pzGeSiGqbWumV4WNvc9Rhp6tiCtm4oHnyPBSEtMGZwrQgudFQ+otqzWoPB7x+hxoWsw==
   dependencies:
-    "@eslint/eslintrc" "^1.4.1"
+    "@eslint/eslintrc" "^2.0.0"
+    "@eslint/js" "8.35.0"
     "@humanwhocodes/config-array" "^0.11.8"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
@@ -626,7 +632,7 @@ eslint@^8.34.0:
     eslint-utils "^3.0.0"
     eslint-visitor-keys "^3.3.0"
     espree "^9.4.0"
-    esquery "^1.4.0"
+    esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
     file-entry-cache "^6.0.1"
@@ -661,10 +667,10 @@ espree@^9.4.0:
     acorn-jsx "^5.3.2"
     eslint-visitor-keys "^3.3.0"
 
-esquery@^1.4.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
-  integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
+esquery@^1.4.2:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.2.tgz#c6d3fee05dd665808e2ad870631f221f5617b1d1"
+  integrity sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==
   dependencies:
     estraverse "^5.1.0"
 
@@ -734,10 +740,10 @@ ethereumjs-util@^7.1.0:
     ethereum-cryptography "^0.1.3"
     rlp "^2.2.4"
 
-ethers@^6.0.5:
-  version "6.0.5"
-  resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.0.5.tgz#88434ce7884511280584ee724cccbe76fbbef093"
-  integrity sha512-qy0HV9NZpRXdFW6Q15o9WCIpKsxQUPjiJPR7EGK8NJO58CQh59Acz58Z+9uEAgcL1e/0Q1DGGm7+WVTjQuvT7g==
+ethers@^6.0.8:
+  version "6.0.8"
+  resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.0.8.tgz#a02e31f2771b66ecab6c731c1141b30d9de09174"
+  integrity sha512-j5smdMwn4t4vEARcfUv54mTJ2NMCorYLL51wPjFInEnrRr2SF5Sl9a7Z4DXS8UO1fBJVGHnjDDrF1b7msY3f7Q==
   dependencies:
     "@adraffy/ens-normalize" "1.8.9"
     "@noble/hashes" "1.1.2"
@@ -1182,9 +1188,9 @@ randombytes@^2.1.0:
     safe-buffer "^5.1.0"
 
 readable-stream@^3.6.0:
-  version "3.6.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
-  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  version "3.6.1"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.1.tgz#f9f9b5f536920253b3d26e7660e7da4ccff9bb62"
+  integrity sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==
   dependencies:
     inherits "^2.0.3"
     string_decoder "^1.1.1"


Test patch:
diff --git a/lib/erc4626-tests b/lib/erc4626-tests
new file mode 160000
index 00000000..8b1d7c2a
--- /dev/null
+++ b/lib/erc4626-tests
@@ -0,0 +1,1 @@
+Subproject commit 8b1d7c2ac248c33c3506b1bff8321758943c5e11
diff --git a/lib/prb-test b/lib/prb-test
index fa795366..9c30adbc 160000
--- a/lib/prb-test
+++ b/lib/prb-test
@@ -1,1 +1,1 @@
-Subproject commit fa795366244b24c34896ac59f1b97d92dc022175
+Subproject commit 9c30adbc7b62b705287f8861d37141f68c64c773
diff --git a/test/auth/AccessControl.t.sol b/test/auth/AccessControl.t.sol
index 8218db2c..816e418c 100644
--- a/test/auth/AccessControl.t.sol
+++ b/test/auth/AccessControl.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/auth/Ownable.t.sol b/test/auth/Ownable.t.sol
index e76661b0..9548a3cd 100644
--- a/test/auth/Ownable.t.sol
+++ b/test/auth/Ownable.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/auth/Ownable2Step.t.sol b/test/auth/Ownable2Step.t.sol
index f11a6993..13aedd8e 100644
--- a/test/auth/Ownable2Step.t.sol
+++ b/test/auth/Ownable2Step.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/auth/interfaces/IAccessControlExtended.sol b/test/auth/interfaces/IAccessControlExtended.sol
index 98445c5d..a7dd9737 100644
--- a/test/auth/interfaces/IAccessControlExtended.sol
+++ b/test/auth/interfaces/IAccessControlExtended.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC165} from "openzeppelin/utils/introspection/IERC165.sol";
 import {IAccessControl} from "openzeppelin/access/IAccessControl.sol";
diff --git a/test/auth/interfaces/IOwnable.sol b/test/auth/interfaces/IOwnable.sol
index f78de753..9adf7b86 100644
--- a/test/auth/interfaces/IOwnable.sol
+++ b/test/auth/interfaces/IOwnable.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IOwnable {
     function owner() external view returns (address);
diff --git a/test/auth/interfaces/IOwnable2Step.sol b/test/auth/interfaces/IOwnable2Step.sol
index 344c2b2a..25231598 100644
--- a/test/auth/interfaces/IOwnable2Step.sol
+++ b/test/auth/interfaces/IOwnable2Step.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IOwnable2Step {
     function owner() external view returns (address);
diff --git a/test/extensions/ERC4626.t.sol b/test/extensions/ERC4626.t.sol
new file mode 100644
index 00000000..2bb7cff8
--- /dev/null
+++ b/test/extensions/ERC4626.t.sol
@@ -0,0 +1,2081 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.19;
+
+import {Test} from "forge-std/Test.sol";
+import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol";
+import {VyperDeployer} from "utils/VyperDeployer.sol";
+
+import {ERC20Mock} from "../utils/mocks/ERC20Mock.sol";
+import {ERC20ExcessDecimalsMock} from "./mocks/ERC20ExcessDecimalsMock.sol";
+
+import {IERC4626Extended} from "./interfaces/IERC4626Extended.sol";
+
+contract ERC4626VaultTest is ERC4626Test {
+    string private constant _NAME = "TokenisedVaultMock";
+    string private constant _NAME_UNDERLYING = "UnderlyingTokenMock";
+    string private constant _SYMBOL = "TVM";
+    string private constant _SYMBOL_UNDERLYING = "UTM";
+    string private constant _NAME_EIP712 = "TokenisedVaultMock";
+    string private constant _VERSION_EIP712 = "1";
+    uint8 private constant _DECIMALS_OFFSET = 0;
+    uint256 private constant _INITIAL_SUPPLY_UNDERLYING = type(uint8).max;
+    bytes32 private constant _TYPE_HASH =
+        keccak256(
+            bytes(
+                "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
+            )
+        );
+    bytes32 private constant _PERMIT_TYPE_HASH =
+        keccak256(
+            bytes(
+                "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
+            )
+        );
+
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+    ERC20Mock private underlying =
+        new ERC20Mock(
+            _NAME_UNDERLYING,
+            _SYMBOL_UNDERLYING,
+            makeAddr("initialAccount"),
+            _INITIAL_SUPPLY_UNDERLYING
+        );
+
+    /* solhint-disable var-name-mixedcase */
+    IERC4626Extended private ERC4626ExtendedDecimalsOffset0;
+    IERC4626Extended private ERC4626ExtendedDecimalsOffset6;
+    IERC4626Extended private ERC4626ExtendedDecimalsOffset12;
+    IERC4626Extended private ERC4626ExtendedDecimalsOffset18;
+    bytes32 private _CACHED_DOMAIN_SEPARATOR;
+    /* solhint-enable var-name-mixedcase */
+
+    address private deployer = address(vyperDeployer);
+    address private self = address(this);
+    address private underlyingAddr = address(underlying);
+    address private zeroAddress = address(0);
+    /* solhint-disable var-name-mixedcase */
+    address private ERC4626ExtendedDecimalsOffset0Addr;
+    address private ERC4626ExtendedDecimalsOffset6Addr;
+    address private ERC4626ExtendedDecimalsOffset12Addr;
+    address private ERC4626ExtendedDecimalsOffset18Addr;
+    /* solhint-enable var-name-mixedcase */
+
+    event Transfer(address indexed from, address indexed to, uint256 value);
+
+    event Approval(
+        address indexed owner,
+        address indexed spender,
+        uint256 value
+    );
+
+    event Deposit(
+        address indexed sender,
+        address indexed owner,
+        uint256 assets,
+        uint256 shares
+    );
+
+    event Withdraw(
+        address indexed sender,
+        address indexed receiver,
+        address indexed owner,
+        uint256 assets,
+        uint256 shares
+    );
+
+    function setUp() public override {
+        bytes memory argsDecimalsOffset0 = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            _DECIMALS_OFFSET,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        ERC4626ExtendedDecimalsOffset0 = IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffset0
+            )
+        );
+        ERC4626ExtendedDecimalsOffset0Addr = address(
+            ERC4626ExtendedDecimalsOffset0
+        );
+        _CACHED_DOMAIN_SEPARATOR = keccak256(
+            abi.encode(
+                _TYPE_HASH,
+                keccak256(bytes(_NAME_EIP712)),
+                keccak256(bytes(_VERSION_EIP712)),
+                block.chainid,
+                ERC4626ExtendedDecimalsOffset0Addr
+            )
+        );
+
+        bytes memory argsDecimalsOffset6 = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            _DECIMALS_OFFSET + 6,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        ERC4626ExtendedDecimalsOffset6 = IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffset6
+            )
+        );
+        ERC4626ExtendedDecimalsOffset6Addr = address(
+            ERC4626ExtendedDecimalsOffset6
+        );
+
+        bytes memory argsDecimalsOffset12 = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            _DECIMALS_OFFSET + 12,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        ERC4626ExtendedDecimalsOffset12 = IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffset12
+            )
+        );
+        ERC4626ExtendedDecimalsOffset12Addr = address(
+            ERC4626ExtendedDecimalsOffset12
+        );
+
+        bytes memory argsDecimalsOffset18 = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            _DECIMALS_OFFSET + 18,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        ERC4626ExtendedDecimalsOffset18 = IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffset18
+            )
+        );
+        ERC4626ExtendedDecimalsOffset18Addr = address(
+            ERC4626ExtendedDecimalsOffset18
+        );
+
+        /**
+         * @dev ERC-4626 property tests (https://github.com/a16z/erc4626-tests) setup.
+         */
+        _underlying_ = underlyingAddr;
+        _vault_ = ERC4626ExtendedDecimalsOffset0Addr;
+        _delta_ = 0;
+        _vaultMayBeEmpty = true;
+        _unlimitedAmount = true;
+    }
+
+    function testInitialSetup() public {
+        assertEq(ERC4626ExtendedDecimalsOffset0.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffset0.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffset0.decimals(), 18);
+        assertEq(ERC4626ExtendedDecimalsOffset0.asset(), underlyingAddr);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffset6.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffset6.decimals(), 18 + 6);
+        assertEq(ERC4626ExtendedDecimalsOffset6.asset(), underlyingAddr);
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffset12.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffset12.decimals(), 18 + 12);
+        assertEq(ERC4626ExtendedDecimalsOffset12.asset(), underlyingAddr);
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffset18.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffset18.decimals(), 18 + 18);
+        assertEq(ERC4626ExtendedDecimalsOffset18.asset(), underlyingAddr);
+
+        /**
+         * @dev Check the case where the asset has not yet been created.
+         */
+        bytes memory argsDecimalsOffsetEOA = abi.encode(
+            _NAME,
+            _SYMBOL,
+            makeAddr("someAccount"),
+            _DECIMALS_OFFSET + 3,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        // solhint-disable-next-line var-name-mixedcase
+        IERC4626Extended ERC4626ExtendedDecimalsOffsetEOA = IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffsetEOA
+            )
+        );
+        assertEq(ERC4626ExtendedDecimalsOffsetEOA.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffsetEOA.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffsetEOA.decimals(), 18 + 3);
+        assertEq(
+            ERC4626ExtendedDecimalsOffsetEOA.asset(),
+            makeAddr("someAccount")
+        );
+
+        /**
+         * @dev Check the case where success is `False`.
+         */
+        bytes memory argsDecimalsOffsetNoDecimals = abi.encode(
+            _NAME,
+            _SYMBOL,
+            deployer,
+            _DECIMALS_OFFSET + 6,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        // solhint-disable-next-line var-name-mixedcase
+        IERC4626Extended ERC4626ExtendedDecimalsOffsetNoDecimals = IERC4626Extended(
+                vyperDeployer.deployContract(
+                    "src/extensions/",
+                    "ERC4626",
+                    argsDecimalsOffsetNoDecimals
+                )
+            );
+        assertEq(ERC4626ExtendedDecimalsOffsetNoDecimals.name(), _NAME);
+        assertEq(ERC4626ExtendedDecimalsOffsetNoDecimals.symbol(), _SYMBOL);
+        assertEq(ERC4626ExtendedDecimalsOffsetNoDecimals.decimals(), 18 + 6);
+        assertEq(ERC4626ExtendedDecimalsOffsetNoDecimals.asset(), deployer);
+
+        /**
+         * @dev Check the case where the return value is above the
+         * maximum value of the type `uint8`.
+         */
+        address erc20ExcessDecimalsMock = address(
+            new ERC20ExcessDecimalsMock()
+        );
+        bytes memory argsDecimalsOffsetTooHighDecimals = abi.encode(
+            _NAME,
+            _SYMBOL,
+            erc20ExcessDecimalsMock,
+            _DECIMALS_OFFSET + 9,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        // solhint-disable-next-line var-name-mixedcase
+        IERC4626Extended ERC4626ExtendedDecimalsOffsetTooHighDecimals = IERC4626Extended(
+                vyperDeployer.deployContract(
+                    "src/extensions/",
+                    "ERC4626",
+                    argsDecimalsOffsetTooHighDecimals
+                )
+            );
+        assertEq(ERC4626ExtendedDecimalsOffsetTooHighDecimals.name(), _NAME);
+        assertEq(
+            ERC4626ExtendedDecimalsOffsetTooHighDecimals.symbol(),
+            _SYMBOL
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffsetTooHighDecimals.decimals(),
+            18 + 9
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffsetTooHighDecimals.asset(),
+            erc20ExcessDecimalsMock
+        );
+
+        /**
+         * @dev Check the case where calculated `decimals` value overflows
+         * the `uint8` type.
+         */
+        bytes memory argsDecimalsOffsetOverflow = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            type(uint8).max,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        vm.expectRevert();
+        IERC4626Extended(
+            vyperDeployer.deployContract(
+                "src/extensions/",
+                "ERC4626",
+                argsDecimalsOffsetOverflow
+            )
+        );
+    }
+
+    function testEmptyVaultDeposit() public {
+        address holder = makeAddr("holder");
+        address receiver = makeAddr("receiver");
+        uint256 assets = 1;
+        uint256 shares = 1;
+        vm.startPrank(holder);
+        underlying.mint(holder, type(uint16).max);
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset6Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset12Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset18Addr,
+            type(uint256).max
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.maxDeposit(holder),
+            type(uint256).max
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.previewDeposit(assets), shares);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.maxDeposit(holder),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.previewDeposit(assets),
+            shares * 10 ** 6
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.maxDeposit(holder),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.previewDeposit(assets),
+            shares * 10 ** 12
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.maxDeposit(holder),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.previewDeposit(assets),
+            shares * 10 ** 18
+        );
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset0Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Deposit(holder, receiver, assets, shares);
+        ERC4626ExtendedDecimalsOffset0.deposit(assets, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset6Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 6);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 6);
+        ERC4626ExtendedDecimalsOffset6.deposit(assets, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset12Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 12);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 12);
+        ERC4626ExtendedDecimalsOffset12.deposit(assets, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset18Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 18);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 18);
+        ERC4626ExtendedDecimalsOffset18.deposit(assets, receiver);
+
+        assertEq(underlying.balanceOf(holder), type(uint16).max - 4);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(receiver), shares);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), shares);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.balanceOf(receiver),
+            shares * 10 ** 6
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.totalSupply(),
+            shares * 10 ** 6
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.balanceOf(receiver),
+            shares * 10 ** 12
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.totalSupply(),
+            shares * 10 ** 12
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.balanceOf(receiver),
+            shares * 10 ** 18
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.totalSupply(),
+            shares * 10 ** 18
+        );
+        vm.stopPrank();
+    }
+
+    function testEmptyVaultMint() public {
+        address holder = makeAddr("holder");
+        address receiver = makeAddr("receiver");
+        uint256 assets = 1;
+        uint256 shares = 1;
+        vm.startPrank(holder);
+        underlying.mint(holder, type(uint16).max);
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset6Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset12Addr,
+            type(uint256).max
+        );
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset18Addr,
+            type(uint256).max
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.maxMint(receiver),
+            type(uint256).max
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.previewMint(shares), assets);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.maxMint(receiver),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.previewMint(shares * 10 ** 6),
+            assets
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.maxMint(receiver),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.previewMint(shares * 10 ** 12),
+            assets
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.maxMint(receiver),
+            type(uint256).max
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.previewMint(shares * 10 ** 18),
+            assets
+        );
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset0Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Deposit(holder, receiver, assets, shares);
+        ERC4626ExtendedDecimalsOffset0.mint(shares, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset6Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 6);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 6);
+        ERC4626ExtendedDecimalsOffset6.mint(shares * 10 ** 6, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset12Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 12);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 12);
+        ERC4626ExtendedDecimalsOffset12.mint(shares * 10 ** 12, receiver);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(holder, ERC4626ExtendedDecimalsOffset18Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Transfer(zeroAddress, receiver, shares * 10 ** 18);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Deposit(holder, receiver, assets, shares * 10 ** 18);
+        ERC4626ExtendedDecimalsOffset18.mint(shares * 10 ** 18, receiver);
+
+        assertEq(underlying.balanceOf(holder), type(uint16).max - 4);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(receiver), shares);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), shares);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.balanceOf(receiver),
+            shares * 10 ** 6
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset6.totalSupply(),
+            shares * 10 ** 6
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.balanceOf(receiver),
+            shares * 10 ** 12
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset12.totalSupply(),
+            shares * 10 ** 12
+        );
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), assets);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(holder), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.balanceOf(receiver),
+            shares * 10 ** 18
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset18.totalSupply(),
+            shares * 10 ** 18
+        );
+        vm.stopPrank();
+    }
+
+    function testEmptyVaultwithdraw() public {
+        address holder = makeAddr("holder");
+        address receiver = makeAddr("receiver");
+        vm.startPrank(holder);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.maxWithdraw(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.previewWithdraw(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.maxWithdraw(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.previewWithdraw(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.maxWithdraw(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.previewWithdraw(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.maxWithdraw(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.previewWithdraw(0), 0);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset0Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset0.withdraw(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset6Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset6.withdraw(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset12Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset12.withdraw(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset18Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset18.withdraw(0, receiver, holder);
+
+        assertEq(underlying.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalSupply(), 0);
+        vm.stopPrank();
+    }
+
+    function testEmptyVaultRedeem() public {
+        address holder = makeAddr("holder");
+        address receiver = makeAddr("receiver");
+        vm.startPrank(holder);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.maxRedeem(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.previewRedeem(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.maxRedeem(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.previewRedeem(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.maxRedeem(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.previewRedeem(0), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.maxRedeem(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.previewRedeem(0), 0);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset0Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset0.redeem(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset6Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset6Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset6.redeem(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset12Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset12Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset12.redeem(0, receiver, holder);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Transfer(holder, zeroAddress, 0);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset18Addr, receiver, 0);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset18Addr
+        );
+        emit Withdraw(holder, receiver, holder, 0, 0);
+        ERC4626ExtendedDecimalsOffset18.redeem(0, receiver, holder);
+
+        assertEq(underlying.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset6.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset12.totalSupply(), 0);
+
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(holder), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.balanceOf(receiver), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset18.totalSupply(), 0);
+        vm.stopPrank();
+    }
+
+    /**
+     * @dev For the remaining tests, we only use the test contract `ERC4626ExtendedDecimalsOffset0`,
+     * as we have correctly proven the decimals offset behaviour above.
+     * @notice Forked and adjusted accordingly from here:
+     * https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol.
+     */
+    function testSingleDepositWithdraw() public {
+        address alice = makeAddr("alice");
+        uint256 assets = 100;
+        vm.startPrank(alice);
+        underlying.mint(alice, assets);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, assets);
+        assertEq(
+            underlying.allowance(alice, ERC4626ExtendedDecimalsOffset0Addr),
+            assets
+        );
+        uint256 alicePreDepositBalance = underlying.balanceOf(alice);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(alice, ERC4626ExtendedDecimalsOffset0Addr, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(zeroAddress, alice, assets);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Deposit(alice, alice, assets, assets);
+        uint256 aliceShareAmount = ERC4626ExtendedDecimalsOffset0.deposit(
+            assets,
+            alice
+        );
+
+        assertEq(assets, aliceShareAmount);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.previewWithdraw(aliceShareAmount),
+            assets
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.previewDeposit(assets),
+            aliceShareAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalSupply(),
+            aliceShareAmount
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), assets);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.balanceOf(alice),
+            aliceShareAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            assets
+        );
+        assertEq(underlying.balanceOf(alice), alicePreDepositBalance - assets);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(alice, zeroAddress, assets);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset0Addr, alice, assets);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Withdraw(alice, alice, alice, assets, assets);
+        ERC4626ExtendedDecimalsOffset0.withdraw(assets, alice, alice);
+
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            0
+        );
+        assertEq(underlying.balanceOf(alice), alicePreDepositBalance);
+        vm.stopPrank();
+    }
+
+    /**
+     * @notice Forked and adjusted accordingly from here:
+     * https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol.
+     */
+    function testSingleMintRedeem() public {
+        address alice = makeAddr("alice");
+        uint256 shares = 100;
+        vm.startPrank(alice);
+        underlying.mint(alice, shares);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, shares);
+        assertEq(
+            underlying.allowance(alice, ERC4626ExtendedDecimalsOffset0Addr),
+            shares
+        );
+        uint256 alicePreDepositBalance = underlying.balanceOf(alice);
+
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(alice, ERC4626ExtendedDecimalsOffset0Addr, shares);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(zeroAddress, alice, shares);
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Deposit(alice, alice, shares, shares);
+        uint256 aliceAssetAmount = ERC4626ExtendedDecimalsOffset0.mint(
+            shares,
+            alice
+        );
+
+        assertEq(aliceAssetAmount, shares);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.previewWithdraw(aliceAssetAmount),
+            shares
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.previewDeposit(shares),
+            aliceAssetAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalSupply(),
+            aliceAssetAmount
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), shares);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.balanceOf(alice),
+            aliceAssetAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            shares
+        );
+        assertEq(underlying.balanceOf(alice), alicePreDepositBalance - shares);
+
+        vm.expectEmit(
+            true,
+            true,
+            false,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Transfer(alice, zeroAddress, shares);
+        vm.expectEmit(true, true, false, true, underlyingAddr);
+        emit Transfer(ERC4626ExtendedDecimalsOffset0Addr, alice, shares);
+        vm.expectEmit(
+            true,
+            true,
+            true,
+            true,
+            ERC4626ExtendedDecimalsOffset0Addr
+        );
+        emit Withdraw(alice, alice, alice, shares, shares);
+        ERC4626ExtendedDecimalsOffset0.redeem(shares, alice, alice);
+
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            0
+        );
+        assertEq(underlying.balanceOf(alice), alicePreDepositBalance);
+        vm.stopPrank();
+    }
+
+    /**
+     * @notice Forked and adjusted accordingly from here:
+     * https://github.com/transmissions11/solmate/blob/main/src/test/ERC4626.t.sol.
+     */
+    function testMultipleMintDepositRedeemWithdraw() public {
+        address alice = makeAddr("alice");
+        address bob = makeAddr("bob");
+        uint256 initialAmountAlice = 4000;
+        uint256 initialAmountBob = 7001;
+        uint256 mutationUnderlyingAmount = 3000;
+        vm.startPrank(alice);
+        underlying.mint(alice, initialAmountAlice);
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            initialAmountAlice
+        );
+        assertEq(
+            underlying.allowance(alice, ERC4626ExtendedDecimalsOffset0Addr),
+            initialAmountAlice
+        );
+        vm.stopPrank();
+
+        vm.startPrank(bob);
+        underlying.mint(bob, initialAmountBob);
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            initialAmountBob
+        );
+        assertEq(
+            underlying.allowance(bob, ERC4626ExtendedDecimalsOffset0Addr),
+            initialAmountBob
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 1. Alice mints 2000 shares (costs 2000 tokens).
+         */
+        vm.startPrank(alice);
+        uint256 aliceUnderlyingAmount = ERC4626ExtendedDecimalsOffset0.mint(
+            2000,
+            alice
+        );
+        uint256 aliceShareAmount = ERC4626ExtendedDecimalsOffset0
+            .previewDeposit(aliceUnderlyingAmount);
+        assertEq(aliceShareAmount, 2000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.balanceOf(alice),
+            aliceShareAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            aliceUnderlyingAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToShares(
+                aliceUnderlyingAmount
+            ),
+            ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+        );
+        assertEq(aliceUnderlyingAmount, 2000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalSupply(),
+            aliceShareAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalAssets(),
+            aliceUnderlyingAmount
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 2. Bob deposits 4000 tokens (mints 4000 shares).
+         */
+        vm.startPrank(bob);
+        uint256 bobShareAmount = ERC4626ExtendedDecimalsOffset0.deposit(
+            4000,
+            bob
+        );
+        uint256 bobUnderlyingAmount = ERC4626ExtendedDecimalsOffset0
+            .previewWithdraw(bobShareAmount);
+        assertEq(bobUnderlyingAmount, 4000);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), bobShareAmount);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            bobUnderlyingAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToShares(bobUnderlyingAmount),
+            ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+        );
+        assertEq(bobShareAmount, bobUnderlyingAmount);
+        uint256 preMutationShareBal = aliceShareAmount + bobShareAmount;
+        uint256 preMutationBal = aliceUnderlyingAmount + bobUnderlyingAmount;
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalSupply(),
+            preMutationShareBal
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), preMutationBal);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 6000);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 6000);
+        vm.stopPrank();
+
+        /**
+         * @dev 3. Vault mutates by +3000 tokens (simulated yield returned from strategy).
+         */
+        underlying.mint(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            mutationUnderlyingAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalSupply(),
+            preMutationShareBal
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.totalAssets(),
+            preMutationBal + mutationUnderlyingAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.balanceOf(alice),
+            aliceShareAmount
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            aliceUnderlyingAmount + (mutationUnderlyingAmount / 3) * 1 - 1
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), bobShareAmount);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            bobUnderlyingAmount + (mutationUnderlyingAmount / 3) * 2 - 1
+        );
+
+        /**
+         * @dev 4. Alice deposits 2000 tokens (mints 1333 shares).
+         */
+        vm.startPrank(alice);
+        ERC4626ExtendedDecimalsOffset0.deposit(2000, alice);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 7333);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 3333);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            4999
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 4000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            6000
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 5. Bob mints 2000 shares (costs 3000 assets).
+         * @notice Bob's assets spent got rounded up and Alices's
+         * vault assets got rounded up.
+         */
+        vm.startPrank(bob);
+        ERC4626ExtendedDecimalsOffset0.mint(2000, bob);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 9333);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 3333);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            4999
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 6000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            9000
+        );
+        assertEq(underlying.balanceOf(alice), 0);
+        assertEq(underlying.balanceOf(bob), 1);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 14000);
+        vm.stopPrank();
+
+        /**
+         * @dev 6. Vault mutates by +3000 tokens.
+         */
+        underlying.mint(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            mutationUnderlyingAmount
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 17000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            6070
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            10928
+        );
+
+        /**
+         * @dev 7. Alice redeems 1333 shares (2427 assets).
+         */
+        vm.startPrank(alice);
+        ERC4626ExtendedDecimalsOffset0.redeem(1333, alice, alice);
+        assertEq(underlying.balanceOf(alice), 2427);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 8000);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 14573);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 2000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            3643
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 6000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            10929
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 8. Bob withdraws 2929 assets (1608 shares).
+         */
+        vm.startPrank(bob);
+        ERC4626ExtendedDecimalsOffset0.withdraw(2929, bob, bob);
+        assertEq(underlying.balanceOf(bob), 2930);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 6392);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 11644);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 2000);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            3643
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 4392);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            8000
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 9. Alice withdraws 3643 assets (2000 shares).
+         * @notice Bob's assets have been rounded back up.
+         */
+        vm.startPrank(alice);
+        ERC4626ExtendedDecimalsOffset0.withdraw(3643, alice, alice);
+        assertEq(underlying.balanceOf(alice), 6070);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 4392);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 8001);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            0
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 4392);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            8000
+        );
+        vm.stopPrank();
+
+        /**
+         * @dev 10. Bob redeems 4392 shares (8000 tokens).
+         */
+        vm.startPrank(bob);
+        ERC4626ExtendedDecimalsOffset0.redeem(4392, bob, bob);
+        assertEq(underlying.balanceOf(bob), 10930);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 1);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(alice)
+            ),
+            0
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(bob)
+            ),
+            0
+        );
+        assertEq(underlying.balanceOf(ERC4626ExtendedDecimalsOffset0Addr), 1);
+        vm.stopPrank();
+    }
+
+    function testDepositInsufficientAllowance() public {
+        underlying.mint(self, type(uint8).max);
+        underlying.approve(
+            ERC4626ExtendedDecimalsOffset0Addr,
+            type(uint8).max - 1
+        );
+        assertEq(
+            underlying.allowance(self, ERC4626ExtendedDecimalsOffset0Addr),
+            type(uint8).max - 1
+        );
+        vm.expectRevert(bytes("ERC20: insufficient allowance"));
+        ERC4626ExtendedDecimalsOffset0.deposit(type(uint8).max, self);
+    }
+
+    function testWithdrawInsufficientAssets() public {
+        underlying.mint(self, type(uint8).max);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, type(uint8).max);
+        ERC4626ExtendedDecimalsOffset0.deposit(type(uint8).max, self);
+        vm.expectRevert(bytes("ERC4626: withdraw more than maximum"));
+        ERC4626ExtendedDecimalsOffset0.withdraw(
+            uint256(type(uint8).max) + 1,
+            self,
+            self
+        );
+    }
+
+    function testWithdrawInsufficientAllowance() public {
+        underlying.mint(self, type(uint8).max);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, type(uint8).max);
+        ERC4626ExtendedDecimalsOffset0.deposit(type(uint8).max, self);
+        vm.expectRevert(bytes("ERC20: insufficient allowance"));
+        vm.prank(makeAddr("otherAccount"));
+        ERC4626ExtendedDecimalsOffset0.withdraw(type(uint8).max, self, self);
+    }
+
+    function testRedeemInsufficientShares() public {
+        underlying.mint(self, type(uint16).max);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, type(uint8).max);
+        ERC4626ExtendedDecimalsOffset0.deposit(type(uint8).max, self);
+        vm.expectRevert(bytes("ERC4626: redeem more than maximum"));
+        ERC4626ExtendedDecimalsOffset0.redeem(
+            uint256(type(uint8).max) + 1,
+            self,
+            self
+        );
+    }
+
+    function testWithdrawWithNoAssets() public {
+        vm.expectRevert(bytes("ERC4626: withdraw more than maximum"));
+        ERC4626ExtendedDecimalsOffset0.withdraw(type(uint8).max, self, self);
+    }
+
+    function testRedeemWithNoShares() public {
+        vm.expectRevert(bytes("ERC4626: redeem more than maximum"));
+        ERC4626ExtendedDecimalsOffset0.redeem(type(uint8).max, self, self);
+    }
+
+    function testDepositWithNoApproval() public {
+        vm.expectRevert(bytes("ERC20: insufficient allowance"));
+        ERC4626ExtendedDecimalsOffset0.deposit(type(uint8).max, self);
+    }
+
+    function testMintWithNoApproval() public {
+        vm.expectRevert(bytes("ERC20: insufficient allowance"));
+        ERC4626ExtendedDecimalsOffset0.mint(type(uint8).max, self);
+    }
+
+    function testDepositZero() public {
+        ERC4626ExtendedDecimalsOffset0.deposit(0, self);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(self), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(self)
+            ),
+            0
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+    }
+
+    function testMintZero() public {
+        ERC4626ExtendedDecimalsOffset0.mint(0, self);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(self), 0);
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.convertToAssets(
+                ERC4626ExtendedDecimalsOffset0.balanceOf(self)
+            ),
+            0
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalSupply(), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.totalAssets(), 0);
+    }
+
+    function testVaultInteractionsForSomeoneElse() public {
+        address alice = makeAddr("alice");
+        address bob = makeAddr("bob");
+        uint256 amount = 1000;
+        underlying.mint(alice, amount);
+        underlying.mint(bob, amount);
+
+        vm.prank(alice);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, amount);
+        vm.prank(bob);
+        underlying.approve(ERC4626ExtendedDecimalsOffset0Addr, amount);
+
+        vm.startPrank(alice);
+        ERC4626ExtendedDecimalsOffset0.deposit(amount, bob);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), amount);
+        assertEq(underlying.balanceOf(alice), 0);
+        vm.stopPrank();
+
+        vm.startPrank(bob);
+        ERC4626ExtendedDecimalsOffset0.mint(amount, alice);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), amount);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), amount);
+        assertEq(underlying.balanceOf(bob), 0);
+        vm.stopPrank();
+
+        vm.startPrank(alice);
+        ERC4626ExtendedDecimalsOffset0.redeem(amount, bob, alice);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(alice), 0);
+        assertEq(ERC4626ExtendedDecimalsOffset0.balanceOf(bob), amount);
+        assertEq(underlying.balanceOf(bob), amount);
+        vm.stopPrank();
+
+        vm.startPrank(bob);
+        vm.expectRevert(bytes("ERC4626: withdraw more than maximum"));
+        ERC4626ExtendedDecimalsOffset0.withdraw(amount, alice, bob);
+        vm.stopPrank();
+    }
+
+    function testPermitSuccess() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + 100000;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectEmit(true, true, false, true);
+        emit Approval(owner, spender, amount);
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.allowance(owner, spender),
+            amount
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.nonces(owner), 1);
+    }
+
+    function testPermitReplaySignature() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + 100000;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectEmit(true, true, false, true);
+        emit Approval(owner, spender, amount);
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+        vm.expectRevert(bytes("ERC20Permit: invalid signature"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testPermitOtherSignature() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + 100000;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key + 1,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectRevert(bytes("ERC20Permit: invalid signature"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testPermitBadChainId() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + 100000;
+        bytes32 domainSeparator = keccak256(
+            abi.encode(
+                _TYPE_HASH,
+                keccak256(bytes(_NAME_EIP712)),
+                keccak256(bytes(_VERSION_EIP712)),
+                block.chainid + 1,
+                ERC4626ExtendedDecimalsOffset0Addr
+            )
+        );
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectRevert(bytes("ERC20Permit: invalid signature"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testPermitBadNonce() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = 1;
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + 100000;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectRevert(bytes("ERC20Permit: invalid signature"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testPermitExpiredDeadline() public {
+        (address owner, uint256 key) = makeAddrAndKey("owner");
+        address spender = makeAddr("spender");
+        uint256 amount = 100;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(owner);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp - 1;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            owner,
+                            spender,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectRevert(bytes("ERC20Permit: expired deadline"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            owner,
+            spender,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testCachedDomainSeparator() public {
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.DOMAIN_SEPARATOR(),
+            _CACHED_DOMAIN_SEPARATOR
+        );
+    }
+
+    function testDomainSeparator() public {
+        vm.chainId(block.chainid + 1);
+        bytes32 digest = keccak256(
+            abi.encode(
+                _TYPE_HASH,
+                keccak256(bytes(_NAME_EIP712)),
+                keccak256(bytes(_VERSION_EIP712)),
+                block.chainid,
+                ERC4626ExtendedDecimalsOffset0Addr
+            )
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.DOMAIN_SEPARATOR(), digest);
+    }
+
+    function testFuzzPermitSuccess(
+        string calldata owner,
+        string calldata spender,
+        uint16 increment
+    ) public {
+        (address ownerAddr, uint256 key) = makeAddrAndKey(owner);
+        address spenderAddr = makeAddr(spender);
+        uint256 amount = block.number;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(ownerAddr);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + increment;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            key,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            ownerAddr,
+                            spenderAddr,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectEmit(true, true, false, true);
+        emit Approval(ownerAddr, spenderAddr, amount);
+        ERC4626ExtendedDecimalsOffset0.permit(
+            ownerAddr,
+            spenderAddr,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+        assertEq(
+            ERC4626ExtendedDecimalsOffset0.allowance(ownerAddr, spenderAddr),
+            amount
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.nonces(ownerAddr), 1);
+    }
+
+    function testFuzzPermitInvalid(
+        string calldata owner,
+        string calldata spender,
+        uint16 increment
+    ) public {
+        vm.assume(
+            keccak256(abi.encode(owner)) != keccak256(abi.encode("ownerWrong"))
+        );
+        (address ownerAddr, ) = makeAddrAndKey(owner);
+        (, uint256 keyWrong) = makeAddrAndKey("ownerWrong");
+        address spenderAddr = makeAddr(spender);
+        uint256 amount = block.number;
+        uint256 nonce = ERC4626ExtendedDecimalsOffset0.nonces(ownerAddr);
+        // solhint-disable-next-line not-rely-on-time
+        uint256 deadline = block.timestamp + increment;
+        bytes32 domainSeparator = ERC4626ExtendedDecimalsOffset0
+            .DOMAIN_SEPARATOR();
+        (uint8 v, bytes32 r, bytes32 s) = vm.sign(
+            keyWrong,
+            keccak256(
+                abi.encodePacked(
+                    "\x19\x01",
+                    domainSeparator,
+                    keccak256(
+                        abi.encode(
+                            _PERMIT_TYPE_HASH,
+                            ownerAddr,
+                            spenderAddr,
+                            amount,
+                            nonce,
+                            deadline
+                        )
+                    )
+                )
+            )
+        );
+        vm.expectRevert(bytes("ERC20Permit: invalid signature"));
+        ERC4626ExtendedDecimalsOffset0.permit(
+            ownerAddr,
+            spenderAddr,
+            amount,
+            deadline,
+            v,
+            r,
+            s
+        );
+    }
+
+    function testFuzzDomainSeparator(uint8 increment) public {
+        vm.chainId(block.chainid + increment);
+        bytes32 digest = keccak256(
+            abi.encode(
+                _TYPE_HASH,
+                keccak256(bytes(_NAME_EIP712)),
+                keccak256(bytes(_VERSION_EIP712)),
+                block.chainid,
+                ERC4626ExtendedDecimalsOffset0Addr
+            )
+        );
+        assertEq(ERC4626ExtendedDecimalsOffset0.DOMAIN_SEPARATOR(), digest);
+    }
+}
+
+contract ERC4626VaultInvariants is Test {
+    string private constant _NAME = "TokenisedVaultMock";
+    string private constant _NAME_UNDERLYING = "UnderlyingTokenMock";
+    string private constant _SYMBOL = "TVM";
+    string private constant _SYMBOL_UNDERLYING = "UTM";
+    string private constant _NAME_EIP712 = "TokenisedVaultMock";
+    string private constant _VERSION_EIP712 = "1";
+    uint8 private constant _DECIMALS_OFFSET = 9;
+    uint256 private constant _INITIAL_SUPPLY_UNDERLYING = type(uint8).max;
+
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+    address private deployer = address(vyperDeployer);
+    ERC20Mock private underlying =
+        new ERC20Mock(
+            _NAME_UNDERLYING,
+            _SYMBOL_UNDERLYING,
+            deployer,
+            _INITIAL_SUPPLY_UNDERLYING
+        );
+
+    // solhint-disable-next-line var-name-mixedcase
+    IERC4626Extended private ERC4626Extended;
+    ERC4626VaultHandler private erc4626VaultHandler;
+
+    function setUp() public {
+        bytes memory args = abi.encode(
+            _NAME,
+            _SYMBOL,
+            underlying,
+            _DECIMALS_OFFSET,
+            _NAME_EIP712,
+            _VERSION_EIP712
+        );
+        ERC4626Extended = IERC4626Extended(
+            vyperDeployer.deployContract("src/extensions/", "ERC4626", args)
+        );
+        erc4626VaultHandler = new ERC4626VaultHandler(ERC4626Extended);
+        targetContract(address(erc4626VaultHandler));
+        targetSender(deployer);
+    }
+
+    function invariantTotalSupply() public {
+        assertEq(
+            ERC4626Extended.totalSupply(),
+            erc4626VaultHandler.totalSupply()
+        );
+    }
+
+    function invariantTotalAssets() public {
+        assertEq(
+            ERC4626Extended.totalAssets(),
+            erc4626VaultHandler.totalAssets()
+        );
+    }
+}
+
+contract ERC4626VaultHandler {
+    uint256 public totalSupply;
+    uint256 public totalAssets;
+
+    IERC4626Extended private vault;
+
+    constructor(IERC4626Extended vault_) {
+        vault = vault_;
+    }
+
+    function transfer(address to, uint256 amount) public {
+        vault.transfer(to, amount);
+    }
+
+    function approve(address spender, uint256 amount) public {
+        vault.approve(spender, amount);
+    }
+
+    function transferFrom(address owner, address to, uint256 amount) public {
+        vault.transferFrom(owner, to, amount);
+    }
+
+    function permit(
+        address owner,
+        address spender,
+        uint256 value,
+        uint256 deadline,
+        uint8 v,
+        bytes32 r,
+        bytes32 s
+    ) public {
+        vault.permit(owner, spender, value, deadline, v, r, s);
+    }
+
+    function increase_allowance(address spender, uint256 addedAmount) public {
+        vault.increase_allowance(spender, addedAmount);
+    }
+
+    function decrease_allowance(
+        address spender,
+        uint256 subtractedAmount
+    ) public {
+        vault.decrease_allowance(spender, subtractedAmount);
+    }
+
+    function deposit(uint256 assets, address receiver) public {
+        uint256 shares = vault.deposit(assets, receiver);
+        totalSupply += shares;
+        totalAssets += assets;
+    }
+
+    function mint(uint256 shares, address receiver) public {
+        uint256 assets = vault.mint(shares, receiver);
+        totalSupply += shares;
+        totalAssets += assets;
+    }
+
+    function withdraw(uint256 assets, address receiver, address owner) public {
+        uint256 shares = vault.withdraw(assets, receiver, owner);
+        totalSupply -= shares;
+        totalAssets -= assets;
+    }
+
+    function redeem(uint256 shares, address receiver, address owner) public {
+        uint256 assets = vault.redeem(shares, receiver, owner);
+        totalSupply -= shares;
+        totalAssets -= assets;
+    }
+}
diff --git a/test/extensions/interfaces/IERC4626Extended.sol b/test/extensions/interfaces/IERC4626Extended.sol
new file mode 100644
index 00000000..2411aebf
--- /dev/null
+++ b/test/extensions/interfaces/IERC4626Extended.sol
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.19;
+
+import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
+import {IERC20Permit} from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";
+import {IERC4626} from "openzeppelin/interfaces/IERC4626.sol";
+
+interface IERC4626Extended is IERC20Metadata, IERC20Permit, IERC4626 {
+    function increase_allowance(
+        address spender,
+        uint256 addedAmount
+    ) external returns (bool);
+
+    function decrease_allowance(
+        address spender,
+        uint256 subtractedAmount
+    ) external returns (bool);
+}
diff --git a/test/extensions/mocks/ERC20ExcessDecimalsMock.sol b/test/extensions/mocks/ERC20ExcessDecimalsMock.sol
new file mode 100644
index 00000000..63dbc9ef
--- /dev/null
+++ b/test/extensions/mocks/ERC20ExcessDecimalsMock.sol
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+
+/**
+ * @title ERC20ExcessDecimalsMock
+ * @author pcaversaccio
+ * @dev Allows to mock a `decimals` return value greater than the maximum value
+ * of the type `uint8`.
+ */
+contract ERC20ExcessDecimalsMock {
+    /**
+     * @dev Mocks a `decimals` return value greater than the maximum value of the
+     * type `uint8`.
+     * @return uint256 The number of `decimals` used to get its user representation.
+     */
+    function decimals() public pure returns (uint256) {
+        return type(uint256).max;
+    }
+}
diff --git a/test/tokens/ERC1155.t.sol b/test/tokens/ERC1155.t.sol
index 4c28966a..5c46c914 100644
--- a/test/tokens/ERC1155.t.sol
+++ b/test/tokens/ERC1155.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/tokens/ERC20.t.sol b/test/tokens/ERC20.t.sol
index f7eb38d7..11f75a80 100644
--- a/test/tokens/ERC20.t.sol
+++ b/test/tokens/ERC20.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/tokens/ERC721.t.sol b/test/tokens/ERC721.t.sol
index c67151de..6287da72 100644
--- a/test/tokens/ERC721.t.sol
+++ b/test/tokens/ERC721.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {stdError} from "forge-std/StdError.sol";
diff --git a/test/tokens/interfaces/IERC1155Extended.sol b/test/tokens/interfaces/IERC1155Extended.sol
index 80b50844..215bf350 100644
--- a/test/tokens/interfaces/IERC1155Extended.sol
+++ b/test/tokens/interfaces/IERC1155Extended.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC1155MetadataURI} from "openzeppelin/token/ERC1155/extensions/IERC1155MetadataURI.sol";
 
diff --git a/test/tokens/interfaces/IERC20Extended.sol b/test/tokens/interfaces/IERC20Extended.sol
index e38dc0b7..01239db8 100644
--- a/test/tokens/interfaces/IERC20Extended.sol
+++ b/test/tokens/interfaces/IERC20Extended.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC20Metadata} from "openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
 import {IERC20Permit} from "openzeppelin/token/ERC20/extensions/IERC20Permit.sol";
diff --git a/test/tokens/interfaces/IERC4494.sol b/test/tokens/interfaces/IERC4494.sol
index 66155e13..e80d8ab0 100644
--- a/test/tokens/interfaces/IERC4494.sol
+++ b/test/tokens/interfaces/IERC4494.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC165} from "openzeppelin/utils/introspection/IERC165.sol";
 
diff --git a/test/tokens/interfaces/IERC721Extended.sol b/test/tokens/interfaces/IERC721Extended.sol
index 653f5258..917c12db 100644
--- a/test/tokens/interfaces/IERC721Extended.sol
+++ b/test/tokens/interfaces/IERC721Extended.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC721Metadata} from "openzeppelin/token/ERC721/extensions/IERC721Metadata.sol";
 import {IERC721Enumerable} from "openzeppelin/token/ERC721/extensions/IERC721Enumerable.sol";
diff --git a/test/tokens/mocks/ERC1155ReceiverMock.sol b/test/tokens/mocks/ERC1155ReceiverMock.sol
index b7e20f03..099c5d86 100644
--- a/test/tokens/mocks/ERC1155ReceiverMock.sol
+++ b/test/tokens/mocks/ERC1155ReceiverMock.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {ERC165} from "openzeppelin/utils/introspection/ERC165.sol";
 import {IERC1155Receiver} from "openzeppelin/token/ERC1155/IERC1155Receiver.sol";
diff --git a/test/tokens/mocks/ERC721ReceiverMock.sol b/test/tokens/mocks/ERC721ReceiverMock.sol
index e9ff75cb..94cb1733 100644
--- a/test/tokens/mocks/ERC721ReceiverMock.sol
+++ b/test/tokens/mocks/ERC721ReceiverMock.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC721Receiver} from "openzeppelin/token/ERC721/IERC721Receiver.sol";
 
diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol
index d6296f55..2e5ee489 100644
--- a/test/utils/Base64.t.sol
+++ b/test/utils/Base64.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {PRBTest} from "prb/test/PRBTest.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/BatchDistributor.t.sol b/test/utils/BatchDistributor.t.sol
index cff14039..fb541341 100644
--- a/test/utils/BatchDistributor.t.sol
+++ b/test/utils/BatchDistributor.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/Create2Address.t.sol b/test/utils/Create2Address.t.sol
index dcd8fdce..20fc76b6 100644
--- a/test/utils/Create2Address.t.sol
+++ b/test/utils/Create2Address.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/CreateAddress.t.sol b/test/utils/CreateAddress.t.sol
index 19297372..1fcf56eb 100644
--- a/test/utils/CreateAddress.t.sol
+++ b/test/utils/CreateAddress.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/ECDSA.t.sol b/test/utils/ECDSA.t.sol
index d7ddaa7e..d9e04e1e 100644
--- a/test/utils/ECDSA.t.sol
+++ b/test/utils/ECDSA.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/EIP712DomainSeparator.t.sol b/test/utils/EIP712DomainSeparator.t.sol
index b63c8071..a5a29689 100644
--- a/test/utils/EIP712DomainSeparator.t.sol
+++ b/test/utils/EIP712DomainSeparator.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/Math.t.sol b/test/utils/Math.t.sol
new file mode 100644
index 00000000..7b08b9ec
--- /dev/null
+++ b/test/utils/Math.t.sol
@@ -0,0 +1,214 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.19;
+
+import {Test} from "forge-std/Test.sol";
+import {VyperDeployer} from "utils/VyperDeployer.sol";
+
+import {IMath} from "./interfaces/IMath.sol";
+
+contract MathTest is Test {
+    VyperDeployer private vyperDeployer = new VyperDeployer();
+
+    IMath private math;
+
+    /**
+     * @dev An `internal` helper function that uses inline assembly to
+     * perform a `mulmod` operation.
+     * @param x The 32-byte multiplicand.
+     * @param y The 32-byte multiplier.
+     * @param denominator The 32-byte divisor.
+     * @return result The 32-byte result of the `mulmod` operation.
+     */
+    function mulMod(
+        uint256 x,
+        uint256 y,
+        uint256 denominator
+    ) internal pure returns (uint256 result) {
+        // solhint-disable-next-line no-inline-assembly
+        assembly {
+            result := mulmod(x, y, denominator)
+        }
+    }
+
+    /**
+     * @dev An `internal` helper function to calculate the full precision
+     * for "x * y".
+     * @param x The 32-byte multiplicand.
+     * @param y The 32-byte multiplier.
+     * @return high The most significant 32 bytes of the product.
+     * @return low The least significant 32 bytes of the product.
+     */
+    function mulHighLow(
+        uint256 x,
+        uint256 y
+    ) internal pure returns (uint256 high, uint256 low) {
+        (uint256 x0, uint256 x1) = (x & type(uint128).max, x >> 128);
+        (uint256 y0, uint256 y1) = (y & type(uint128).max, y >> 128);
+
+        /**
+         * @dev Karatsuba algorithm: https://en.wikipedia.org/wiki/Karatsuba_algorithm.
+         */
+        uint256 z2 = x1 * y1;
+        uint256 z1a = x1 * y0;
+        uint256 z1b = x0 * y1;
+        uint256 z0 = x0 * y0;
+
+        uint256 carry = ((z1a & type(uint128).max) +
+            (z1b & type(uint128).max) +
+            (z0 >> 128)) >> 128;
+
+        high = z2 + (z1a >> 128) + (z1b >> 128) + carry;
+
+        unchecked {
+            low = x * y;
+        }
+    }
+
+    /**
+     * @dev An `internal` helper function for internal remainder calculation
+     * and carry addition.
+     * @param x The least significant 32 bytes.
+     * @param y The 32-byte result of a `mulmod` operation.
+     * @return remainder The 32-byte remainder.
+     * @return carry The 32-byte carry.
+     */
+    function addCarry(
+        uint256 x,
+        uint256 y
+    ) internal pure returns (uint256 remainder, uint256 carry) {
+        unchecked {
+            remainder = x + y;
+        }
+        carry = remainder < x ? 1 : 0;
+    }
+
+    function setUp() public {
+        math = IMath(vyperDeployer.deployContract("src/utils/", "Math"));
+    }
+
+    function testMulDivDivisionByZero() public {
+        vm.expectRevert(bytes("Math: mul_div division by zero"));
+        math.mul_div(1, 1, 0, false);
+        vm.expectRevert(bytes("Math: mul_div division by zero"));
+        math.mul_div(1, 1, 0, true);
+    }
+
+    function testMulDivOverflow() public {
+        vm.expectRevert(bytes("Math: mul_div overflow"));
+        math.mul_div(type(uint256).max, type(uint256).max, 1, false);
+        vm.expectRevert(bytes("Math: mul_div overflow"));
+        math.mul_div(type(uint256).max, type(uint256).max, 1, true);
+    }
+
+    function testMulDivRoundDownSmallValues() public {
+        assertEq(math.mul_div(3, 4, 5, false), 2);
+        assertEq(math.mul_div(5, 7, 6, false), 5);
+        assertEq(math.mul_div(7, 9, 8, false), 7);
+    }
+
+    function testMulDivRoundDownLargeValues() public {
+        uint256 maxUint256 = type(uint256).max;
+        uint256 maxUint256Sub1 = maxUint256 - 1;
+        uint256 maxUint256Sub2 = maxUint256 - 2;
+        assertEq(math.mul_div(42, maxUint256Sub1, maxUint256, false), 41);
+        assertEq(math.mul_div(23, maxUint256, maxUint256, false), 23);
+        assertEq(
+            math.mul_div(maxUint256Sub1, maxUint256Sub1, maxUint256, false),
+            maxUint256Sub2
+        );
+        assertEq(
+            math.mul_div(maxUint256, maxUint256Sub1, maxUint256, false),
+            maxUint256Sub1
+        );
+        assertEq(
+            math.mul_div(maxUint256, maxUint256, maxUint256, false),
+            maxUint256
+        );
+    }
+
+    function testMulDivRoundUpSmallValues() public {
+        assertEq(math.mul_div(3, 4, 5, true), 3);
+        assertEq(math.mul_div(5, 7, 6, true), 6);
+        assertEq(math.mul_div(7, 9, 8, true), 8);
+    }
+
+    function testMulDivRoundUpLargeValues() public {
+        uint256 maxUint256 = type(uint256).max;
+        uint256 maxUint256Sub1 = maxUint256 - 1;
+        assertEq(math.mul_div(42, maxUint256Sub1, maxUint256, true), 42);
+        assertEq(math.mul_div(23, maxUint256, maxUint256, true), 23);
+        assertEq(
+            math.mul_div(maxUint256Sub1, maxUint256Sub1, maxUint256, true),
+            maxUint256Sub1
+        );
+        assertEq(
+            math.mul_div(maxUint256, maxUint256Sub1, maxUint256, true),
+            maxUint256Sub1
+        );
+        assertEq(
+            math.mul_div(maxUint256, maxUint256, maxUint256, true),
+            maxUint256
+        );
+    }
+
+    /**
+     * @notice Forked and adjusted accordingly from here:
+     * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/utils/math/Math.t.sol.
+     */
+    function testFuzzMulDiv(uint256 x, uint256 y, uint256 d) public {
+        /**
+         * @dev Full precision for "x * y".
+         */
+        (uint256 xyHi, uint256 xyLo) = mulHighLow(x, y);
+
+        /**
+         * @dev Assume the result will not overflow (see `testFuzzMulDivDomain`).
+         * This also checks that `d` is positive.
+         */
+        vm.assume(xyHi < d);
+
+        uint256 qDown = math.mul_div(x, y, d, false);
+
+        /**
+         * @dev Full precision for "q * d".
+         */
+        (uint256 qdHi, uint256 qdLo) = mulHighLow(qDown, d);
+        /**
+         * @dev Add remainder of "(x * y) / d", computed as "remainder = ((x * y) % d)".
+         */
+        (uint256 qdRemLo, uint256 c) = addCarry(qdLo, mulMod(x, y, d));
+        uint256 qdRemHi = qdHi + c;
+
+        /**
+         * @dev Full precision check that "x * y = q * d + remainder" holds.
+         */
+        assertEq(xyHi, qdRemHi);
+        assertEq(xyLo, qdRemLo);
+
+        /**
+         * @dev Full precision check in case of "ceil((x * y) / denominator)".
+         */
+        vm.assume(mulmod(x, y, d) > 0 && qDown < type(uint256).max);
+        assertEq(math.mul_div(x, y, d, true), qDown + 1);
+    }
+
+    /**
+     * @notice Forked and adjusted accordingly from here:
+     * https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/test/utils/math/Math.t.sol.
+     */
+    function testFuzzMulDivDomain(uint256 x, uint256 y, uint256 d) public {
+        (uint256 xyHi, ) = mulHighLow(x, y);
+
+        /**
+         * @dev Violate `testFuzzMulDiv` assumption, i.e. `d` is 0 and result overflows.
+         */
+        vm.assume(xyHi >= d);
+
+        try math.mul_div(x, y, d, false) returns (uint256) {
+            fail();
+        } catch {}
+        try math.mul_div(x, y, d, true) returns (uint256) {
+            fail();
+        } catch {}
+    }
+}
diff --git a/test/utils/MerkleProofVerification.t.sol b/test/utils/MerkleProofVerification.t.sol
index 84439962..d45fc059 100644
--- a/test/utils/MerkleProofVerification.t.sol
+++ b/test/utils/MerkleProofVerification.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/Multicall.t.sol b/test/utils/Multicall.t.sol
index d15ae847..19059a38 100644
--- a/test/utils/Multicall.t.sol
+++ b/test/utils/Multicall.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/SignatureChecker.t.sol b/test/utils/SignatureChecker.t.sol
index fec3c3b3..cf4167f9 100644
--- a/test/utils/SignatureChecker.t.sol
+++ b/test/utils/SignatureChecker.t.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Test} from "forge-std/Test.sol";
 import {VyperDeployer} from "utils/VyperDeployer.sol";
diff --git a/test/utils/interfaces/IBase64.sol b/test/utils/interfaces/IBase64.sol
index 0443fec4..9b6aa24f 100644
--- a/test/utils/interfaces/IBase64.sol
+++ b/test/utils/interfaces/IBase64.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IBase64 {
     function encode(
diff --git a/test/utils/interfaces/IBatchDistributor.sol b/test/utils/interfaces/IBatchDistributor.sol
index 7f75aeaf..584820a6 100644
--- a/test/utils/interfaces/IBatchDistributor.sol
+++ b/test/utils/interfaces/IBatchDistributor.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol";
 
diff --git a/test/utils/interfaces/ICreate2Address.sol b/test/utils/interfaces/ICreate2Address.sol
index 04fe079b..b16ed32c 100644
--- a/test/utils/interfaces/ICreate2Address.sol
+++ b/test/utils/interfaces/ICreate2Address.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface ICreate2Address {
     function compute_address_self(
diff --git a/test/utils/interfaces/ICreateAddress.sol b/test/utils/interfaces/ICreateAddress.sol
index a4dbc9a0..d5ccee52 100644
--- a/test/utils/interfaces/ICreateAddress.sol
+++ b/test/utils/interfaces/ICreateAddress.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface ICreateAddress {
     function compute_address_rlp_self(
diff --git a/test/utils/interfaces/IECDSA.sol b/test/utils/interfaces/IECDSA.sol
index 641593a2..87e4b18a 100644
--- a/test/utils/interfaces/IECDSA.sol
+++ b/test/utils/interfaces/IECDSA.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IECDSA {
     function recover_sig(
diff --git a/test/utils/interfaces/IEIP712DomainSeparator.sol b/test/utils/interfaces/IEIP712DomainSeparator.sol
index 96887c69..e4ce3f98 100644
--- a/test/utils/interfaces/IEIP712DomainSeparator.sol
+++ b/test/utils/interfaces/IEIP712DomainSeparator.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IEIP712DomainSeparator {
     function domain_separator_v4() external view returns (bytes32);
diff --git a/test/utils/interfaces/IMath.sol b/test/utils/interfaces/IMath.sol
new file mode 100644
index 00000000..24c0f62d
--- /dev/null
+++ b/test/utils/interfaces/IMath.sol
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: WTFPL
+pragma solidity ^0.8.19;
+
+interface IMath {
+    function mul_div(
+        uint256 x,
+        uint256 y,
+        uint256 denominator,
+        bool roundup
+    ) external pure returns (uint256);
+}
diff --git a/test/utils/interfaces/IMerkleProofVerification.sol b/test/utils/interfaces/IMerkleProofVerification.sol
index 41f94444..ac2e9049 100644
--- a/test/utils/interfaces/IMerkleProofVerification.sol
+++ b/test/utils/interfaces/IMerkleProofVerification.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IMerkleProofVerification {
     function verify(
diff --git a/test/utils/interfaces/IMulticall.sol b/test/utils/interfaces/IMulticall.sol
index 26c9a97a..7c2e421a 100644
--- a/test/utils/interfaces/IMulticall.sol
+++ b/test/utils/interfaces/IMulticall.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface IMulticall {
     struct Batch {
diff --git a/test/utils/interfaces/ISignatureChecker.sol b/test/utils/interfaces/ISignatureChecker.sol
index fa886230..273b1b21 100644
--- a/test/utils/interfaces/ISignatureChecker.sol
+++ b/test/utils/interfaces/ISignatureChecker.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: WTFPL
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 interface ISignatureChecker {
     function is_valid_signature_now(
diff --git a/test/utils/mocks/Create2Impl.sol b/test/utils/mocks/Create2Impl.sol
index 824dfcc6..3e43c980 100644
--- a/test/utils/mocks/Create2Impl.sol
+++ b/test/utils/mocks/Create2Impl.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Create2} from "openzeppelin/utils/Create2.sol";
 import {ERC1820Implementer} from "openzeppelin/utils/introspection/ERC1820Implementer.sol";
diff --git a/test/utils/mocks/ERC1271MaliciousMock.sol b/test/utils/mocks/ERC1271MaliciousMock.sol
index a1381a2d..cf3cb366 100644
--- a/test/utils/mocks/ERC1271MaliciousMock.sol
+++ b/test/utils/mocks/ERC1271MaliciousMock.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {IERC1271} from "openzeppelin/interfaces/IERC1271.sol";
 
diff --git a/test/utils/mocks/ERC1271WalletMock.sol b/test/utils/mocks/ERC1271WalletMock.sol
index ff8aa206..e8574a9c 100644
--- a/test/utils/mocks/ERC1271WalletMock.sol
+++ b/test/utils/mocks/ERC1271WalletMock.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {Ownable} from "openzeppelin/access/Ownable.sol";
 import {IERC1271} from "openzeppelin/interfaces/IERC1271.sol";
diff --git a/test/utils/mocks/ERC20Mock.sol b/test/utils/mocks/ERC20Mock.sol
index 9e8b15d4..cfb1aa6d 100644
--- a/test/utils/mocks/ERC20Mock.sol
+++ b/test/utils/mocks/ERC20Mock.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol";
 
diff --git a/test/utils/mocks/EtherReceiver.sol b/test/utils/mocks/EtherReceiver.sol
index 5a96145f..485e38c5 100644
--- a/test/utils/mocks/EtherReceiver.sol
+++ b/test/utils/mocks/EtherReceiver.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 /**
  * @title EtherReceiver
diff --git a/test/utils/mocks/MockCallee.sol b/test/utils/mocks/MockCallee.sol
index bd57894a..ed3792c3 100644
--- a/test/utils/mocks/MockCallee.sol
+++ b/test/utils/mocks/MockCallee.sol
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: MIT
-pragma solidity ^0.8.18;
+pragma solidity ^0.8.19;
 
 /**
  * @dev Error that occurs when a method reverts.