Step 5: Transfer Tokens
Once the vault is registered, move the DST into it. This step is also where the DS Token compliance layer kicks in: any transfer to a wallet that the registry doesn't know about will be rejected by the token contract before the registrar even gets a chance to look at it. The order of operations matters.
Why this fails before step 4
DST is a regulated security token. Its transfer pre-checks consult the DS Registry: both from and to must resolve to a known investor identity. Until you've called registerVault() on the new vault address, the registry has no entry for it, and any transfer / transferFrom into the vault reverts.
The pattern is therefore strict:
1. Faucet drip → investor wallet (auto-registers the investor)
2. Vault deployed
3. registerVault() → vault is now bound to investor in the registry
4. transferFrom(investor → vault) ← only works after step 3
If you try step 4 before step 3 you'll see a token-side compliance revert, not a registrar error.
Transfer pattern
This is the MockDeFiProtocol shape. The investor approves the factory once, the factory deposits on demand, and the factory's deposit() is responsible for both registering and transferring inside one transaction.
// One-time approval
await investorWalletClient.writeContract({
address: DS_TOKEN,
abi: erc20Abi,
functionName: "approve",
args: [factoryAddress, parseUnits("100", 6)],
});
// Deposit (factory deploys vault on first call, then pulls funds)
await investorWalletClient.writeContract({
address: factoryAddress,
abi: factoryAbi,
functionName: "deposit",
args: [parseUnits("100", 6), deadline, signature],
});
Inside deposit():
function deposit(uint256 amount, uint256 deadline, bytes calldata signature) external {
address vault = investorVaults[msg.sender];
if (vault == address(0)) {
vault = address(new MinimalVault(address(this)));
investorVaults[msg.sender] = vault;
vaultRegistrar.registerVault(vault, msg.sender, deadline, signature);
}
IERC20(dsToken).transferFrom(msg.sender, vault, amount);
emit Deposit(msg.sender, vault, amount);
}
Properties:
- One investor signature (for the registrar) and one investor approval (for the token) bootstraps an unbounded number of future deposits.
- The factory must hold
OPERATOR_ROLEbecause it ismsg.senderofregisterVault(). - The investor pays gas only on the deposit they initiate.
Operator checklist
-
registerVault()confirmed before any transfer attempt. -
IERC20.allowance(investor, spender) >= amount(Mode A only). - After the transfer:
IERC20(dst).balanceOf(vault) >= amount. - After the transfer:
isRegistered(vault, investor) == true(sanity check; should still hold).