TownSquare Vault Setup
TownSqVault — Deploy, Deposit, Withdraw
This repo contains TownSqVault, an upgradeable ERC-4626 vault that supplies deposits into TownSq via spokeTokenOperations.topup(...), and uses a 2-step withdrawal flow:
Step 1:
initiateWithdraw(shares, receiver, owner)initiates the withdrawal and burns shares.Step 2: After a timelock (currently 6 hours),
withdraw(owner)finalizes by transferring assets toreceiver(or mints shares back if transfer fails).
What gets deployed
TownSqVault is written as an upgradeable contract and is intended to be used behind a proxy.
Implementation:
src/TownSqVault.solProxy pattern:
TransparentUpgradeableProxyInitializer:
TownSqVault.initialize(...)(called once via proxy constructor or upgrade tooling)
Deployment (Foundry)
Prerequisites
Foundry installed (
forge,cast)RPC URL for your target network (the script is currently set up for chainId = 143)
A deployer private key loaded in your environment (examples use Foundry’s
--sender+ broadcast key)
Parameters you must supply
TownSqVault.initialize(...) requires:
Owner:
_owner(admin of vault settings like fees)TownSq addresses:
_spokeOperations_spokeTokenOperations_assetHubPool_loanManager
Asset:
_asset(ERC20 address; current script uses USDC)Pool Id:
_assetPoolId(vault collateral pool id in TownSq)Timelock:
initialTimelock(vault’s configurable timelock param; separate from withdrawal init timelock)Chain / loan identity:
chainIdvaultLoanTypeIdnonceloanName
ERC20 share token metadata:
_name_symbol
Messaging params:
_vaultMessageParams(Messages.MessageParams)
Example values (from script/TownSqVault.s.sol)
script/TownSqVault.s.sol)Your script currently uses (chain 143):
initialOwner:0xa031f11d7CDF039eeF0e73E47Bd5B487f3659B65USDC:0x754704Bc059F8C67012fEd69BC8A327a5aafb603spokeOperations:0x63CB1CF5aCCbCC57e0cCa047bE9673EA5022b8DBspokeTokenOperations:0xA457235B68606a7921b7c525D92e9592e793b4C0assetHubPool:0xdb4E67F878289A820046f46f6304fd6Ee1449281loanManager:0xC4C20EFbEfA4Bde14091a3040d112cF981d8B2DBvaultAssetPoolId:10vaultLoanTypeId:1nonce:0x00000001loanName:"USDC VAULT"name/symbol:"USDC VAULT"/"vUSDC"messageParams:{ adapterId: 1, returnAdapterId: 1, receiverValue: 0, gasLimit: 5_000_000, returnGasLimit: 5_000_000 }
Deploying a new proxy (recommended pattern)
The canonical flow is:
Deploy
TownSqVaultimplementation.Encode
initializerData = abi.encodeCall(TownSqVault.initialize, (...)).Deploy
TransparentUpgradeableProxy(impl, admin, initializerData).
You already do this in tests (test/TownSqVault.t.sol).
Running the script
There is a working Foundry invocation note at the bottom of script/TownSqVault.s.sol:
Notes:
--broadcastwrites transactions + receipts intobroadcast/....If you’re deploying a new vault proxy, ensure your script uses the proxy-constructor deployment pattern (implementation +
TransparentUpgradeableProxy) and not only an upgrade call.
Depositing
User flow (ERC-4626 deposit)
To deposit assets of the underlying token:
Approve the vault to spend your asset.
Call
deposit(assets, receiver)on the vault.
In Solidity terms:
User calls
TownSqVault.deposit(assets, receiver)Vault:
accrues interest (
_accrueInterest())calculates shares using
lastTotalAssetsand currenttotalSupply()transfers the ERC20
assetsinto the vault (ERC-4626_deposit)supplies to TownSq by calling
spokeTokenOperations.topup(...)updates
lastTotalAssets += assets
Example (cast)
Replace:
$VAULTwith the proxy address$ASSETwith the underlying asset (e.g. USDC)$AMOUNTwith the amount in smallest units (USDC has 6 decimals)
Shares / decimals note
The vault’s share token uses 18 decimals (ERC20 default), and computes an internal decimalOffset as:
decimalOffset = 18 - ERC20(asset).decimals()
So for USDC (6 decimals), decimalOffset = 12. This is why in tests a USDC deposit can mint “18-decimal shares”.
Withdrawing (2-step flow)
Withdrawals are not done via the standard ERC-4626 withdraw(...) / redeem(...) path in this contract. Instead, the user must use:
initiateWithdraw(shares, receiver, owner)wait 6 hours
withdraw(owner)
Step 1 — initiateWithdraw
Call:
initiateWithdraw(uint256 shares, address receiver, address owner)
What it does:
Reverts if
owneralready has a pending withdrawal (ErrorsLib.AlreadyInitiated()).Accrues interest.
Converts
shares → assetsusing current totals (rounding up).Calls
_withdrawFromTownSq(assets, owner, receiver):Ensures requested assets don’t exceed vault’s expected supply.
Enforces liquidity via
_withdrawable()(deposit minus borrows fromassetHubPool).Stores
initiatedWithdrawal[owner] = { receiver, assets, shares, timeInitiated, true }Burns the owner’s shares immediately.
Updates
lastTotalAssetsdown.Calls
spokeOperations.withdraw(...)to start withdrawing from TownSq.
If TownSq liquidity is insufficient, this step can revert with:
ErrorsLib.NotEnoughLiquidity()
Step 2
The finalize call requires a minimum delay:
ConstantsLib.WITHDRAW_INITIALIZATION_TIMELOCK = 6 hours
If you call finalize too early:
ErrorsLib.WithdrawTimeLockNotExpired()
Step 3 — withdraw (finalize)
Call:
withdraw(address owner)
What it does:
Reverts if there is no initiated withdrawal (
ErrorsLib.NoInitiatedWithdrawal()).Reverts if 6 hours hasn’t elapsed.
Attempts to transfer
assetstoreceiverviasafeTransferExternal(receiver, assets).If the transfer succeeds: emits
EventsLib.AssetWithdraw(owner, receiver, assets)and clears the pending state.If the transfer fails (caught by
try/catch): it mints back the burned shares toreceiverand clears the pending state.
This “mint-back” fallback ensures the user isn’t stuck permanently if the final transfer cannot be executed for some reason.
Example (cast)
Assuming you want to withdraw all shares you own:
Operational notes
Liquidity matters:
initiateWithdrawenforces available liquidity usingassetHubPooltotals. If TownSq has outstanding borrows, withdrawals may revert until liquidity is available.Interest + fees: the vault accrues interest on interaction (
deposit,initiateWithdraw) via_accrueInterest(). Iffee != 0, it mints fee shares tofeeRecipient.Timelock variables:
WITHDRAW_INITIALIZATION_TIMELOCKis a hard-coded constant (6 hours).timelockis a separate vault parameter set ininitialize()via_setTimelock(initialTimelock)and bounded for non-zero values.
Last updated