Skip to content

Commit 6f3d4bd

Browse files
committed
feat: aerodrome swap fees first pass
1 parent eebe37d commit 6f3d4bd

9 files changed

Lines changed: 249 additions & 3 deletions

File tree

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "packages/libs/contracts-sdk/lib/forge-std"]
22
path = packages/libs/contracts-sdk/lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4+
[submodule "packages/libs/contracts-sdk/lib/contracts"]
5+
path = packages/libs/contracts-sdk/lib/contracts
6+
url = https://github.com/aerodrome-finance/contracts

packages/libs/contracts-sdk/contracts/fees/Fee.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ contract Fee {
4242

4343
// default to 10% performance fee
4444
LibFeeStorage.getStorage().performanceFeePercentage = 1000;
45+
46+
// default to 0.25% swap fee
47+
LibFeeStorage.getStorage().swapFeePercentage = 25;
4548
}
4649

4750
/**

packages/libs/contracts-sdk/contracts/fees/LibFeeStorage.sol

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ library LibFeeStorage {
2121
// performance fee percentage, expressed in basis points
2222
// so 1000 = 10%. multiply percentage by 100 to get basis points
2323
uint256 performanceFeePercentage;
24+
// aerodrome swap fee percentage, expressed in basis points
25+
// so 25 = 0.25%. multiply percentage by 100 to get basis points
26+
uint256 swapFeePercentage;
2427
// set of tokens that have collected fees
2528
// used to track which tokens have collected fees
2629
// so we know where to look for collected fees
2730
EnumerableSet.AddressSet tokensWithCollectedFees;
2831
// aave pool contract address for this chain
2932
address aavePool;
33+
// aerdrome router contract address for this chain
34+
address aerodromeRouter;
3035
// maps user address to a set of vault or pool asset addresses
3136
// this means the user has deposited into this vault or pool
3237
// and if the vincent app disappears, the user can grab this set
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.29;
3+
4+
import "../LibFeeStorage.sol";
5+
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
6+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import { EnumerableSet } from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol";
8+
import { IRouter } from "@aerodrome/contracts/interfaces/IRouter.sol";
9+
10+
/**
11+
* @title AerodromeSwapFeeFacet
12+
* @notice A facet of the Fee Diamond that manages Aerodrome swap fees
13+
*/
14+
contract AerodromeSwapFeeFacet {
15+
using EnumerableSet for EnumerableSet.AddressSet;
16+
17+
function swapExactTokensForTokensOnAerodrome(
18+
uint256 amountIn,
19+
uint256 amountOutMin,
20+
IRouter.Route[] calldata routes,
21+
address to,
22+
uint256 deadline
23+
) external returns (uint256[] memory amounts) {
24+
// first, transfer the amountIn to this contract
25+
IERC20(routes[0].from).transferFrom(msg.sender, address(this), amountIn);
26+
27+
// calculate the fee from amountIn
28+
uint256 fee = amountIn * LibFeeStorage.getStorage().swapFeePercentage / 10000;
29+
// subtract the fee from amountIn
30+
amountIn -= fee;
31+
32+
// reduce amountOutMin by the same percentage since we're taking
33+
// the fee from the amountIn and amountOutMin depends on it
34+
amountOutMin -= amountOutMin * LibFeeStorage.getStorage().swapFeePercentage / 10000;
35+
36+
// approve the router to spend the amountIn
37+
IERC20(routes[0].from).approve(LibFeeStorage.getStorage().aerodromeRouter, amountIn);
38+
39+
// swap the tokens
40+
amounts = IRouter(LibFeeStorage.getStorage().aerodromeRouter).swapExactTokensForTokens(amountIn, amountOutMin, routes, address(this), deadline);
41+
42+
// add the input token to the collected fees list
43+
LibFeeStorage.getStorage().tokensWithCollectedFees.add(routes[0].from);
44+
45+
// transfer the tokens to the recipient
46+
IERC20(routes[routes.length - 1].to).transfer(to, amounts[amounts.length - 1]);
47+
48+
// return the amounts just like aerodrome
49+
return amounts;
50+
}
51+
52+
}

packages/libs/contracts-sdk/contracts/fees/facets/FeeAdminFacet.sol

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ contract FeeAdminFacet {
3232
return LibFeeStorage.getStorage().performanceFeePercentage;
3333
}
3434

