Skip to content

Commit

Permalink
✨ contracts: charge keeper fee on collect credit
Browse files Browse the repository at this point in the history
  • Loading branch information
itofarina committed Nov 6, 2024
1 parent 1eec7c5 commit 81765c6
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 57 deletions.
111 changes: 56 additions & 55 deletions contracts/.gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,60 +1,61 @@
ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3158124, ~: 3034600)
ExaPluginTest:test_collectCredit_collects() (gas: 882389)
ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 960460)
ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 337950)
ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 333516)
ExaPluginTest:test_collectCredit_reverts_whenPrposalCausesInsufficientLiquidity() (gas: 960633)
ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 804248)
ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 330145)
ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 765110)
ExaPluginTest:test_collectDebit_collects() (gas: 615934)
ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 808293)
ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 337760)
ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 333279)
ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 805306)
ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 654930)
ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 329933)
ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 615989)
ExaPluginTest:test_collectInstallments_collects() (gas: 1293972)
ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 338811)
ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 335652)
ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1025631)
ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 332257)
ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1150682)
ExaPluginTest:test_crossRepay_lifi() (gas: 15773348)
ExaPluginTest:test_onUninstall_uninstalls() (gas: 207493)
ExaPluginTest:test_poke() (gas: 291515)
ExaPluginTest:test_pokeETH_deposits() (gas: 362169)
ExaPluginTest:test_propose_emitsProposed() (gas: 207648)
ExaAccountFactoryTest:testFuzz_createAccount_EOAOwners(uint256,address[63]) (runs: 256, μ: 3098322, ~: 2951493)
ExaPluginTest:test_collectCredit_collects() (gas: 910344)
ExaPluginTest:test_collectCredit_passes_whenProposalLeavesEnoughLiquidity() (gas: 988371)
ExaPluginTest:test_collectCredit_reverts_asNotKeeper() (gas: 337884)
ExaPluginTest:test_collectCredit_reverts_whenExpired() (gas: 333427)
ExaPluginTest:test_collectCredit_reverts_whenPrposalCausesInsufficientLiquidity() (gas: 988542)
ExaPluginTest:test_collectCredit_reverts_whenReplay() (gas: 832114)
ExaPluginTest:test_collectCredit_reverts_whenTimelocked() (gas: 330034)
ExaPluginTest:test_collectCredit_toleratesTimeDrift() (gas: 793065)
ExaPluginTest:test_collectDebit_collects() (gas: 615890)
ExaPluginTest:test_collectDebit_collects_whenProposalLeavesEnoughLiquidity() (gas: 808205)
ExaPluginTest:test_collectDebit_reverts_asNotKeeper() (gas: 337694)
ExaPluginTest:test_collectDebit_reverts_whenExpired() (gas: 333257)
ExaPluginTest:test_collectDebit_reverts_whenProposalCausesInsufficientLiquidity() (gas: 805218)
ExaPluginTest:test_collectDebit_reverts_whenReplay() (gas: 654864)
ExaPluginTest:test_collectDebit_reverts_whenTimelocked() (gas: 329889)
ExaPluginTest:test_collectDebit_toleratesTimeDrift() (gas: 615945)
ExaPluginTest:test_collectInstallments_collects() (gas: 1293906)
ExaPluginTest:test_collectInstallments_reverts_asNotKeeper() (gas: 338745)
ExaPluginTest:test_collectInstallments_reverts_whenExpired() (gas: 335608)
ExaPluginTest:test_collectInstallments_reverts_whenReplay() (gas: 1025521)
ExaPluginTest:test_collectInstallments_reverts_whenTimelocked() (gas: 332191)
ExaPluginTest:test_collectInstallments_toleratesTimeDrift() (gas: 1150616)
ExaPluginTest:test_crossRepay_lifi() (gas: 15920853)
ExaPluginTest:test_fuzz_repayCredit_paysKeeperFee(uint256,uint256) (runs: 256, μ: 1179797, ~: 1184150)
ExaPluginTest:test_onUninstall_uninstalls() (gas: 207521)
ExaPluginTest:test_poke() (gas: 291493)
ExaPluginTest:test_pokeETH_deposits() (gas: 362125)
ExaPluginTest:test_propose_emitsProposed() (gas: 207626)
ExaPluginTest:test_refund_refunds() (gas: 360757)
ExaPluginTest:test_repay_partiallyRepays() (gas: 1660857)
ExaPluginTest:test_repay_repays() (gas: 1092371)
ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40404)
ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32126)
ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33695)
ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39683)
ExaPluginTest:test_setKeeperRateModel_emitsKeeperRateModelSet() (gas: 40405)
ExaPluginTest:test_setKeeperRateModel_reverts_whenAddressZero() (gas: 32060)
ExaPluginTest:test_setKeeperRateModel_reverts_whenNotAdmin() (gas: 33687)
ExaPluginTest:test_setKeeperRateModel_sets_whenAdmin() (gas: 39651)
ExaPluginTest:test_setKeeper_emitsKeeperSet() (gas: 40383)
ExaPluginTest:test_setKeeper_reverts_whenAddressZero() (gas: 32117)
ExaPluginTest:test_setKeeper_reverts_whenNotAdmin() (gas: 33697)
ExaPluginTest:test_setKeeper_sets_whenAdmin() (gas: 39748)
ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 812510)
ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 381851)
ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 333193)
ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 330278)
ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 576708)
ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 287478)
ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 289180)
ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 288494)
ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 289147)
ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 288127)
ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 773488)
ExaPluginTest:test_withdraw_transfersAsset_asOwner() (gas: 772350)
ExaPluginTest:test_repay_partiallyRepays() (gas: 1759959)
ExaPluginTest:test_repay_repays() (gas: 1169469)
ExaPluginTest:test_setCollector_emitsCollectorSet() (gas: 40426)
ExaPluginTest:test_setCollector_reverts_whenAddressZero() (gas: 32148)
ExaPluginTest:test_setCollector_reverts_whenNotAdmin() (gas: 33672)
ExaPluginTest:test_setCollector_sets_whenAdmin() (gas: 39727)
ExaPluginTest:test_setKeeperRateModel_emitsKeeperRateModelSet() (gas: 40427)
ExaPluginTest:test_setKeeperRateModel_reverts_whenAddressZero() (gas: 32082)
ExaPluginTest:test_setKeeperRateModel_reverts_whenNotAdmin() (gas: 33664)
ExaPluginTest:test_setKeeperRateModel_sets_whenAdmin() (gas: 39695)
ExaPluginTest:test_setKeeper_emitsKeeperSet() (gas: 40405)
ExaPluginTest:test_setKeeper_reverts_whenAddressZero() (gas: 32139)
ExaPluginTest:test_setKeeper_reverts_whenNotAdmin() (gas: 33674)
ExaPluginTest:test_setKeeper_sets_whenAdmin() (gas: 39792)
ExaPluginTest:test_withdrawWETH_transfersETH() (gas: 812400)
ExaPluginTest:test_withdraw_reverts_whenNoProposal() (gas: 381829)
ExaPluginTest:test_withdraw_reverts_whenNoProposalKeeper() (gas: 333127)
ExaPluginTest:test_withdraw_reverts_whenNotKeeper() (gas: 330212)
ExaPluginTest:test_withdraw_reverts_whenReceiverIsContractAndMarketNotWETH() (gas: 576664)
ExaPluginTest:test_withdraw_reverts_whenTimelocked() (gas: 287434)
ExaPluginTest:test_withdraw_reverts_whenTimelockedKeeper() (gas: 289092)
ExaPluginTest:test_withdraw_reverts_whenWrongAmount() (gas: 288472)
ExaPluginTest:test_withdraw_reverts_whenWrongMarket() (gas: 289103)
ExaPluginTest:test_withdraw_reverts_whenWrongReceiver() (gas: 288105)
ExaPluginTest:test_withdraw_transfersAsset_asKeeper() (gas: 773400)
ExaPluginTest:test_withdraw_transfersAsset_asOwner() (gas: 772306)
InstallmentsPreviewerTest:test_preview_returns() (gas: 134970)
KeeperRateModelTest:test_fuzz_refRate(uint256,uint256[],uint256) (runs: 256, μ: 3172732, ~: 3173071)
KeeperRateModelTest:test_fuzz_refRate(uint256,uint256[],uint256) (runs: 256, μ: 3172809, ~: 3173098)
KeeperRateModelTest:test_invalidRangeDeploy_reverts() (gas: 3440478)
RefunderTest:test_refund_refunds() (gas: 242057)
RefunderTest:test_refund_reverts_whenExpired() (gas: 68949)
Expand Down
19 changes: 17 additions & 2 deletions contracts/src/ExaPlugin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {
address public keeper;
KeeperRateModel public keeperRateModel;
mapping(address account => Proposal lastProposal) public proposals;
mapping(address account => mapping(uint256 maturity => uint256 fee)) public keeperFees;

bytes32 private callHash;

Expand Down Expand Up @@ -118,11 +119,14 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {
uint256 maxRepay;
(positionAssets, maxRepay) = _previewRepay(maturity);

uint256 amount = maxRepay.min(EXA_USDC.maxWithdraw(msg.sender));
uint256 keeperFee = keeperFees[msg.sender][maturity];
uint256 amount = maxRepay.min(EXA_USDC.maxWithdraw(msg.sender) - keeperFee);
positionAssets = positionAssets.min(EXA_USDC.maxWithdraw(msg.sender));

IPluginExecutor(msg.sender).executeFromPluginExternal(
address(EXA_USDC), 0, abi.encodeCall(IERC20.approve, (address(this), EXA_USDC.previewWithdraw(amount)))
address(EXA_USDC),
0,
abi.encodeCall(IERC20.approve, (address(this), EXA_USDC.previewWithdraw(amount) + keeperFee))
);
_flashLoan(
amount,
Expand All @@ -131,6 +135,7 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {
borrower: msg.sender,
positionAssets: positionAssets.min(EXA_USDC.maxWithdraw(msg.sender)),
maxRepay: amount,
keeperFee: keeperFee,
marketIn: IMarket(address(0)),
amountIn: 0,
route: ""
Expand All @@ -157,6 +162,7 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {
positionAssets: positionAssets,
maxRepay: maxRepay,
marketIn: collateral,
keeperFee: 0,
amountIn: amountIn,
route: route
})
Expand All @@ -167,11 +173,16 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {
external
onlyIssuer(amount, timestamp, signature)
{
uint256 duration = maturity - block.timestamp;
keeperFees[msg.sender][maturity] +=
amount.mulWad(keeperRateModel.rate(duration.divWad(365 days)).mulDiv(duration, 365 days));

IPluginExecutor(msg.sender).executeFromPluginExternal(
address(EXA_USDC),
0,
abi.encodeCall(IMarket.borrowAtMaturity, (maturity, amount, type(uint256).max, collector, msg.sender)) // TODO slippage control
);

_checkLiquidity(msg.sender);
}

Expand Down Expand Up @@ -471,13 +482,16 @@ contract ExaPlugin is AccessControl, BasePlugin, IExaAccount {

uint256 actualRepay;
if (b.amountIn == 0) {
keeperFees[b.borrower][b.maturity] -= b.keeperFee;
actualRepay = EXA_USDC.repayAtMaturity(b.maturity, b.positionAssets, b.maxRepay, b.borrower).min(
EXA_USDC.maxWithdraw(b.borrower)
);

if (actualRepay < b.maxRepay) EXA_USDC.deposit(b.maxRepay - actualRepay, b.borrower);

EXA_USDC.withdraw(b.maxRepay, address(BALANCER_VAULT), b.borrower);
// slither-disable-next-line arbitrary-send-erc20
IERC20(EXA_USDC).safeTransferFrom(b.borrower, keeper, b.keeperFee);
} else {
actualRepay = EXA_USDC.repayAtMaturity(b.maturity, b.positionAssets, b.maxRepay, b.borrower);

Expand Down Expand Up @@ -605,6 +619,7 @@ struct BalancerCallbackData {
address borrower;
uint256 positionAssets;
uint256 maxRepay;
uint256 keeperFee;
IMarket marketIn;
uint256 amountIn;
bytes route;
Expand Down
20 changes: 20 additions & 0 deletions contracts/test/ExaPlugin.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -865,6 +865,26 @@ contract ExaPluginTest is ForkTest {
assertEq(usdc.balanceOf(address(exaPlugin)), 0, "usdc dust");
}

function test_fuzz_repayCredit_paysKeeperFee(uint256 maturity, uint256 amount) external {
maturity = _bound(maturity, FixedLib.INTERVAL, FixedLib.INTERVAL * 7);
maturity = maturity - maturity % FixedLib.INTERVAL;
amount = _bound(amount, 1, type(uint32).max);
vm.startPrank(keeper);
account.poke(exaUSDC);
account.collectCredit(maturity, amount, block.timestamp, _issuerOp(amount, block.timestamp));
vm.stopPrank();

uint256 duration = maturity - block.timestamp;
uint256 keeperFee =
amount.mulWad(exaPlugin.keeperRateModel().rate(duration.divWad(365 days)).mulDiv(duration, 365 days));

assertEq(usdc.balanceOf(keeper), 0);
vm.prank(owner);
account.execute(address(account), 0, abi.encodeCall(IExaAccount.repay, (maturity)));
assertEq(usdc.balanceOf(address(exaPlugin)), 0, "usdc dust");
assertEq(exaUSDC.balanceOf(keeper), keeperFee, "keeper fee not paid");
}

// solhint-enable func-name-mixedcase

function _op(bytes memory callData, uint256 privateKey) internal view returns (UserOperation memory op) {
Expand Down

0 comments on commit 81765c6

Please sign in to comment.