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

Add settleAndRefund #2

Merged
merged 5 commits into from
Mar 21, 2024
Merged
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
2 changes: 1 addition & 1 deletion .forge-snapshots/VaultTest#Vault.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6762
7075
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
122540
2 changes: 1 addition & 1 deletion .forge-snapshots/VaultTest#registerPoolManager.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
24484
24506
2 changes: 1 addition & 1 deletion .forge-snapshots/VaultTest#testLock_NoOp.snap
Original file line number Diff line number Diff line change
@@ -1 +1 @@
11502
11692
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
56176
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
36514
22 changes: 22 additions & 0 deletions src/Vault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,28 @@ contract Vault is IVault, VaultToken, Ownable {
SettlementGuard.accountDelta(msg.sender, currency, -(paid.toInt128()));
}

function settleAndRefund(Currency currency, address to)
external
payable
override
isLocked
returns (uint256 paid, uint256 refund)
{
paid = currency.balanceOfSelf() - reservesOfVault[currency];
int256 currentDelta = SettlementGuard.getCurrencyDelta(msg.sender, currency);

if (currentDelta >= 0 && paid > currentDelta.toUint256()) {
// msg.sender owes vault but paid more than than whats owed
refund = paid - currentDelta.toUint256();
paid = currentDelta.toUint256();
}

reservesOfVault[currency] += paid;
SettlementGuard.accountDelta(msg.sender, currency, -(paid.toInt128()));

if (refund > 0) currency.transfer(to, refund);
}

/// @inheritdoc IVault
function settleFor(Currency currency, address target, uint256 amount) external isLocked {
/// @notice settle all outstanding debt if amount is 0
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ interface IVault is IVaultToken {
/// @notice Called by the user to pay what is owed
function settle(Currency token) external payable returns (uint256 paid);

/// @notice Called by the user to pay what is owed. If the payment is more than the debt, the surplus is refunded
/// @param currency The currency to settle
/// @param to The address to refund the surplus to
/// @return paid The amount paid
/// @return refund The amount refunded
function settleAndRefund(Currency currency, address to) external payable returns (uint256 paid, uint256 refund);

/// @notice move the delta from target to the msg.sender, only payment delta can be moved
/// @param currency The currency to settle
/// @param target The address whose delta will be updated
Expand Down
12 changes: 12 additions & 0 deletions test/vault/FakePoolManagerRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ contract FakePoolManagerRouter {
// settle ETH
vault.settle{value: 5 ether}(CurrencyLibrary.NATIVE);
vault.take(CurrencyLibrary.NATIVE, address(this), 5 ether);
} else if (data[0] == 0x18) {
// call this method via vault.lock(abi.encodePacked(hex"18", alice));
address to = address(uint160(uint256(bytes32(data[1:0x15]) >> 96)));
vault.settleAndRefund(poolKey.currency0, to);
vault.settleAndRefund(poolKey.currency1, to);
} else if (data[0] == 0x19) {
poolManager.mockAccounting(poolKey, 3 ether, -3 ether);
vault.settle(poolKey.currency0);

/// try to call settleAndRefund should not revert
vault.settleAndRefund(poolKey.currency1, address(this));
vault.take(poolKey.currency1, address(this), 3 ether);
}

return "";
Expand Down
51 changes: 50 additions & 1 deletion test/vault/Vault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,55 @@ contract VaultTest is Test, GasSnapshot {
vault.lock(hex"02");
}

function testSettleAndRefund_WithErc20Transfer() public {
address alice = makeAddr("alice");

// simulate someone transferred token to vault
currency0.transfer(address(vault), 10 ether);
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(fakePoolManagerRouter)), 0 ether);
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)), 0 ether);

// settle and refund
vm.prank(address(fakePoolManagerRouter));
snapStart("VaultTest#testSettleAndRefund_WithErc20Transfer");
vault.lock(abi.encodePacked(hex"18", alice));
snapEnd();

// verify
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(fakePoolManagerRouter)), 0 ether);
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)), 10 ether);
}

function testSettleAndRefund_WithoutErc20Transfer() public {
address alice = makeAddr("alice");

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(fakePoolManagerRouter)), 0 ether);
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)), 0 ether);

// settleAndRefund works even if there's no excess currency
vm.prank(address(fakePoolManagerRouter));
snapStart("VaultTest#testSettleAndRefund_WithoutErc20Transfer");
vault.lock(abi.encodePacked(hex"18", alice));
snapEnd();

