Skip to content
Open
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
107 changes: 107 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CashflowVaults (Avalanche Hackathon MVP)

CashflowVaults is a Solidity-first MVP that tokenizes **simulated future cashflows** using an ERC-4626 style vault.

- Investors deposit mock USDC and receive fungible vault shares.
- A trusted `creator` periodically contributes cashflows to the vault via `CashflowSchedule`.
- Investors redeem shares for underlying assets. When the creator pays, assets increase while share supply stays fixed, so PPS increases.

> Trust model (explicit): this MVP is intentionally offchain-light. There is no oracle, KYC, legal enforcement, or payment guarantee. The creator is trusted to keep paying.

## Contracts

- `src/MockUSDC.sol`: 6-decimal mintable ERC20 for local testing.
- `src/CashflowVault.sol`: ERC-4626 style vault implementation with deposit/mint/withdraw/redeem.
- `src/CashflowSchedule.sol`: single agreement state machine (`ACTIVE/PAUSED/ENDED`) and periodic `pay()`.
- `src/CashflowShareNFT.sol`: optional ERC721 investor participation receipt (demo/storytelling only).

## Architecture (ASCII)

```text
Creator Investors
| |
| approve + pay() | deposit()/mint()
v v
+-------------------+ transfer USDC +-------------------+
| CashflowSchedule | -------------------> | CashflowVault |
| - cadence | | (share ERC20) |
| - paymentAmount | | - totalAssets |
| - startTime | | - totalSupply |
| - state | +-------------------+
+-------------------+ |
| holds
v
+-----------+
| MockUSDC |
+-----------+

Optional:
CashflowVault -> mintReceipt() -> CashflowShareNFT (1st deposit receipt)
```

## Local Commands

```bash
cd contracts
forge build
forge test -vv
```

### Deploy script

```bash
cd contracts
CREATOR_ADDRESS=<creator_addr> \
PAYMENT_CADENCE=604800 \
PAYMENT_AMOUNT=100000000 \
START_TIME=<unix_ts> \
forge script script/Deploy.s.sol:DeployCashflowVaults --broadcast
```

## Scripted Demo (cast + scripts)

### A) One-command scripted flow with `Demo.s.sol`

```bash
cd contracts
CREATOR_PK=<hex_private_key> \
INVESTOR_PK=<hex_private_key> \
forge script script/Demo.s.sol:DemoCashflowVaults --broadcast
```

### B) Manual `cast` walkthrough (assuming deployed addresses)

```bash
# Environment
RPC_URL=<your_rpc>
CREATOR_PK=<creator_pk>
INVESTOR_PK=<investor_pk>
USDC=<mock_usdc_address>
VAULT=<vault_address>
SCHEDULE=<schedule_address>
INVESTOR=$(cast wallet address --private-key $INVESTOR_PK)

# 1) Mint investor + creator balances (demo token is permissionless mint)
cast send $USDC "mint(address,uint256)" $INVESTOR 1000000000 --private-key $INVESTOR_PK --rpc-url $RPC_URL
cast send $USDC "mint(address,uint256)" $(cast wallet address --private-key $CREATOR_PK) 10000000000 --private-key $CREATOR_PK --rpc-url $RPC_URL

# 2) Investor deposits 1,000 USDC
cast send $USDC "approve(address,uint256)" $VAULT 1000000000 --private-key $INVESTOR_PK --rpc-url $RPC_URL
cast send $VAULT "deposit(uint256,address)" 1000000000 $INVESTOR --private-key $INVESTOR_PK --rpc-url $RPC_URL

# 3) Creator pays twice
cast send $USDC "approve(address,uint256)" $SCHEDULE 200000000 --private-key $CREATOR_PK --rpc-url $RPC_URL
cast send $SCHEDULE "pay()" --private-key $CREATOR_PK --rpc-url $RPC_URL
# wait >= cadence
cast send $SCHEDULE "pay()" --private-key $CREATOR_PK --rpc-url $RPC_URL

# 4) Investor redeems all shares
SHARES=$(cast call $VAULT "balanceOf(address)(uint256)" $INVESTOR --rpc-url $RPC_URL)
cast send $VAULT "redeem(uint256,address,address)" $SHARES $INVESTOR $INVESTOR --private-key $INVESTOR_PK --rpc-url $RPC_URL
```

## Notes for Avalanche readiness

- Contracts use standard EVM Solidity (`^0.8.24`) only.
- No chain-specific precompiles/opcodes.
- Works on Avalanche C-Chain or any EVM-compatible network.
2 changes: 1 addition & 1 deletion contracts/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
src = "src"
out = "out"
libs = ["lib"]
solc_version = "0.8.20"
solc_version = "0.8.24"
optimizer = true
optimizer_runs = 200
46 changes: 46 additions & 0 deletions contracts/script/Demo.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "forge-std/Script.sol";

import {MockUSDC} from "../src/MockUSDC.sol";
import {CashflowVault} from "../src/CashflowVault.sol";
import {CashflowSchedule} from "../src/CashflowSchedule.sol";

