Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions contracts/v2/VaultV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ contract VaultV2 is ReentrancyGuard {
address public admin;
bool public paused;

/// @notice Platform treasury address — authorized to call depositFor()
address public treasury;

/// @notice Internal ledger balances
mapping(address => uint256) public balances;

Expand Down Expand Up @@ -80,12 +83,14 @@ contract VaultV2 is ReentrancyGuard {
event Paused(address indexed admin);
event Unpaused(address indexed admin);
event AdminTransferred(address indexed oldAdmin, address indexed newAdmin);
event TreasuryUpdated(address indexed oldTreasury, address indexed newTreasury);

// ============================================================
// Errors
// ============================================================

error NotAdmin();
error NotTreasury();
error VaultPaused();
error InsufficientBalance();
error InsufficientSplitReserve();
Expand Down Expand Up @@ -128,6 +133,11 @@ contract VaultV2 is ReentrancyGuard {
_;
}

modifier onlyTreasury() {
if (msg.sender != treasury) revert NotTreasury();
_;
}

// ============================================================
// Constructor
// ============================================================
Expand Down Expand Up @@ -157,6 +167,31 @@ contract VaultV2 is ReentrancyGuard {
emit LedgerTransfer(address(0), msg.sender, amount, LedgerReason.UserDeposit);
}

/**
* @notice Deposit USDC on behalf of a recipient (platform treasury only)
* @param recipient Address whose vault balance is credited
* @param amount USDC amount (6 decimals)
* @dev Used by the Trial Market Program: treasury tops up the creator's vault
* balance so the factory can pull the $30 seed via pullForNewMarket().
* Requires treasury to be configured via setTreasury() by admin.
* See docs/TRIAL_MARKET_PROGRAM.md §8.1
*/
function depositFor(address recipient, uint256 amount)
external
nonReentrant
whenNotPaused
onlyTreasury
{
if (amount == 0) revert ZeroAmount();
if (recipient == address(0)) revert ZeroAddress();

usdc.safeTransferFrom(msg.sender, address(this), amount);
balances[recipient] += amount;
totalBalances += amount;

emit LedgerTransfer(address(0), recipient, amount, LedgerReason.PlatformSeed);
}

/**
* @notice Withdraw USDC from the vault
* @param amount USDC amount to withdraw
Expand Down Expand Up @@ -381,6 +416,12 @@ contract VaultV2 is ReentrancyGuard {
emit TrustedFactoryUpdated(_factory, false);
}

function setTreasury(address _treasury) external onlyAdmin {
address old = treasury;
treasury = _treasury;
emit TreasuryUpdated(old, _treasury);
}

function setExchange(address _exchange) external onlyAdmin {
exchange = _exchange;
}
Expand Down
3 changes: 2 additions & 1 deletion contracts/v2/interfaces/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ enum LedgerReason {
AccumulateFee,
WithdrawFeePool,
TransferBetween,
MarketInitLiquidity
MarketInitLiquidity,
PlatformSeed // Treasury deposits seed on behalf of trial market creator
}

// ============================================================
Expand Down
144 changes: 144 additions & 0 deletions test/VaultV2.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -287,4 +287,148 @@ contract VaultV2Test is Test {
uint256 accounting = vault.totalBalances() + vault.splitReserve() + vault.feePool();
assertEq(usdcBal, accounting, "invariant: USDC should exactly match accounting");
}

// ============================================================
// depositFor — Trial Market Program treasury seeding
// ============================================================

address public treasury = makeAddr("treasury");

function _setupTreasury() internal {
// Admin sets the treasury address
vm.prank(admin);
vault.setTreasury(treasury);

// Treasury mints and approves USDC
usdc.mint(treasury, 1_000_000_000); // 1000 USDC
vm.prank(treasury);
usdc.approve(address(vault), type(uint256).max);
}

function test_depositFor_creditsRecipient() public {
_setupTreasury();

uint256 seed = 30_000_000; // $30
vm.prank(treasury);
vault.depositFor(bob, seed);

assertEq(vault.balances(bob), seed);
assertEq(vault.totalBalances(), seed);
assertEq(usdc.balanceOf(address(vault)), seed);
}

function test_depositFor_invariantHolds() public {
_setupTreasury();

vm.prank(treasury);
vault.depositFor(bob, 30_000_000);

uint256 usdcBal = usdc.balanceOf(address(vault));
uint256 accounting = vault.totalBalances() + vault.splitReserve() + vault.feePool();
assertEq(usdcBal, accounting, "invariant must hold after depositFor");
}

function test_depositFor_allowsSubsequentPullForNewMarket() public {
_setupTreasury();

// Treasury tops up bob's balance ($30 seed)
vm.prank(treasury);
vault.depositFor(bob, 30_000_000);

// Factory pulls seed from bob to market (simulates trial market creation)
vm.prank(factory);
vault.pullForNewMarket(bob, market, 30_000_000);

assertEq(vault.balances(bob), 0);
assertEq(vault.balances(market), 30_000_000);
// totalBalances unchanged (zero-sum pullForNewMarket)
assertEq(vault.totalBalances(), 30_000_000);
}

function test_depositFor_revertsIfNotTreasury() public {
_setupTreasury();

vm.prank(alice); // not treasury
vm.expectRevert(VaultV2.NotTreasury.selector);
vault.depositFor(bob, 30_000_000);
}

function test_depositFor_revertsIfTreasuryNotSet() public {
// No setTreasury call — treasury is address(0)
usdc.mint(address(this), 30_000_000);
usdc.approve(address(vault), 30_000_000);

vm.expectRevert(VaultV2.NotTreasury.selector);
vault.depositFor(bob, 30_000_000);
}

function test_depositFor_revertsZeroAmount() public {
_setupTreasury();

vm.prank(treasury);
vm.expectRevert(VaultV2.ZeroAmount.selector);
vault.depositFor(bob, 0);
}

function test_depositFor_revertsZeroAddress() public {
_setupTreasury();

vm.prank(treasury);
vm.expectRevert(VaultV2.ZeroAddress.selector);
vault.depositFor(address(0), 30_000_000);
}

function test_depositFor_revertsWhenPaused() public {
_setupTreasury();

vm.prank(admin);
vault.pause();

vm.prank(treasury);
vm.expectRevert(VaultV2.VaultPaused.selector);
vault.depositFor(bob, 30_000_000);
}

// ============================================================
// setTreasury
// ============================================================

function test_setTreasury_byAdmin() public {
address newTreasury = makeAddr("newTreasury");

vm.prank(admin);
vault.setTreasury(newTreasury);

assertEq(vault.treasury(), newTreasury);
}

function test_setTreasury_emitsEvent() public {
address newTreasury = makeAddr("newTreasury");

vm.prank(admin);
vm.expectEmit(true, true, false, false);
emit VaultV2.TreasuryUpdated(address(0), newTreasury);
vault.setTreasury(newTreasury);
}

function test_setTreasury_revertsIfNotAdmin() public {
vm.prank(alice);
vm.expectRevert(VaultV2.NotAdmin.selector);
vault.setTreasury(makeAddr("treasury"));
}

function test_setTreasury_canClearTreasury() public {
_setupTreasury();

// Clear treasury (set to zero)
vm.prank(admin);
vault.setTreasury(address(0));

assertEq(vault.treasury(), address(0));

// depositFor should now revert (msg.sender != address(0))
vm.prank(treasury);
vm.expectRevert(VaultV2.NotTreasury.selector);
vault.depositFor(bob, 30_000_000);
}
}