Skip to content

Commit dfeec1e

Browse files
committed
fix: principal burned on repay, only stability fee to treasury — redeploy vDOT market
1 parent 90d134e commit dfeec1e

2 files changed

Lines changed: 63 additions & 52 deletions

File tree

contracts/TreasuryRouter.sol

Lines changed: 51 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,43 +13,34 @@ interface IUSDH {
1313
/// @title TreasuryRouter — passed as _usdh to LendingPool constructor.
1414
///
1515
/// Revenue model (MakerDAO-style):
16-
/// 100% of repayment/liquidation flows go to the protocol treasury.
17-
/// USDH is a stablecoin — burning it destroys borrowing capacity for
18-
/// zero token-value benefit. Instead, treasury governance directs funds to:
19-
/// Phase 1 (testnet): operations, hackathon prizes, liquidity
20-
/// Phase 2 (mainnet): buy DOT/vDOT on open market → distribute to stakers
21-
/// Phase 3 (token): buy DOTLEND governance token → burn
16+
/// Only accrued stability fee goes to treasury.
17+
/// Principal is burned — correct USDH peg mechanics.
2218
///
23-
/// Intercept strategy:
24-
/// LendingPool.repay() calls in order:
25-
/// (A) usdh.transferFrom(user, address(this), amount) — pull from user
26-
/// (B) usdh.burn(amount) — burn from pool
27-
///
28-
/// We intercept at (A): when transferFrom(user, lendingPool, amount) is called,
29-
/// the router pulls USDH from user directly into itself, sends 100% to
30-
/// treasury. When (B) usdh.burn(amount) is called, it's a no-op.
31-
///
32-
/// Deploy order:
33-
/// 1. Deploy TreasuryRouter(usdhAddress, treasuryAddress)
34-
/// 2. Deploy CollateralVault(collateral, oracle)
35-
/// 3. Deploy LendingPool(vault, ROUTER_ADDRESS, oracle, collateral)
36-
/// 4. vault.setLendingPool(pool)
37-
/// 5. router.setLendingPool(pool) ← so router knows pool address
19+
/// Principal tracking:
20+
/// mint(user, amount) is called on borrow — we record principal here.
21+
/// On repay, transferFrom intercepts and splits:
22+
/// principalPortion → burned
23+
/// feePortion (interest) → treasury
3824
contract TreasuryRouter is Ownable {
3925
IUSDH public immutable usdh;
4026
address public treasury;
4127
address public lendingPool;
4228

29+
/// @notice Tracks original borrowed principal per user
30+
mapping(address => uint256) public principalDebt;
31+
4332
uint256 public totalFeesCollected;
33+
uint256 public totalBurned;
4434

45-
event ProtocolFeeCollected(uint256 amount);
35+
event ProtocolFeeCollected(address indexed user, uint256 feeAmount);
36+
event PrincipalBurned(address indexed user, uint256 burnAmount);
4637
event TreasuryUpdated(address indexed newTreasury);
4738
event LendingPoolSet(address indexed pool);
4839

4940
constructor(address _usdh, address _treasury) {
50-
require(_usdh != address(0), "TR: zero usdh");
41+
require(_usdh != address(0), "TR: zero usdh");
5142
require(_treasury != address(0), "TR: zero treasury");
52-
usdh = IUSDH(_usdh);
43+
usdh = IUSDH(_usdh);
5344
treasury = _treasury;
5445
}
5546

@@ -65,28 +56,50 @@ contract TreasuryRouter is Ownable {
6556
emit TreasuryUpdated(_treasury);
6657
}
6758

68-
/// @notice Called by LendingPool as "transferFrom(user, lendingPool, amount)".
69-
/// If `to` == lendingPool, this is a repay/liquidation — intercept
70-
/// and route 100% to treasury. Otherwise forward transparently.
59+
/// @notice Called by LendingPool.borrow() — intercept to record principal.
60+
function mint(address to, uint256 amount) external {
61+
principalDebt[to] += amount;
62+
usdh.mint(to, amount);
63+
}
64+
65+
/// @notice Called by LendingPool.repay() via transferFrom.
66+
/// Splits: principal → burn, interest → treasury.
7167
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
7268
if (to == lendingPool && amount > 0) {
73-
// Intercept: pull from user into router, send 100% to treasury
69+
// Pull USDH from user into router
7470
usdh.transferFrom(from, address(this), amount);
75-
usdh.transfer(treasury, amount);
76-
totalFeesCollected += amount;
77-
emit ProtocolFeeCollected(amount);
71+
72+
// Split principal vs fee
73+
uint256 principal = principalDebt[from];
74+
uint256 principalPortion = amount <= principal ? amount : principal;
75+
uint256 feePortion = amount - principalPortion;
76+
77+
// Update tracked principal
78+
if (principalPortion > 0) {
79+
principalDebt[from] = principal - principalPortion;
80+
}
81+
82+
// Burn the principal
83+
if (principalPortion > 0) {
84+
usdh.burn(principalPortion);
85+
totalBurned += principalPortion;
86+
emit PrincipalBurned(from, principalPortion);
87+
}
88+
89+
// Send fee to treasury
90+
if (feePortion > 0) {
91+
usdh.transfer(treasury, feePortion);
92+
totalFeesCollected += feePortion;
93+
emit ProtocolFeeCollected(from, feePortion);
94+
}
95+
7896
return true;
7997
}
8098
return usdh.transferFrom(from, to, amount);
8199
}
82100

83-
/// @notice Called by LendingPool after transferFrom — already handled, no-op.
101+
/// @notice Called by LendingPool after transferFrom — already handled.
84102
function burn(uint256) external {
85-
// Revenue already routed to treasury in transferFrom() — nothing to burn
86-
}
87-
88-
/// @notice Forwards mint() to real USDH
89-
function mint(address to, uint256 amount) external {
90-
usdh.mint(to, amount);
103+
// Principal already burned in transferFrom() — no-op
91104
}
92105
}