/// @title DemoCashflowVaults
/// @notice Scripted local demo: mint USDC -> deposit -> creator pays twice -> investor redeems.
contract DemoCashflowVaults is Script {
/// @notice Executes the scripted MVP flow against freshly deployed contracts.
function run() external {
uint256 creatorPk = vm.envUint("CREATOR_PK");
uint256 investorPk = vm.envUint("INVESTOR_PK");

address creator = vm.addr(creatorPk);
address investor = vm.addr(investorPk);

vm.startBroadcast(creatorPk);
MockUSDC usdc = new MockUSDC();
CashflowVault vault = new CashflowVault(usdc);
CashflowSchedule schedule = new CashflowSchedule(creator, vault, 7 days, 100e6, block.timestamp);
usdc.mint(creator, 10_000e6);
usdc.approve(address(schedule), type(uint256).max);
vm.stopBroadcast();

vm.startBroadcast(investorPk);
usdc.mint(investor, 1_000e6);
usdc.approve(address(vault), type(uint256).max);
vault.deposit(1_000e6, investor);
vm.stopBroadcast();

vm.startBroadcast(creatorPk);
schedule.pay();
vm.warp(block.timestamp + 7 days);
schedule.pay();
vm.stopBroadcast();

vm.startBroadcast(investorPk);
uint256 shares = vault.balanceOf(investor);
vault.redeem(shares, investor, investor);
vm.stopBroadcast();
}
}
31 changes: 25 additions & 6 deletions contracts/script/Deploy.s.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
pragma solidity ^0.8.24;

import "forge-std/Script.sol";
import "../src/PriceAlertRegistry.sol";

contract DeployPriceAlertRegistry is Script {
function run() external returns (PriceAlertRegistry) {
import {MockUSDC} from "../src/MockUSDC.sol";
import {CashflowVault} from "../src/CashflowVault.sol";
import {CashflowSchedule} from "../src/CashflowSchedule.sol";
import {CashflowShareNFT} from "../src/CashflowShareNFT.sol";

/// @title DeployCashflowVaults
/// @notice Deployment entrypoint for CashflowVaults MVP contracts.
contract DeployCashflowVaults is Script {
/// @notice Deploys asset, vault, schedule, and optional receipt NFT.
/// @return usdc Deployed MockUSDC instance.
/// @return vault Deployed cashflow vault.
/// @return schedule Deployed payment schedule.
/// @return nft Deployed participation receipt NFT.
function run() external returns (MockUSDC usdc, CashflowVault vault, CashflowSchedule schedule, CashflowShareNFT nft) {
address creator = vm.envAddress("CREATOR_ADDRESS");
uint256 cadence = vm.envUint("PAYMENT_CADENCE");
uint256 amount = vm.envUint("PAYMENT_AMOUNT");
uint256 start = vm.envUint("START_TIME");

vm.startBroadcast();
PriceAlertRegistry registry = new PriceAlertRegistry();
usdc = new MockUSDC();
vault = new CashflowVault(usdc);
schedule = new CashflowSchedule(creator, vault, cadence, amount, start);
nft = new CashflowShareNFT("Cashflow Receipt", "CFR", address(vault), 1);
vault.setShareReceiptNft(nft);
vm.stopBroadcast();
return registry;
}
}
93 changes: 93 additions & 0 deletions contracts/src/CashflowSchedule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {MockUSDC} from "./MockUSDC.sol";
import {CashflowVault} from "./CashflowVault.sol";

/// @title CashflowSchedule
/// @notice Manages a single simulated cashflow agreement and periodic creator payments.
/// @dev Trust model: creator is trusted to keep approving and funding payments; no oracle or enforcement layer.
contract CashflowSchedule {
enum State {
ACTIVE,
PAUSED,
ENDED
}

address public immutable creator;
CashflowVault public immutable vault;
MockUSDC public immutable asset;
uint256 public immutable cadence;
uint256 public immutable paymentAmount;
uint256 public immutable startTime;

State public state;
uint256 public lastPaymentTime;

event Paid(address indexed payer, uint256 amount, uint256 paidAt);
event StateChanged(State indexed newState);

/// @notice Creates a new cashflow agreement.
/// @param _creator Creator responsible for scheduled payments.
/// @param _vault Vault receiving cashflows.
/// @param _cadence Minimum seconds between two `pay` calls.
/// @param _paymentAmount Fixed payment size per period.
/// @param _startTime Timestamp at which payments become valid.
constructor(address _creator, CashflowVault _vault, uint256 _cadence, uint256 _paymentAmount, uint256 _startTime) {
require(_creator != address(0), "ZERO_CREATOR");
require(address(_vault) != address(0), "ZERO_VAULT");
require(_cadence > 0, "ZERO_CADENCE");
require(_paymentAmount > 0, "ZERO_PAYMENT");

creator = _creator;
vault = _vault;
asset = _vault.asset();
cadence = _cadence;
paymentAmount = _paymentAmount;
startTime = _startTime;
state = State.ACTIVE;
}

/// @notice Executes one scheduled payment from creator to vault.
/// @dev We intentionally donate assets directly to the vault instead of calling vault.deposit.
/// If we called `deposit`, new shares would be minted and PPS would remain mostly unchanged.
/// Direct donation increases assets backing existing shares, so investors capture upside.
function pay() external {
require(state == State.ACTIVE, "NOT_ACTIVE");
require(block.timestamp >= startTime, "NOT_STARTED");
if (lastPaymentTime != 0) {
require(block.timestamp >= lastPaymentTime + cadence, "TOO_EARLY");
}

require(asset.transferFrom(creator, address(vault), paymentAmount), "PAYMENT_TRANSFER_FAILED");
lastPaymentTime = block.timestamp;
emit Paid(creator, paymentAmount, block.timestamp);
}

/// @notice Pauses future payments.
function pause() external onlyCreator {
require(state == State.ACTIVE, "BAD_STATE");
state = State.PAUSED;
emit StateChanged(state);
}

/// @notice Resumes payments after a pause.
function resume() external onlyCreator {
require(state == State.PAUSED, "BAD_STATE");
state = State.ACTIVE;
emit StateChanged(state);
}

/// @notice Permanently ends the agreement.
function end() external onlyCreator {
require(state != State.ENDED, "ALREADY_ENDED");
state = State.ENDED;
emit StateChanged(state);
}

/// @notice Restricts calls to creator.
modifier onlyCreator() {
require(msg.sender == creator, "ONLY_CREATOR");
_;
}
}
Loading