35+
/**
36+
* @notice Gets the swap fee percentage
37+
* @return the swap fee percentage in basis points
38+
* so 25 = 0.25%. multiply percentage by 100 to get basis points
39+
*/
40+
function swapFeePercentage() external view returns (uint256) {
41+
return LibFeeStorage.getStorage().swapFeePercentage;
42+
}
43+
3544
/**
3645
* @notice Gets the entire list of tokens that have collected fees
3746
* if this list gets too long and the view call is timing out,
@@ -67,6 +76,14 @@ contract FeeAdminFacet {
6776
return LibFeeStorage.getStorage().aavePool;
6877
}
6978

79+
/**
80+
* @notice Gets the aerodrome router contract address for this chain
81+
* @return the aerodrome router contract address for this chain
82+
*/
83+
function aerodromeRouter() external view returns (address) {
84+
return LibFeeStorage.getStorage().aerodromeRouter;
85+
}
86+
7087

7188
/* ========== MUTATIVE FUNCTIONS ========== */
7289

@@ -100,6 +117,17 @@ contract FeeAdminFacet {
100117
LibFeeStorage.getStorage().performanceFeePercentage = newPerformanceFeePercentage;
101118
}
102119

120+
/**
121+
* @notice Sets the swap fee percentage
122+
* @param newSwapFeePercentage the new swap fee percentage
123+
* in basis points
124+
* so 25 = 0.25%. multiply percentage by 100 to get basis points
125+
* @dev this can only be called by the owner
126+
*/
127+
function setSwapFeePercentage(uint256 newSwapFeePercentage) onlyOwner external {
128+
LibFeeStorage.getStorage().swapFeePercentage = newSwapFeePercentage;
129+
}
130+
103131
/**
104132
* @notice Sets the aave pool contract address for this chain
105133
* @param newAavePool the new aave pool contract address for this chain
@@ -108,4 +136,13 @@ contract FeeAdminFacet {
108136
function setAavePool(address newAavePool) onlyOwner external {
109137
LibFeeStorage.getStorage().aavePool = newAavePool;
110138
}
139+
140+
/**
141+
* @notice Sets the aerodrome router contract address for this chain
142+
* @param newAerodromeRouter the new aerodrome router contract address for this chain
143+
* @dev this can only be called by the owner
144+
*/
145+
function setAerodromeRouter(address newAerodromeRouter) onlyOwner external {
146+
LibFeeStorage.getStorage().aerodromeRouter = newAerodromeRouter;
147+
}
111148
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[profile.default]
2-
src = "src"
2+
src = "contracts"
33
out = "out"
44
libs = ["node_modules", "lib"]
55
solc_version = "0.8.29"
66
evm_version = "prague"
77
bytecode_hash = "none"
88
cbor_metadata = false
99

10-
remappings = ["forge-std/=lib/forge-std/src/", "@aave-dao/aave-v3-origin/=node_modules/@aave-dao/aave-v3-origin/"]
10+
remappings = ["forge-std/=lib/forge-std/src/", "@aave-dao/aave-v3-origin/=node_modules/@aave-dao/aave-v3-origin/", "@aerodrome/=lib/contracts/"]
1111
via_ir = true
1212

1313
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
Submodule contracts added at a5fae2e

packages/libs/contracts-sdk/script/DeployFeeDiamond.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import "../contracts/fees/facets/FeeViewsFacet.sol";
1010
import "../contracts/fees/facets/FeeAdminFacet.sol";
1111
import "../contracts/fees/facets/MorphoPerfFeeFacet.sol";
1212
import "../contracts/fees/facets/AavePerfFeeFacet.sol";
13+
import "../contracts/fees/facets/AerodromeSwapFeeFacet.sol";
1314

1415
import "../contracts/diamond-base/interfaces/IDiamondCut.sol";
1516
import "../contracts/diamond-base/interfaces/IDiamondLoupe.sol";
@@ -68,7 +69,7 @@ contract DeployFeeDiamond is Script {
6869
vm.startBroadcast(deployerPrivateKey);
6970

7071
// Deploy the facets
71-
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](6);
72+
IDiamondCut.FacetCut[] memory cuts = new IDiamondCut.FacetCut[](7);
7273

