KipuBankV3 is a multi-asset, non-custodial vault contract on Ethereum. It allows users to deposit and withdraw ETH and various ERC-20 tokens into a personal, gas-efficient balance sheet. The contract enforces a global, USD-denominated deposit cap and a per-transaction USD-denominated extraction limit.
This version also integrates with Uniswap V4 to provide a swapExactInputForUSDC function, allowing users to deposit any ERC-20 token and have it automatically converted and credited as USDC in their vault balance.
This contract version includes several key improvements over a V2 design:
-
Functional Swaps: The primary improvement was re-architecting the contract to support a swappable, non-constant
i_usdcAddress. The V2 design hadUSDC_ADDRESSset toaddress(0), which made the swap function untestable and non-functional. By makingi_usdcAddressanimmutablevariable set in the constructor, the contract can now correctly interact with a real USDC token, enabling theswapExactInputForUSDCfeature. -
Enhanced Testability: The
getBalancefunction was refactored fromgetBalance(token)togetBalance(user, token). On a public blockchain, all state is public, so this change does not introduce any security or privacy risk. It serves as a crucial "getter" function that simplifies on-chain data access for UIs, analytics, and—most importantly—our Foundry test suite. -
Robust Error Handling: The contract's internal logic was validated to ensure it reverts with the correct custom errors (e.g.,
KipuBank_CapReached,KipuBank_LimitExceeded). Tests were also written to confirm that external call failures (likeERC20InsufficientAllowance) are correctly handled and revert as expected.
The contract must be deployed using Foundry, providing all 6 immutable arguments.
-
Set Environment Variables:
export RPC_URL=<YOUR_RPC_URL> export PRIVATE_KEY=<YOUR_PRIVATE_KEY> export ETHERSCAN_API_KEY=<YOUR_ETHERSCAN_API_KEY>
-
Set Constructor Arguments:
_maxExtractUSD: Max extraction limit (in 6 decimals)._bankCapUSD: Total bank cap (in 6 decimals)._priceFeedAddress: Chainlink ETH/USD feed._routerAddress: Uniswap Universal Router address._permit2Address: Uniswap Permit2 address._usdcAddress: The official USDC token address.
-
Deploy Command:
forge create src/KipuBankV3.sol:KipuBankV3 \ --rpc-url $RPC_URL \ --private-key $PRIVATE_KEY \ --etherscan-api-key $ETHERSCAN_API_KEY \ --verify \ --constructor-args 10000e6 1000000e6 <_priceFeedAddress> <_routerAddress> <_permit2Address> <_usdcAddress>
You can interact with the deployed contract using cast.
Example: Deposit 0.1 ETH
cast send <BANK_ADDRESS> "depositETH()" --value 0.1ether --rpc-url $RPC_URL --private-key $PRIVATE_KEYExample: Deposit 100 WETH (ERC-20) First, approve the bank to spend your WETH:
cast send <WETH_ADDRESS> "approve(address,uint256)" <BANK_ADDRESS> 100e18 --rpc-url $RPC_URL --private-key $PRIVATE_KEYThen, call depositERC20:
cast send <BANK_ADDRESS> "depositERC20(address,uint256)" <WETH_ADDRESS> 100e18 --rpc-url $RPC_URL --private-key $PRIVATE_KEYExample: Extract 0.05 ETH
cast send <BANK_ADDRESS> "extractFromAccount(address,uint256)" 0x0000000000000000000000000000000000000000 0.05e18 --rpc-url $RPC_URL --private-key $PRIVATE_KEY-
_getUSDValueOracle Simplification:- Decision: The
_getUSDValuefunction perfectly values ETH using a Chainlink oracle. However, for all other ERC-20 tokens, it assumes a 1:1 USD peg and simply adjusts for decimals. - Trade-off: This was a major simplification to make the core deposit/extract logic testable without integrating a complex multi-token oracle system.
- Result: This makes the contract unsafe for production use with any non-stablecoin ERC-20 (e.g., WETH, WBTC), as it would incorrectly value them, breaking the cap and limit logic.
- Decision: The
-
getBalance(user, token):- Decision: The
getBalancefunction was made public and takes a_userargument. - Trade-off: This may seem like a privacy leak to Web2 developers. However, all storage on Ethereum is public, and
s_accountscould be read externally regardless. - Result: This design provides a convenient, gas-efficient "getter" for public data, which is standard practice and greatly simplifies UI development and testing.
- Decision: The
-
Hardcoded V4 Pool Key:
- Decision: The
getPoolKeyfunction hardcodes the fee tier (3000) and tick spacing (60) for swaps. - Trade-off: This simplifies the
swapExactInputForUSDCfunction, as the user doesn't need to provide this data. - Result: The swap function will fail if a liquid pool for
TokenIn -> USDCdoes not exist at the 0.3% fee tier.
- Decision: The
The single greatest weakness preventing this contract from being production-ready is the oracle logic in _getUSDValue.
-
Critical Weakness: The 1:1 USD peg assumption for all non-ETH ERC-20s is fundamentally insecure. A user could deposit 1,000 WETH (worth $3,000,000) and the contract would value it at $1,000, effectively bypassing the
i_bankCapUSDentirely. -
Missing Step: To reach maturity, this function must be refactored to:
- Use a robust, production-grade oracle (like Chainlink Price Feeds) for every token the contract will accept.
- Implement a whitelist of acceptable ERC-20 tokens that have a corresponding price feed.
- A
depositERC20call with a non-whitelisted token should be rejected.
-
Minor Weakness: The contract does not check for stale data from the Chainlink ETH/USD feed. While it checks for
price <= 0, a production contract should also checkupdatedAtto ensure the price is recent. -
Minor Weakness: The swap function relies on a hardcoded V4 pool key. This is inflexible and will fail for tokens that have liquidity at different fee tiers.
The contract is accompanied by a 17-test suite built with Foundry. All 17 tests are currently passing.
-
12 Revert Tests: These tests confirm that all security modifiers and internal checks function correctly. This includes:
KipuBank_CapReachedKipuBank_LimitExceededKipuBank_InsufficientBalanceKipuBank_ZeroValueKipuBank_UseDepositEthKipuBank_ZeroPriceFeed- External
ERC20InsufficientAllowancereverts
-
5 Success Tests: These tests confirm the "happy path" for every core function, including:
depositETHdepositERC20extractFromAccount(for ETH and ERC-20)swapExactInputForUSDC
Coverage includes all external functions, modifiers, and primary event emissions.
- Framework: Foundry
- Methodology: Unit Testing
- Mocks: All external dependencies were fully mocked for isolated, deterministic testing:
MockERC20(for USDC and TokenIn)MockAggregatorV3(for the Chainlink Price Feed)MockUniversalRouter(to simulate swap outputs)
- Validation: Tests were written to validate:
- Correct state changes (using
assertEq). - Correct revert-with-error (using
vm.expectRevert). - Correct event emission and order (using
vm.expectEmit).
- Correct state changes (using