Skip to content

Commit d8185d0

Browse files
committed
Refactor vault into storage accounting and transfer modules
1 parent 8919a5f commit d8185d0

4 files changed

Lines changed: 110 additions & 72 deletions

File tree

src/YieldSaveVault.sol

Lines changed: 11 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,12 @@ pragma solidity ^0.8.30;
44
import {ReentrancyGuard} from "openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol";
55

66
import {IERC20} from "./interfaces/IERC20.sol";
7-
import {IPool} from "./interfaces/IPool.sol";
7+
import {YieldSaveVaultStorage} from "./base/YieldSaveVaultStorage.sol";
8+
import {ERC20TransferLib} from "./libraries/ERC20TransferLib.sol";
9+
import {YieldSaveVaultAccounting} from "./base/YieldSaveVaultAccounting.sol";
810

9-
contract YieldSaveVault is ReentrancyGuard {
10-
uint256 public constant BPS_DENOMINATOR = 10_000;
11-
uint256 public constant MAX_FEE_BPS = 1_000;
12-
13-
IERC20 public immutable usdc;
14-
IERC20 public immutable aUsdc;
15-
IPool public immutable aavePool;
16-
address public immutable treasury;
17-
uint256 public immutable feeRate;
18-
19-
uint256 public totalShares;
20-
mapping(address => uint256) public userShares;
21-
mapping(address => uint256) public userDeposits;
11+
contract YieldSaveVault is ReentrancyGuard, YieldSaveVaultAccounting {
12+
using ERC20TransferLib for IERC20;
2213

2314
error ZeroAddress();
2415
error ZeroAmount();
@@ -36,17 +27,13 @@ contract YieldSaveVault is ReentrancyGuard {
3627
uint256 payout
3728
);
3829

39-
constructor(address usdc_, address aUsdc_, address aavePool_, address treasury_, uint256 feeRate_) {
30+
constructor(address usdc_, address aUsdc_, address aavePool_, address treasury_, uint256 feeRate_)
31+
YieldSaveVaultStorage(usdc_, aUsdc_, aavePool_, treasury_, feeRate_)
32+
{
4033
if (usdc_ == address(0) || aUsdc_ == address(0) || aavePool_ == address(0) || treasury_ == address(0)) {
4134
revert ZeroAddress();
4235
}
4336
if (feeRate_ > MAX_FEE_BPS) revert InvalidFeeRate();
44-
45-
usdc = IERC20(usdc_);
46-
aUsdc = IERC20(aUsdc_);
47-
aavePool = IPool(aavePool_);
48-
treasury = treasury_;
49-
feeRate = feeRate_;
5037
}
5138

5239
/// @notice Deposits USDC into the vault and mints internal shares for the sender.
@@ -139,63 +126,15 @@ contract YieldSaveVault is ReentrancyGuard {
139126
(payout, grossAssets, fee) = _previewWithdrawForUser(user, shares);
140127
}
141128

142-
function _previewWithdrawForUser(address user, uint256 shares)
143-
internal
144-
view
145-
returns (uint256 payout, uint256 grossAssets, uint256 fee)
146-
{
147-
uint256 userShareBalance = userShares[user];
148-
if (shares == 0 || userShareBalance == 0 || shares > userShareBalance) {
149-
return (0, 0, 0);
150-
}
151-
152-
(grossAssets,, fee) = _quoteWithdraw(user, shares, _totalAssets(), totalShares, userShareBalance);
153-
payout = grossAssets - fee;
154-
}
155-
156-
function _previewDeposit(uint256 amount, uint256 assetsBefore) internal view returns (uint256) {
157-
if (amount == 0) return 0;
158-
if (totalShares == 0 || assetsBefore == 0) return amount;
159-
return amount * totalShares / assetsBefore;
160-
}
161-
162-
function _quoteWithdraw(
163-
address user,
164-
uint256 shares,
165-
uint256 assets,
166-
uint256 currentTotalShares,
167-
uint256 userShareBalance
168-
) internal view returns (uint256 grossAssets, uint256 principalPortion, uint256 fee) {
169-
grossAssets = shares * assets / currentTotalShares;
170-
principalPortion = userDeposits[user] * shares / userShareBalance;
171-
172-
uint256 yld = grossAssets > principalPortion ? grossAssets - principalPortion : 0;
173-
fee = yld * feeRate / BPS_DENOMINATOR;
174-
}
175-
176-
// get the vault total assets (that is the USDC and the accrued yield)
177-
function _totalAssets() internal view returns (uint256) {
178-
return aUsdc.balanceOf(address(this));
179-
}
180-
181129
function _safeTransfer(IERC20 token, address to, uint256 amount) internal {
182-
(bool success, bytes memory data) =
183-
address(token).call(abi.encodeCall(IERC20.transfer, (to, amount)));
184-
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert ERC20CallFailed();
130+
if (!token.safeTransfer(to, amount)) revert ERC20CallFailed();
185131
}
186132

187133
function _safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal {
188-
(bool success, bytes memory data) =
189-
address(token).call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));
190-
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert ERC20CallFailed();
134+
if (!token.safeTransferFrom(from, to, amount)) revert ERC20CallFailed();
191135
}
192136

193137
function _forceApprove(IERC20 token, address spender, uint256 amount) internal {
194-
(bool success, bytes memory data) =
195-
address(token).call(abi.encodeCall(IERC20.approve, (spender, 0)));
196-
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert ERC20CallFailed();
197-
198-
(success, data) = address(token).call(abi.encodeCall(IERC20.approve, (spender, amount)));
199-
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) revert ERC20CallFailed();
138+
if (!token.forceApprove(spender, amount)) revert ERC20CallFailed();
200139
}
201140
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
import {YieldSaveVaultStorage} from "./YieldSaveVaultStorage.sol";
5+
6+
abstract contract YieldSaveVaultAccounting is YieldSaveVaultStorage {
7+
function _previewDeposit(uint256 amount, uint256 assetsBefore) internal view returns (uint256) {
8+
if (amount == 0) return 0;
9+
if (totalShares == 0 || assetsBefore == 0) return amount;
10+
return amount * totalShares / assetsBefore;
11+
}
12+
13+
function _quoteWithdraw(
14+
address user,
15+
uint256 shares,
16+
uint256 assets,
17+
uint256 currentTotalShares,
18+
uint256 userShareBalance
19+
) internal view returns (uint256 grossAssets, uint256 principalPortion, uint256 fee) {
20+
grossAssets = shares * assets / currentTotalShares;
21+
principalPortion = userDeposits[user] * shares / userShareBalance;
22+
23+
uint256 yld = grossAssets > principalPortion ? grossAssets - principalPortion : 0;
24+
fee = yld * feeRate / BPS_DENOMINATOR;
25+
}
26+
27+
function _previewWithdrawForUser(address user, uint256 shares)
28+
internal
29+
view
30+
returns (uint256 payout, uint256 grossAssets, uint256 fee)
31+
{
32+
uint256 userShareBalance = userShares[user];
33+
if (shares == 0 || userShareBalance == 0 || shares > userShareBalance) {
34+
return (0, 0, 0);
35+
}
36+
37+
(grossAssets,, fee) = _quoteWithdraw(user, shares, _totalAssets(), totalShares, userShareBalance);
38+
payout = grossAssets - fee;
39+
}
40+
41+
function _totalAssets() internal view returns (uint256) {
42+
return aUsdc.balanceOf(address(this));
43+
}
44+
}

