Skip to content

Commit 4d89ab4

Browse files
committed
feat: implement redeem, withdraw, collateral deposit & redemption flows with full test coverage
- Added redeem(), withdraw(), depositCollateral(), redeemForCollateral() entrypoints in ZNative - Implemented _redeem, _withdraw, _depositCollateral, _redeemForCollateral with events - Added SafeTransferLib for native token transfers - Added Withdrawn, CollateralDeposited, CollateralRedeemed events - Updated IZToken interface with new events and struct changes - Updated tests for mint, deposit, redeem, withdraw, collateral flows - Added modifiers and structured tests using when/it blocks - Updated .tree specification for all new behaviors - Minor cleanup and internal refactoring across ZNative and ZToken
1 parent b689508 commit 4d89ab4

6 files changed

Lines changed: 265 additions & 7 deletions

File tree

src/ZNative.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.30;
33

44
import { IZToken, ZToken } from "./ZToken.sol";
55
import { IZNative } from "./interfaces/IZNative.sol";
6+
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
67

78
contract ZNative is ZToken, IZNative {
89
address private constant NATIVE_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
@@ -19,10 +20,30 @@ contract ZNative is ZToken, IZNative {
1920
return _deposit(msg.value);
2021
}
2122

23+
function redeem(uint256 _zAmount) external returns (uint256) {
24+
return _redeem(_zAmount);
25+
}
26+
27+
function withdraw(uint256 _underlyingAmount) external returns (uint256) {
28+
return _withdraw(_underlyingAmount);
29+
}
30+
31+
function depositCollateral() external payable {
32+
_depositCollateral(msg.value);
33+
}
34+
35+
function redeemForCollateral(uint256 _zAmount) external returns (uint256) {
36+
return _redeemForCollateral(_zAmount);
37+
}
38+
2239
function _transferIn(uint256 _amount) internal override {
2340
if (msg.value != _amount) revert InvalidMsgValue(_amount, msg.value);
2441
}
2542

43+
function _transferOut(uint256 _amount) internal override {
44+
SafeTransferLib.safeTransferETH(msg.sender, _amount);
45+
}
46+
2647
function name() public pure override returns (string memory) {
2748
return type(ZNative).name;
2849
}

src/ZToken.sol

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,66 @@ abstract contract ZToken is IZToken, ERC20 {
9595
emit Deposited(msg.sender, _underlyingAmount, zAmount);
9696
}
9797

98+
/**
99+
* @notice Redeems ZTokens for the corresponding amount of underlying asset.
100+
* @dev
101+
* - Reverts if `_zAmount` is zero.
102+
* - Computes the amount of underlying to return using:
103+
* underlying = zAmount * exchangeRate
104+
* - Burns `_zAmount` ZTokens from the caller.
105+
* - Sends `underlyingAmount` of the underlying asset to the caller.
106+
* - Emits a {Redeemed} event.
107+
*
108+
* @param _zAmount The amount of ZTokens the user wants to redeem.
109+
*
110+
* @return underlyingAmount The amount of underlying asset returned to the user.
111+
*/
112+
function _redeem(uint256 _zAmount) internal returns (uint256 underlyingAmount) {
113+
if (_zAmount == 0) revert ZeroAmountIsNotAllowed();
114+
115+
underlyingAmount = _zAmount.mulWad(getExchangeRate());
116+
117+
_burn(msg.sender, _zAmount);
118+
_transferOut(underlyingAmount);
119+
120+
emit Redeemed(msg.sender, _zAmount, underlyingAmount);
121+
}
122+
123+
/**
124+
* @notice Withdraws a specific amount of underlying by burning the equivalent ZTokens.
125+
* @dev
126+
* - Reverts if `_underlyingAmount` is zero.
127+
* - Computes the ZTokens to burn using:
128+
* zAmount = underlyingAmount / exchangeRate
129+
* - Burns `zAmount` ZTokens from the caller.
130+
* - Sends `_underlyingAmount` of the underlying asset to the caller.
131+
* - Emits a {Withdrawn} event.
132+
*
133+
* @param _underlyingAmount The amount of underlying asset the user wants to withdraw.
134+
*
135+
* @return zAmount The amount of ZTokens burned from the user.
136+
*/
137+
function _withdraw(uint256 _underlyingAmount) internal returns (uint256 zAmount) {
138+
if (_underlyingAmount == 0) revert ZeroAmountIsNotAllowed();
139+
140+
zAmount = _underlyingAmount.divWad(getExchangeRate());
141+
142+
_burn(msg.sender, zAmount);
143+
_transferOut(_underlyingAmount);
144+
145+
emit Withdrawn(msg.sender, _underlyingAmount, zAmount);
146+
}
147+
148+
function _depositCollateral(uint256 _underlyingAmount) internal {
149+
if (_underlyingAmount == 0) revert ZeroAmountIsNotAllowed();
150+
}
151+
152+
function _redeemForCollateral(uint256 _zAmount) internal returns (uint256) {
153+
if (_zAmount == 0) revert ZeroAmountIsNotAllowed();
154+
}
155+
98156
function _transferIn(uint256 _amount) internal virtual { }
157+
function _transferOut(uint256 _amount) internal virtual { }
99158

100159
function _accureInterest() internal {
101160
uint256 cash = _getCash();

src/interfaces/IZToken.sol

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pragma solidity 0.8.30;
33

44
interface IZToken {
55
error ZeroAmountIsNotAllowed();
6+
// error InsufficientBalance();
67

78
struct MarketConfig {
89
address underlyingToken;
@@ -19,7 +20,13 @@ interface IZToken {
1920
uint256 snapshotIndex;
2021
}
2122

23+
event CollateralDeposited(address indexed account, uint256 underlyingAmount, uint256 newCollateralBalance);
24+
event CollateralRedeemed(
25+
address indexed account, uint256 zAmount, uint256 underlyingAmount, uint256 newCollateralBalance
26+
);
27+
event Redeemed(address indexed account, uint256 zAmount, uint256 underlyingAmount);
2228
event Minted(address indexed account, uint256 zAmount, uint256 underlyingAmount);
2329
event Deposited(address indexed account, uint256 underlyingAmount, uint256 zAmount);
30+
event Withdrawn(address indexed account, uint256 underlyingAmount, uint256 zAmount);
2431
}
2532

test/unit/ZNative.t.sol

Lines changed: 137 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ pragma solidity 0.8.30;
44
import { IZToken, ZNative } from "../../src/ZNative.sol";
55
import { SetUp } from "./utils/SetUp.sol";
66
import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";
7+
// import { console } from "forge-std/console.sol";
8+
9+
error InsufficientBalance();
710

811
contract ZNativeTest is SetUp {
912
using FixedPointMathLib for uint256;
@@ -24,12 +27,12 @@ contract ZNativeTest is SetUp {
2427
assertGt(bytes(native.symbol()).length, 0);
2528
}
2629

27-
function test_Mint_RevertWhen_zAmountIsZero() external {
30+
function test_Mint_WhenZTokenAmountIsZero() external {
2831
vm.expectRevert(IZToken.ZeroAmountIsNotAllowed.selector);
2932
native.mint(0);
3033
}
3134

32-
function test_Mint_WhenzAmountIsGreaterThanZero(uint256 _zAmount) external {
35+
function test_Mint_WhenZTokenAmountIsValid(uint256 _zAmount) external {
3336
_zAmount = bound(_zAmount, 1000, type(uint64).max);
3437

3538
uint256 expectedUnderlyingTokenAmount = native.getExchangeRate().mulWad(_zAmount);
@@ -60,7 +63,7 @@ contract ZNativeTest is SetUp {
6063
native.deposit();
6164
}
6265

63-
function test_Deposit_WhenZAmountIsGreaterThanZero(uint256 _underlyingAmount) external {
66+
function test_Deposit_WhenZTokenAmountIsValid(uint256 _underlyingAmount) external {
6467
uint256 balance = lender.balance;
6568
_underlyingAmount = bound(_underlyingAmount, 1000, balance);
6669

@@ -76,4 +79,135 @@ contract ZNativeTest is SetUp {
7679
assertEq(native.balanceOf(lender), zAmount);
7780
assertEq(address(native).balance, _underlyingAmount);
7881
}
82+
83+
function test_Redeem_WhenZTokenRedeemAmountIsZero() external {
84+
vm.expectRevert(IZToken.ZeroAmountIsNotAllowed.selector);
85+
native.redeem(0);
86+
}
87+
88+
modifier whenDepositAmountIsValid(uint256 _underlyingAmount) {
89+
uint256 balance = lender.balance;
90+
_underlyingAmount = bound(_underlyingAmount, 1e18, balance);
91+
vm.prank(lender);
92+
native.deposit{ value: _underlyingAmount }();
93+
_;
94+
}
95+
96+
modifier whenZTokenRedeemAmountIsValid(uint256 _underlyingAmount) {
97+
_;
98+
}
99+
100+
function test_Redeem_WhenZTokenRedeemAmountIsValid(uint256 _underlyingAmount)
101+
external
102+
whenDepositAmountIsValid(_underlyingAmount)
103+
{
104+
uint256 balance = lender.balance;
105+
106+
uint256 lenderZTokenBalanceBefore = native.balanceOf(lender);
107+
uint256 expectedUnderlyingAmount = lenderZTokenBalanceBefore.mulWad(native.getExchangeRate());
108+
uint256 zTokenTotalSupplyBefore = native.totalSupply();
109+
110+
vm.expectEmit(address(native));
111+
emit IZToken.Redeemed(lender, lenderZTokenBalanceBefore, expectedUnderlyingAmount);
112+
113+
vm.prank(lender);
114+
uint256 underlyingAmount = native.redeem(lenderZTokenBalanceBefore);
115+
116+
uint256 lenderNativeTokenBalanceAfter = lender.balance;
117+
uint256 lenderZTokenBalanceAfter = native.balanceOf(lender);
118+
uint256 zTokenTotalSupplyAfter = native.totalSupply();
119+
120+
assertEq(lenderZTokenBalanceAfter, 0);
121+
assertEq(zTokenTotalSupplyAfter, zTokenTotalSupplyBefore - lenderZTokenBalanceBefore);
122+
assertEq(lenderNativeTokenBalanceAfter, balance + underlyingAmount);
123+
}
124+
125+
function test_Withraw_WhenUnderlyingWithdrawalAmountIsZero() external {
126+
vm.expectRevert(IZToken.ZeroAmountIsNotAllowed.selector);
127+
native.withdraw(0);
128+
}
129+
130+
modifier whenUnderlyingWithdrawalAmountIsValid() {
131+
_;
132+
}
133+
134+
function test_Withraw_WhenUnderlyingWithdrawalAmountIsValid(uint256 _underlyingAmount)
135+
external
136+
whenDepositAmountIsValid(_underlyingAmount)
137+
whenUnderlyingWithdrawalAmountIsValid
138+
{
139+
uint256 balance = lender.balance;
140+
uint256 lenderZTokenBalanceBefore = native.balanceOf(lender);
141+
uint256 zTokenTotalSupplyBefore = native.totalSupply();
142+
143+
uint256 lenderUnderlyingBalance = lenderZTokenBalanceBefore.mulWad(native.getExchangeRate());
144+
145+
vm.expectEmit(address(native));
146+
emit IZToken.Withdrawn(lender, lenderUnderlyingBalance, lenderZTokenBalanceBefore);
147+
148+
vm.prank(lender);
149+
uint256 zAmount = native.withdraw(lenderUnderlyingBalance);
150+
assertEq(native.totalSupply(), zTokenTotalSupplyBefore - zAmount);
151+
assertEq(native.balanceOf(lender), lenderZTokenBalanceBefore - zAmount);
152+
assertEq(lender.balance, balance + lenderUnderlyingBalance);
153+
154+
// it should accure interest before processing the withrawal
155+
}
156+
157+
function test_DepositCollateral_WhenUnderlyingCollateralAmountIsZero() external {
158+
// it should revert with ZeroAmountIsNotAllowed
159+
vm.expectRevert(IZToken.ZeroAmountIsNotAllowed.selector);
160+
native.depositCollateral{ value: 0 }();
161+
}
162+
163+
function test_DepositCollateral_WhenUnderlyingCollateralAmountIsValid(uint256 _underlyingAmount) external {
164+
uint256 balance = borrower.balance;
165+
_underlyingAmount = bound(_underlyingAmount, 1e18, balance);
166+
167+
uint256 contractBalanceBefore = address(native).balance;
168+
169+
// it should emit a CollateralDeposited event
170+
vm.expectEmit(address(native));
171+
emit IZToken.CollateralDeposited(borrower, _underlyingAmount, _underlyingAmount);
172+
173+
vm.prank(borrower);
174+
native.depositCollateral{ value: _underlyingAmount }();
175+
// it should transfer the underlying to this contract
176+
assertEq(address(native).balance, contractBalanceBefore + _underlyingAmount);
177+
// it should update the user collateral balance
178+
}
179+
180+
function test_RedeemForCollateral_WhenZTokenCollateralAmountIsZero() external {
181+
// it should revert with ZeroAmountIsNotAllowed
182+
vm.expectRevert(IZToken.ZeroAmountIsNotAllowed.selector);
183+
native.redeemForCollateral(0);
184+
}
185+
186+
// another when to do this after multiple borrows
187+
// another when to do this after multiple deposit
188+
// another when after certain % of liquidation
189+
function test_RedeemForCollateral_WhenZTokenCollateralAmountIsValid(uint256 _underlyingAmount, uint256 _zAmount)
190+
external
191+
whenDepositAmountIsValid(_underlyingAmount)
192+
{
193+
uint256 lenderZTokenBalanceBefore = native.balanceOf(lender);
194+
_zAmount = bound(_zAmount, 1e18, lenderZTokenBalanceBefore);
195+
uint256 contractBalanceBefore = address(native).balance;
196+
197+
uint256 expectedCollateralAmount = _zAmount.mulWad(native.getExchangeRate());
198+
199+
vm.expectEmit(address(native));
200+
emit IZToken.CollateralRedeemed(lender, _zAmount, expectedCollateralAmount, expectedCollateralAmount);
201+
202+
vm.prank(lender);
203+
uint256 collateralAmount = native.redeemForCollateral(_zAmount);
204+
205+
// it should accure interest
206+
// it should burn the ZToken
207+
assertEq(native.balanceOf(lender), lenderZTokenBalanceBefore - _zAmount);
208+
// it should transfer the underlying token to this contract
209+
assertEq(address(native).balance, contractBalanceBefore + collateralAmount);
210+
// it should update the user collateral balance
211+
// it should emit a CollateralRedeemed event
212+
}
79213
}

test/unit/ZNative.tree

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ ZNativeTest::symbol
77
ZNativeTest::mint
88
├── when ZToken amount is zero
99
│ └── it should revert with ZeroAmountIsNotAllowed
10-
├── when ZToken amount is greater than zero
10+
├── when ZToken amount is valid
1111
│ ├── it should convert ZToken to underlying
1212
│ ├── it should transfer underlying to ZToken contract
1313
│ ├── it should mint ZToken to the sender
@@ -22,8 +22,47 @@ ZNativeTest::mint
2222
ZNativeTest::deposit
2323
├── when underlying amount is zero
2424
│ └── it should revert with ZeroAmountIsNotAllowed
25-
└── when ZToken amount is greater than zero
25+
└── when ZToken amount is valid
2626
├── it should convert underlying to ZToken
2727
├── it should transfer underlying to ZToken contract
2828
├── it should mint ZToken to the sender
2929
└── it should emit a deposit event
30+
31+
ZNativeTest::redeem
32+
├── when ZToken redeem amount is zero
33+
│ └── it should revert with ZeroAmountIsNotAllowed
34+
└── when ZToken redeem amount is valid
35+
├── it should convert ZToken to the correct underlying amount using the exchange rate
36+
├── it should burn the sender's ZToken
37+
├── it should transfer the native token back to the sender
38+
├── it should emit a redeemed event
39+
└── it should accure interest before processing the redeem
40+
41+
ZNativeTest::withraw
42+
├── when underlying withdrawal amount is zero
43+
│ └── it should revert with ZeroAmountIsNotAllowed
44+
└── when underlying withdrawal amount is valid
45+
├── it should convert underlying to the correct ZToken amount using the exchange rate
46+
├── it should burn the sender's ZToken
47+
├── it should transfer the native token back to the sender
48+
├── it should emit a withdrawn event
49+
└── it should accure interest before processing the withrawal
50+
51+
52+
ZNativeTest::depositCollateral
53+
├── when underlying collateral amount is zero
54+
│ └── it should revert with ZeroAmountIsNotAllowed
55+
└── when underlying collateral amount is valid
56+
├── it should transfer the underlying to this contract
57+
├── it should update the user collateral balance
58+
└── it should emit a CollateralDeposited event
59+
60+
ZNativeTest::redeemForCollateral
61+
├── when ZToken collateral amount is zero
62+
│ └── it should revert with ZeroAmountIsNotAllowed
63+
└── when ZToken collateral amount is valid
64+
├── it should accure interest
65+
├── it should burn the ZToken
66+
├── it should transfer the underlying token to this contract
67+
├── it should update the user collateral balance
68+
└── it should emit a CollateralRedeemed event

test/unit/ZToken.t.sol

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ contract ZTokenTest is SetUp {
131131
external
132132
whenThereIsBorrowActivity(_cash, _borrow)
133133
{
134-
// it borrow index should not change
135134
assertEq(zToken.getBorrowIndex(), 1e18);
136135
}
137136

@@ -140,7 +139,6 @@ contract ZTokenTest is SetUp {
140139
whenThereIsBorrowActivity(_cash, _borrow)
141140
whenThereIsBorrowActivity(_cash, _borrow)
142141
{
143-
// it borrow index should be greater than zero
144142
assertGt(zToken.getBorrowIndex(), 1e18);
145143
}
146144
}

0 commit comments

Comments
 (0)