7374
// core diamond lib facets
7475
DiamondLoupeFacet diamondLoupeFacet = new DiamondLoupeFacet{salt: create2Salt}();
@@ -85,6 +86,8 @@ contract DeployFeeDiamond is Script {
8586
cuts[4] = contractToFacetCutAdd("MorphoPerfFeeFacet", address(morphoPerfFeeFacet));
8687
AavePerfFeeFacet aavePerfFeeFacet = new AavePerfFeeFacet{salt: create2Salt}();
8788
cuts[5] = contractToFacetCutAdd("AavePerfFeeFacet", address(aavePerfFeeFacet));
89+
AerodromeSwapFeeFacet aerodromeSwapFeeFacet = new AerodromeSwapFeeFacet{salt: create2Salt}();
90+
cuts[6] = contractToFacetCutAdd("AerodromeSwapFeeFacet", address(aerodromeSwapFeeFacet));
8891

8992
// Deploy the Diamond with the diamondCut facet and all other facets in one transaction
9093
Fee diamond = new Fee{
@@ -102,6 +105,7 @@ contract DeployFeeDiamond is Script {
102105
console.log("FeeAdminFacet:", address(feeAdminFacet));
103106
console.log("MorphoPerfFeeFacet:", address(morphoPerfFeeFacet));
104107
console.log("AavePerfFeeFacet:", address(aavePerfFeeFacet));
108+
console.log("AerodromeSwapFeeFacet:", address(aerodromeSwapFeeFacet));
105109

106110
return address(diamond);
107111
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.29;
3+
4+
import "forge-std/Test.sol";
5+
import "forge-std/console.sol";
6+
7+
import {DeployFeeDiamond} from "../../script/DeployFeeDiamond.sol";
8+
9+
import {Fee} from "../../contracts/fees/Fee.sol";
10+
import {FeeViewsFacet} from "../../contracts/fees/facets/FeeViewsFacet.sol";
11+
import {FeeAdminFacet} from "../../contracts/fees/facets/FeeAdminFacet.sol";
12+
import {AerodromeSwapFeeFacet} from "../../contracts/fees/facets/AerodromeSwapFeeFacet.sol";
13+
import {LibFeeStorage} from "../../contracts/fees/LibFeeStorage.sol";
14+
import {FeeUtils} from "../../contracts/fees/FeeUtils.sol";
15+
import {OwnershipFacet} from "../../contracts/diamond-base/facets/OwnershipFacet.sol";
16+
17+
import {USDC} from "../ABIs/USDC.sol";
18+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
19+
import { IRouter } from "@aerodrome/contracts/interfaces/IRouter.sol";
20+
21+
contract FeeForkTest is Test {
22+
address owner;
23+
address APP_USER_ALICE = makeAddr("Alice");
24+
// real aerodrome router on base from https://www.aerodrome.finance/security
25+
address REAL_AERODROME_ROUTER = 0xcF77a3Ba9A5CA399B7c97c74d54e5b1Beb874E43;
26+
// real USDC address on base
27+
address REAL_USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
28+
// real USDC master_minter
29+
address REAL_USDC_MASTER_MINTER;
30+
address USDC_MINTER = makeAddr("USDCMinter");
31+
// real WETH address on base
32+
address REAL_WETH = 0x4200000000000000000000000000000000000006;
33+
34+
Fee public feeDiamond;
35+
FeeViewsFacet public feeViewsFacet;
36+
FeeAdminFacet public feeAdminFacet;
37+
AerodromeSwapFeeFacet public aerodromeSwapFeeFacet;
38+
OwnershipFacet public ownershipFacet;
39+
40+
USDC public USDCErc20;
41+
ERC20 public WETHErc20;
42+
IRouter public aerodromeRouter;
43+
uint256 public erc20Decimals;
44+
45+
function setUp() public {
46+
uint256 deployerPrivateKey = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80;
47+
vm.setEnv("VINCENT_DEPLOYER_PRIVATE_KEY", vm.toString(deployerPrivateKey));
48+
owner = vm.addr(deployerPrivateKey);
49+
50+
DeployFeeDiamond deployScript = new DeployFeeDiamond();
51+
52+
address diamondAddress = deployScript.deployToNetwork("test", keccak256("testSalt"));
53+
feeDiamond = Fee(payable(diamondAddress));
54+
55+
feeViewsFacet = FeeViewsFacet(diamondAddress);
56+
feeAdminFacet = FeeAdminFacet(diamondAddress);
57+
aerodromeSwapFeeFacet = AerodromeSwapFeeFacet(diamondAddress);
58+
ownershipFacet = OwnershipFacet(diamondAddress);
59+
60+
// set the aave pool address in the fee diamond
61+
vm.startPrank(owner);
62+
feeAdminFacet.setAerodromeRouter(REAL_AERODROME_ROUTER);
63+
vm.stopPrank();
64+
65+
// set up the real aave pool and USDC token
66+
aerodromeRouter = IRouter(REAL_AERODROME_ROUTER);
67+
USDCErc20 = USDC(REAL_USDC);
68+
WETHErc20 = ERC20(REAL_WETH);
69+
REAL_USDC_MASTER_MINTER = USDCErc20.masterMinter();
70+
// configure the USDC minter
71+
vm.prank(REAL_USDC_MASTER_MINTER);
72+
USDCErc20.configureMinter(USDC_MINTER, type(uint256).max);
73+
vm.stopPrank();
74+
erc20Decimals = USDCErc20.decimals();
75+
console.log("setUp complete");
76+
}
77+
78+
function testSingleRouteSwap() public {
79+
uint256 swapAmount = 50 * 10 ** erc20Decimals;
80+
81+
// mint the USDC to the user
82+
vm.startPrank(USDC_MINTER);
83+
USDCErc20.mint(APP_USER_ALICE, swapAmount);
84+
vm.stopPrank();
85+
console.log("minted USDC to user");
86+
87+
vm.startPrank(APP_USER_ALICE);
88+
USDCErc20.approve(address(aerodromeSwapFeeFacet), swapAmount);
89+
console.log("approved USDC to our fee contract");
90+
// create the route
91+
IRouter.Route[] memory routes = new IRouter.Route[](1);
92+
routes[0] = IRouter.Route(address(REAL_USDC), address(REAL_WETH), false, address(0));
93+
94+
// reduce the expected output by 0.5% for slippage
95+
uint256 expectedOutput = (aerodromeRouter.getAmountsOut(swapAmount, routes)[1] * 9950) / 10000;
96+
97+
uint256 userWethBalanceBefore = WETHErc20.balanceOf(APP_USER_ALICE);
98+
99+
aerodromeSwapFeeFacet.swapExactTokensForTokensOnAerodrome(swapAmount, expectedOutput, routes, APP_USER_ALICE, block.timestamp + 1 minutes);
100+
vm.stopPrank();
101+
console.log("swapped USDC to WETH");
102+
uint256 userWethBalanceAfter = WETHErc20.balanceOf(APP_USER_ALICE);
103+
assertGt(userWethBalanceAfter - userWethBalanceBefore, expectedOutput);
104+
console.log("userWethBalanceAfter", userWethBalanceAfter);
105+
106+
// confirm the profit went to the fee contract, and the rest of the tokens went to the user
107+
uint256 userBalance = USDCErc20.balanceOf(APP_USER_ALICE);
108+
uint256 feeContractBalance = USDCErc20.balanceOf(address(aerodromeSwapFeeFacet));
109+
console.log("usdc userBalance", userBalance);
110+
console.log("usdc feeContractBalance", feeContractBalance);
111+
112+
// uint256 expectedTotalProfit = expectedTotalWithdrawal - depositAmount;
113+
// uint256 expectedUserProfit = expectedTotalProfit - (expectedTotalProfit * performanceFeePercentage / 10000);
114+
// uint256 expectedFeeContractProfit = expectedTotalProfit * performanceFeePercentage / 10000;
115+
// console.log("expectedTotalProfit", expectedTotalProfit);
116+
// console.log("expectedUserProfit", expectedUserProfit);
117+
// console.log("expectedFeeContractProfit", expectedFeeContractProfit);
118+
// console.log("userProfit", userBalance);
119+
// console.log("feeContractProfit", feeContractBalance);
120+
121+
// assertEq(userBalance, depositAmount + expectedUserProfit);
122+
// assertEq(feeContractBalance, expectedFeeContractProfit);
123+
124+
// test that USDC is in the set of tokens that have collected fees
125+
address[] memory tokensWithCollectedFees = feeAdminFacet.tokensWithCollectedFees();
126+
assertEq(tokensWithCollectedFees.length, 1);
127+
assertEq(tokensWithCollectedFees[0], address(USDCErc20));
128+
129+
// test withdrawal of profit from the fee contract as owner
130+
vm.startPrank(owner);
131+
feeAdminFacet.withdrawTokens(address(USDCErc20));
132+
vm.stopPrank();
133+
134+
// confirm the profit went to the owner
135+
// assertEq(USDCErc20.balanceOf(owner), expectedFeeContractProfit);
136+
137+
// confirm that the token is no longer in the set of tokens that have collected fees
138+
tokensWithCollectedFees = feeAdminFacet.tokensWithCollectedFees();
139+
assertEq(tokensWithCollectedFees.length, 0);
140+
}
141+
}

0 commit comments

Comments
 (0)