// verify
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(fakePoolManagerRouter)), 0 ether);
assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(alice)), 0 ether);
}

function testSettleAndRefund_NegativeBalanceDelta() public {
// pre-req: ensure vault has some value in reserveOfVault[] before
currency0.transfer(address(vault), 10 ether);
currency1.transfer(address(vault), 10 ether);
vm.prank(address(fakePoolManagerRouter));
vault.lock(hex"02");

// settleAndRefund should not revert even if negative balanceDelta
currency0.transfer(address(vault), 3 ether);
vm.prank(address(fakePoolManagerRouter));
vault.lock(hex"19");
}

function testNotCorrectPoolManager() public {
// router => vault.lock
// vault.lock => periphery.lockAcquired
Expand Down Expand Up @@ -208,7 +257,7 @@ contract VaultTest is Test, GasSnapshot {
vm.prank(address(fakePoolManagerRouter));
snapStart("VaultTest#lockSettledWhenAddLiquidity");
vault.lock(hex"02");
snapStart("end");
snapEnd();

assertEq(IERC20(Currency.unwrap(currency0)).balanceOf(address(vault)), 10 ether);
assertEq(IERC20(Currency.unwrap(currency1)).balanceOf(address(vault)), 10 ether);
Expand Down
28 changes: 27 additions & 1 deletion test/vault/VaultInvariant.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ contract VaultPoolManager is Test {
enum ActionType {
Take,
Settle,
SettleAndRefund,
SettleFor,
Mint,
Burn
Expand Down Expand Up @@ -84,6 +85,21 @@ contract VaultPoolManager is Test {
vault.lock(abi.encode(Action(ActionType.Settle, uint128(amt0), uint128(amt1))));
}

/// @dev In settleAndRefund case, assume user add liquidity and paying to the vault
/// but theres another folk who minted extra token to the vault
function settleAndRefund(uint256 amt0, uint256 amt1, bool sendToVault) public {
amt0 = bound(amt0, 0, MAX_TOKEN_BALANCE - 1 ether);
amt1 = bound(amt1, 0, MAX_TOKEN_BALANCE - 1 ether);

// someone send some token directly to vault
if (sendToVault) token0.mint(address(vault), 1 ether);

// mint token to VaultPoolManager, so VaultPoolManager can pay to the vault
token0.mint(address(this), amt0);
token1.mint(address(this), amt1);
vault.lock(abi.encode(Action(ActionType.SettleAndRefund, uint128(amt0), uint128(amt1))));
}

/// @dev In settleFor case, assume user is paying for hook
function settleFor(uint256 amt0, uint256 amt1) public {
amt0 = bound(amt0, 0, MAX_TOKEN_BALANCE);
Expand Down Expand Up @@ -165,6 +181,15 @@ contract VaultPoolManager is Test {

vault.settle(currency0);
vault.settle(currency1);
} else if (action.actionType == ActionType.SettleAndRefund) {
BalanceDelta delta = toBalanceDelta(int128(action.amt0), int128(action.amt1));
vault.accountPoolBalanceDelta(poolKey, delta, address(this));

token0.transfer(address(vault), action.amt0);
token1.transfer(address(vault), action.amt1);

vault.settleAndRefund(currency0, address(this));
vault.settleAndRefund(currency1, address(this));
} else if (action.actionType == ActionType.SettleFor) {
// hook cash out the fee ahead
BalanceDelta delta = toBalanceDelta(int128(action.amt0), int128(action.amt1));
Expand Down Expand Up @@ -212,13 +237,14 @@ contract VaultInvariant is Test, GasSnapshot {
// Only call vaultPoolManager, otherwise all other contracts deployed in setUp will be called
targetContract(address(vaultPoolManager));

bytes4[] memory selectors = new bytes4[](6);
bytes4[] memory selectors = new bytes4[](7);
selectors[0] = VaultPoolManager.take.selector;
selectors[1] = VaultPoolManager.mint.selector;
selectors[2] = VaultPoolManager.settle.selector;
selectors[3] = VaultPoolManager.burn.selector;
selectors[4] = VaultPoolManager.settleFor.selector;
selectors[5] = VaultPoolManager.collectFee.selector;
selectors[6] = VaultPoolManager.settleAndRefund.selector;
targetSelector(FuzzSelector({addr: address(vaultPoolManager), selectors: selectors}));
}

Expand Down
Loading