frontend/src/lib/contracts.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export const COMMON_ADDRESSES = {
22
priceOracle: "0x1282F6B59869a57Fd2a1D7a5BC8535bB7B15D173" as `0x${string}`,
33
usdh: "0x7d605b39a8EeF1aCA3D63bD7A32E2719abA87683" as `0x${string}`,
4-
treasuryRouter: "0x2Cd79d84A68F9Ba2DeB3e638267A4772f11d8d80" as `0x${string}`,
54
solvencyGateway: "0x199E3E7c1f1382bc389b495B927B0535B390Acd0" as `0x${string}`,
65
xcmTreasuryDispatch: "0x3FfEAC3766F05752f8D3Ae8eEd00B57259Eb3c2d" as `0x${string}`,
76
};
@@ -15,25 +14,26 @@ export type MarketAddresses = {
1514

1615
export const MARKETS: Record<"vdot" | "wpas", MarketAddresses> = {
1716
vdot: {
18-
collateral: "0xfc1ACa9EDF5DA2eBEA5CE1320fb40A74Ac996544",
19-
collateralVault: "0x7E700a00290f4B12467361030b274769A10A490B",
20-
lendingPool: "0xb56dB40faa6Ee37c86Aa356682DfeCCcE7c8C668",
21-
treasuryRouter: "0x2Cd79d84A68F9Ba2DeB3e638267A4772f11d8d80",
17+
collateral: "0xfc1ACa9EDF5DA2eBEA5CE1320fb40A74Ac996544",
18+
collateralVault: "0xF94eBe7F8d8F922B7FBBFb4BE080EB71a69415A2",
19+
lendingPool: "0x34B22768B16262aD5b7fC23DD797D80791e4e7e6",
20+
treasuryRouter: "0x1adEe37eefd054927b14503Ff2076aE12Db76B30",
2221
},
2322
wpas: {
24-
collateral: "0xc09348291775B55Da40433ba44240c262D87Eb90",
23+
collateral: "0xc09348291775B55Da40433ba44240c262D87Eb90",
2524
collateralVault: "0x575B8578F000fC554394C63cec8F07Abd0C66C34",
26-
lendingPool: "0xF68bDd12a8904fd6bB0CbED5623722517FDd3408",
27-
treasuryRouter: "0xcC2Ca486257eED1201FCdc247F9a3120D0E8Be7a",
25+
lendingPool: "0xF68bDd12a8904fd6bB0CbED5623722517FDd3408",
26+
treasuryRouter: "0xcC2Ca486257eED1201FCdc247F9a3120D0E8Be7a",
2827
}
2928
};
3029

31-
// Kept for backward compat inside specific files temporarily if needed, but we will migrate out of this
30+
// Kept for backward compat
3231
export const ADDRESSES = {
3332
...COMMON_ADDRESSES,
34-
vdot: "0xfc1ACa9EDF5DA2eBEA5CE1320fb40A74Ac996544" as `0x${string}`,
35-
collateralVault: "0x7E700a00290f4B12467361030b274769A10A490B" as `0x${string}`,
36-
lendingPool: "0xb56dB40faa6Ee37c86Aa356682DfeCCcE7c8C668" as `0x${string}`,
33+
vdot: "0xfc1ACa9EDF5DA2eBEA5CE1320fb40A74Ac996544" as `0x${string}`,
34+
collateralVault: "0xF94eBe7F8d8F922B7FBBFb4BE080EB71a69415A2" as `0x${string}`,
35+
lendingPool: "0x34B22768B16262aD5b7fC23DD797D80791e4e7e6" as `0x${string}`,
36+
treasuryRouter: "0x1adEe37eefd054927b14503Ff2076aE12Db76B30" as `0x${string}`,
3737
};
3838

3939
export const EXPLORER = "https://blockscout-testnet.polkadot.io";
@@ -80,10 +80,8 @@ export const GATEWAY_ABI = [
8080
] as const;
8181

8282
export const ORACLE_ABI = [
83-
// getPrice reverts when stale — only use for on-chain transactions
8483
{ name: "getPrice", type: "function", stateMutability: "view",
8584
inputs: [{ name: "token", type: "address" }], outputs: [{ type: "uint256" }] },
86-
// Raw mapping getters — never revert, safe for UI display
8785
{ name: "prices", type: "function", stateMutability: "view",
8886
inputs: [{ name: "token", type: "address" }], outputs: [{ type: "uint256" }] },
8987
{ name: "lastUpdated", type: "function", stateMutability: "view",

0 commit comments

Comments
 (0)