Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/repay wrapper #23

Open
wants to merge 7 commits into
base: feat/susds-wrappers
Choose a base branch
from
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
46 changes: 46 additions & 0 deletions src/BaseTokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper {
_borrowToken(amount, msg.sender, referralCode);
}

// @inheritdoc IBaseTokenWrapper
function repayToken(
uint256 amount,
address onBehalfOf
) external virtual returns (uint256) {
return _repayToken(amount, onBehalfOf);
}

// @inheritdoc IBaseTokenWrapper
function repayWithPermit(
uint256 amount,
address onBehalfOf,
PermitSignature calldata signature
) external virtual returns (uint256) {
// explicitly left try-catch block blank to protect users from permit griefing
try
IERC20WithPermit(TOKEN_IN).permit(
msg.sender,
address(this),
amount,
signature.deadline,
signature.v,
signature.r,
signature.s
)
{} catch {}
return _repayToken(amount, onBehalfOf);
}

/// @inheritdoc IBaseTokenWrapper
function rescueTokens(
IERC20 token,
Expand Down Expand Up @@ -240,6 +269,23 @@ abstract contract BaseTokenWrapper is Ownable, IBaseTokenWrapper {
IERC20(TOKEN_IN).transfer(onBehalfOf, amountIn);
}

function _repayToken(
uint256 amount,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to handle max repayment here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so retrieve the outstanding debt of the user, and repay all if amount is higher.

we must do the same thing for withdraw

address onBehalfOf
) internal returns (uint256) {
require(amount > 0, 'INSUFFICIENT_AMOUNT_TO_REPAY');

IERC20(TOKEN_IN).safeTransferFrom(msg.sender, address(this), amount);
uint256 amountWrapped = _wrapTokenIn(amount);
require(amountWrapped > 0, 'INSUFFICIENT_WRAPPED_TOKEN_RECEIVED');

SafeERC20.safeApprove(IERC20(TOKEN_OUT), address(POOL), amountWrapped);
POOL.repay(TOKEN_OUT, amountWrapped, 2, onBehalfOf);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
POOL.repay(TOKEN_OUT, amountWrapped, 2, onBehalfOf);
POOL.repay(TOKEN_OUT, amountWrapped, VARIABLE_INTEREST_RATE_MODE, onBehalfOf);


SafeERC20.safeApprove(IERC20(TOKEN_OUT), address(POOL), 0);
return amountWrapped;
}

/**
* @notice Helper to wrap an amount of tokenIn, receiving tokenOut
* @param amount The amount of tokenIn to wrap
Expand Down
24 changes: 24 additions & 0 deletions src/interfaces/IBaseTokenWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,30 @@ interface IBaseTokenWrapper {
PermitSignature calldata signature
) external;

/**
* @notice Repays token to the Pool, wraps it, and sends it to the pool for repayment
* @param amount The amount of token to repay
* @param onBehalfOf The address that will will repay the tokens
* @return The final amount repaied to the Pool, post-unwrapping
*/
function repayToken(
uint256 amount,
address onBehalfOf
) external returns (uint256);

/**
* @notice Repays token to the Pool, wraps it, and sends it to the pool for repayment
* @param amount The amount of token to repay
* @param onBehalfOf The address that will will repay the tokens
* @param signature The EIP-712 signature data used for permit
* @return The final amount repaied to the Pool, post-unwrapping
*/
function repayWithPermit(
uint256 amount,
address onBehalfOf,
PermitSignature calldata signature
) external returns (uint256);

/**
* @notice Provides way for the contract owner to rescue ERC-20 tokens
* @param token The address of the token to withdraw from this contract
Expand Down
252 changes: 250 additions & 2 deletions test/BaseTokenWrapper.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -878,11 +878,16 @@ abstract contract BaseTokenWrapperTest is Test {
}
}

function testFuzzBorrowToken(uint256 borrowAmount) public {
function testFuzzRepayToken(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are replacing testFuzzBorrowToken test?

uint256 borrowAmount,
uint256 repayAmount
) public {
borrowAmount = bound(borrowAmount, 1, MAX_DEAL_AMOUNT);
borrowAmount *= 10 ** tokenInDecimals;
uint256 collateralAmount = borrowAmount * 10;

repayAmount = bound(repayAmount, 1e6, borrowAmount);

address debtToken = IPool(pool)
.getReserveData(tokenWrapper.TOKEN_OUT())
.variableDebtTokenAddress;
Expand All @@ -905,7 +910,38 @@ abstract contract BaseTokenWrapperTest is Test {
uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount);
assertEq(
IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(alice)),
borrowedAmount
borrowedAmount,
'Borrowed amount mismatch'
);

vm.warp(block.timestamp + 1 days);

uint256 debtBefore = IERC20(debtToken).balanceOf(address(alice));
deal(tokenWrapper.TOKEN_IN(), alice, repayAmount);
IERC20(tokenWrapper.TOKEN_IN()).approve(
address(tokenWrapper),
repayAmount
);

uint256 actualRepaidAmount = tokenWrapper.repayToken(repayAmount, alice);

uint256 debtAfter = IERC20(debtToken).balanceOf(address(alice));
assertLt(debtAfter, debtBefore, 'Debt should decrease after repayment');

uint256 expectedRepaidAmount = tokenWrapper.getTokenOutForTokenIn(
repayAmount
);
assertApproxEqRel(
actualRepaidAmount,
expectedRepaidAmount,
0.01e18, // 1% tolerance
'Repaid amount should match expected amount'
);

assertLe(
IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(alice)),
borrowedAmount - repayAmount,
'Token balance after repayment is incorrect'
);
} else {
vm.expectRevert();
Expand All @@ -914,6 +950,218 @@ abstract contract BaseTokenWrapperTest is Test {
vm.stopPrank();
}

function testPartialRepayToken() public {
uint256 collateralAmount = 1000e18;
uint256 borrowAmount = 100e18;
uint256 partialRepayAmount = 60e18; // Repay 60% of the borrowed amount
deal(collateralAsset, ALICE, collateralAmount);

address debtToken = IPool(pool)
.getReserveData(tokenWrapper.TOKEN_OUT())
.variableDebtTokenAddress;
vm.startPrank(ALICE);

ICreditDelegationToken(debtToken).approveDelegation(
address(tokenWrapper),
borrowAmount
);

IERC20(collateralAsset).approve(address(pool), collateralAmount);
IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0);

if (borrowSupported) {
tokenWrapper.borrowToken(borrowAmount, 0);
uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount);
assertEq(
IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(ALICE)),
borrowedAmount
);

vm.warp(block.timestamp + 1 days);

uint256 underlyingBalanceBeforeRepayment = IERC20(tokenWrapper.TOKEN_IN())
.balanceOf(address(ALICE));

uint256 debtBeforeRepayment = IERC20(debtToken).balanceOf(address(ALICE));

IERC20(tokenWrapper.TOKEN_IN()).approve(
address(tokenWrapper),
partialRepayAmount
);

uint256 amountRepaid = tokenWrapper.repayToken(partialRepayAmount, ALICE);

uint256 underlyingBalanceAfterRepayment = IERC20(tokenWrapper.TOKEN_IN())
.balanceOf(address(ALICE));
uint256 debtAfterRepayment = IERC20(debtToken).balanceOf(address(ALICE));

assertApproxEqRel(
underlyingBalanceBeforeRepayment - underlyingBalanceAfterRepayment,
partialRepayAmount,
0.001e18 // 0.1% tolerance
);

assertApproxEqRel(
debtBeforeRepayment - debtAfterRepayment,
amountRepaid,
0.001e18 // 0.1% tolerance
);

assertTrue(debtAfterRepayment > 0, 'Debt should not be fully repaid');

assertApproxEqRel(
amountRepaid,
partialRepayAmount,
0.001e18 // 0.1% tolerance
);
}
vm.stopPrank();
}

function testRepayTokenWithPermit() public {
uint256 collateralAmount = 1000e18;
uint256 borrowAmount = 100e18;
deal(collateralAsset, ALICE, collateralAmount);

address debtToken = IPool(pool)
.getReserveData(tokenWrapper.TOKEN_OUT())
.variableDebtTokenAddress;
vm.startPrank(ALICE);

ICreditDelegationToken(debtToken).approveDelegation(
address(tokenWrapper),
borrowAmount
);

IERC20(collateralAsset).approve(address(pool), collateralAmount);
IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0);

if (borrowSupported) {
tokenWrapper.borrowToken(borrowAmount, 0);
uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount);
assertEq(
IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(ALICE)),
borrowedAmount
);
uint256 repayAmount = borrowedAmount;

uint256 underlyingBalanceBeforeRepayment = IERC20(tokenWrapper.TOKEN_IN())
.balanceOf(address(ALICE));

SigUtils.Permit memory permit = SigUtils.Permit({
owner: address(ALICE),
spender: address(tokenWrapper),
value: repayAmount,
nonce: IERC2612(tokenWrapper.TOKEN_IN()).nonces(ALICE),
deadline: block.timestamp + 1 days
});

bytes32 digest = SigUtils.getTypedDataHash(
permit,
IERC2612(tokenWrapper.TOKEN_IN()).DOMAIN_SEPARATOR()
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_KEY, digest);

IBaseTokenWrapper.PermitSignature memory signature = IBaseTokenWrapper
.PermitSignature({
deadline: block.timestamp + 1 days,
v: v,
r: r,
s: s
});

tokenWrapper.repayWithPermit(repayAmount, ALICE, signature);
vm.stopPrank();
assertEq(
IERC20(tokenWrapper.TOKEN_OUT()).balanceOf(ALICE),
underlyingBalanceBeforeRepayment - repayAmount
);
}
}

function testPartialRepayTokenWithPermit() public {
uint256 collateralAmount = 1000e18;
uint256 borrowAmount = 100e18;
uint256 partialRepayAmount = 60e18; // Repay 60% of the borrowed amount
deal(collateralAsset, ALICE, collateralAmount);

address debtToken = IPool(pool)
.getReserveData(tokenWrapper.TOKEN_OUT())
.variableDebtTokenAddress;
vm.startPrank(ALICE);

ICreditDelegationToken(debtToken).approveDelegation(
address(tokenWrapper),
borrowAmount
);

IERC20(collateralAsset).approve(address(pool), collateralAmount);
IPool(pool).supply(collateralAsset, collateralAmount, ALICE, 0);

if (borrowSupported) {
tokenWrapper.borrowToken(borrowAmount, 0);
uint256 borrowedAmount = tokenWrapper.getTokenInForTokenOut(borrowAmount);
assertEq(
IERC20(tokenWrapper.TOKEN_IN()).balanceOf(address(ALICE)),
borrowedAmount
);

vm.warp(block.timestamp + 1 days);

uint256 underlyingBalanceBeforeRepayment = IERC20(tokenWrapper.TOKEN_IN())
.balanceOf(address(ALICE));

uint256 debtBeforeRepayment = IERC20(debtToken).balanceOf(address(ALICE));

SigUtils.Permit memory permit = SigUtils.Permit({
owner: address(ALICE),
spender: address(tokenWrapper),
value: partialRepayAmount,
nonce: IERC2612(tokenWrapper.TOKEN_IN()).nonces(ALICE),
deadline: block.timestamp + 1 days
});

bytes32 digest = SigUtils.getTypedDataHash(
permit,
IERC2612(tokenWrapper.TOKEN_IN()).DOMAIN_SEPARATOR()
);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(ALICE_KEY, digest);

IBaseTokenWrapper.PermitSignature memory signature = IBaseTokenWrapper
.PermitSignature({
deadline: block.timestamp + 1 days,
v: v,
r: r,
s: s
});

tokenWrapper.repayWithPermit(partialRepayAmount, ALICE, signature);

uint256 underlyingBalanceAfterRepayment = IERC20(tokenWrapper.TOKEN_IN())
.balanceOf(address(ALICE));
uint256 debtAfterRepayment = IERC20(debtToken).balanceOf(address(ALICE));

assertApproxEqRel(
underlyingBalanceBeforeRepayment - underlyingBalanceAfterRepayment,
partialRepayAmount,
0.001e18 // 0.1% tolerance
);

assertApproxEqRel(
debtBeforeRepayment - debtAfterRepayment,
partialRepayAmount,
0.001e18 // 0.1% tolerance
);
assertTrue(debtAfterRepayment > 0, 'Debt should not be fully repaid');
}
vm.stopPrank();
}

function testRepayTokenZeroAmount() public {
vm.expectRevert('INSUFFICIENT_AMOUNT_TO_REPAY');
tokenWrapper.repayToken(0, address(this));
}

function _signCreditDelegation(
uint256 privateKey,
address delegatee,
Expand Down