src/base/YieldSaveVaultStorage.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
import {IERC20} from "../interfaces/IERC20.sol";
5+
import {IPool} from "../interfaces/IPool.sol";
6+
7+
abstract contract YieldSaveVaultStorage {
8+
uint256 public constant BPS_DENOMINATOR = 10_000;
9+
uint256 public constant MAX_FEE_BPS = 1_000;
10+
11+
IERC20 public immutable usdc;
12+
IERC20 public immutable aUsdc;
13+
IPool public immutable aavePool;
14+
address public immutable treasury;
15+
uint256 public immutable feeRate;
16+
17+
uint256 public totalShares;
18+
mapping(address => uint256) public userShares;
19+
mapping(address => uint256) public userDeposits;
20+
21+
constructor(address usdc_, address aUsdc_, address aavePool_, address treasury_, uint256 feeRate_) {
22+
usdc = IERC20(usdc_);
23+
aUsdc = IERC20(aUsdc_);
24+
aavePool = IPool(aavePool_);
25+
treasury = treasury_;
26+
feeRate = feeRate_;
27+
}
28+
}

src/libraries/ERC20TransferLib.sol

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.30;
3+
4+
import {IERC20} from "../interfaces/IERC20.sol";
5+
6+
library ERC20TransferLib {
7+
function safeTransfer(IERC20 token, address to, uint256 amount) internal returns (bool) {
8+
(bool success, bytes memory data) =
9+
address(token).call(abi.encodeCall(IERC20.transfer, (to, amount)));
10+
return success && (data.length == 0 || abi.decode(data, (bool)));
11+
}
12+
13+
function safeTransferFrom(IERC20 token, address from, address to, uint256 amount) internal returns (bool) {
14+
(bool success, bytes memory data) =
15+
address(token).call(abi.encodeCall(IERC20.transferFrom, (from, to, amount)));
16+
return success && (data.length == 0 || abi.decode(data, (bool)));
17+
}
18+
19+
function forceApprove(IERC20 token, address spender, uint256 amount) internal returns (bool) {
20+
(bool success, bytes memory data) =
21+
address(token).call(abi.encodeCall(IERC20.approve, (spender, 0)));
22+
if (!success || (data.length != 0 && !abi.decode(data, (bool)))) return false;
23+
24+
(success, data) = address(token).call(abi.encodeCall(IERC20.approve, (spender, amount)));
25+
return success && (data.length == 0 || abi.decode(data, (bool)));
26+
}
27+
}

0 commit comments

Comments
 (0)