Skip to content

Commit 8546a59

Browse files
authored
Merge pull request #5 from SolidityUniversity/lesson22
pull lesson22 to main
2 parents 5035af3 + 4989d76 commit 8546a59

14 files changed

Lines changed: 2175 additions & 59 deletions

DOCUMENTATION.md

Lines changed: 925 additions & 0 deletions
Large diffs are not rendered by default.

src/AccessManager.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@ contract AccessManager is AccessControl {
99

1010
constructor() {
1111
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
12+
13+
// Set role admin relationships
14+
_setRoleAdmin(Roles.ADMIN_ROLE, DEFAULT_ADMIN_ROLE);
1215
_setRoleAdmin(Roles.MULTISIG_ADMIN_ROLE, DEFAULT_ADMIN_ROLE);
16+
_setRoleAdmin(Roles.ALLOWED_EIP712_SWAP_ROLE, DEFAULT_ADMIN_ROLE);
17+
}
18+
19+
function addAdmin(address _admin) external onlyRole(DEFAULT_ADMIN_ROLE) {
20+
_grantRole(Roles.ADMIN_ROLE, _admin);
21+
}
22+
23+
function removeAdmin(address _admin) external onlyRole(DEFAULT_ADMIN_ROLE) {
24+
_revokeRole(Roles.ADMIN_ROLE, _admin);
1325
}
1426

1527
function addMultisigAdmin(address _multisigAdmin) external onlyRole(DEFAULT_ADMIN_ROLE) {
@@ -20,7 +32,23 @@ contract AccessManager is AccessControl {
2032
_revokeRole(Roles.MULTISIG_ADMIN_ROLE, _multisigAdmin);
2133
}
2234

35+
function addEIP712Swapper(address _swapper) external onlyRole(DEFAULT_ADMIN_ROLE) {
36+
_grantRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _swapper);
37+
}
38+
39+
function removeEIP712Swapper(address _swapper) external onlyRole(DEFAULT_ADMIN_ROLE) {
40+
_revokeRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _swapper);
41+
}
42+
43+
function isAdmin(address _address) external view returns (bool) {
44+
return hasRole(Roles.ADMIN_ROLE, _address);
45+
}
46+
2347
function isMultisigAdmin(address _address) external view returns (bool) {
2448
return hasRole(Roles.MULTISIG_ADMIN_ROLE, _address);
2549
}
50+
51+
function isEIP712Swapper(address _address) external view returns (bool) {
52+
return hasRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _address);
53+
}
2654
}

src/EIP712Swap.sol

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,14 @@ contract EIP712Swap is EIP712 {
5555
if (_swapRequest.nonce != _nonces[_swapRequest.sender]) revert InvalidNonce();
5656

5757
_nonces[_swapRequest.sender]++;
58-
LiquidityPool(_swapRequest.pool).swap(
59-
_swapRequest.sender,
60-
_swapRequest.tokenIn,
61-
_swapRequest.tokenOut,
62-
_swapRequest.amountIn,
63-
_swapRequest.minAmountOut
64-
);
58+
LiquidityPool(_swapRequest.pool)
59+
.swap(
60+
_swapRequest.sender,
61+
_swapRequest.tokenIn,
62+
_swapRequest.tokenOut,
63+
_swapRequest.amountIn,
64+
_swapRequest.minAmountOut
65+
);
6566

6667
return true;
6768
}

src/FeeManager.sol

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,15 @@ pragma solidity ^0.8.30;
44
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
55
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
66
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
7-
87
import "./ISwap.sol";
98

109
contract FeeManager is Initializable, AccessControlUpgradeable, UUPSUpgradeable {
1110
uint256 public constant FEE_DENOMINATOR = 10000;
12-
13-
uint256 public fee;
11+
uint256 public fee; // Fee in basis points (e.g., 250 = 2.5%)
1412

1513
function initialize(uint256 _fee) external initializer {
16-
_disableInitializers();
14+
__AccessControl_init();
15+
__UUPSUpgradeable_init();
1716
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
1817
fee = _fee;
1918
}
@@ -24,7 +23,13 @@ contract FeeManager is Initializable, AccessControlUpgradeable, UUPSUpgradeable
2423
fee = _fee;
2524
}
2625

26+
/// @notice Calculate absolute fee amount for a swap
27+
/// @param swapParams The swap parameters
28+
/// @return Absolute fee amount in output token units
2729
function getFee(ISwap.SwapParams memory swapParams) external view returns (uint256) {
28-
return (swapParams.amount0 * fee) / FEE_DENOMINATOR;
30+
// Calculate fee based on input amount and convert to output token equivalent
31+
uint256 amountOut =
32+
(swapParams.amount0 * swapParams.reserveToken1) / (swapParams.reserveToken0 + swapParams.amount0);
33+
return (amountOut * fee) / FEE_DENOMINATOR;
2934
}
3035
}

src/LiquidityPool.sol

Lines changed: 85 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
pragma solidity ^0.8.30;
33

44
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import "@openzeppelin/contracts/access/AccessControl.sol";
56
import "./ISwap.sol";
67
import "./FeeManager.sol";
78
import "./EIP712Swap.sol";
9+
import "./Roles.sol";
10+
11+
contract LiquidityPool is ISwap, AccessControl {
12+
using Roles for bytes32;
813

9-
contract LiquidityPool is ISwap, FeeManager {
1014
address public token0;
1115
uint256 public token0Decimals;
1216
address public token1;
@@ -16,24 +20,22 @@ contract LiquidityPool is ISwap, FeeManager {
1620
FeeManager public feeManager;
1721
EIP712Swap public eip712Swap;
1822

19-
// @notice Emitted when liquidity is added to the pool
20-
/// @param _token The token that was added
21-
/// @param _amount The amount of tokens that were added
22-
event LiquidityAdded(address indexed _token, uint256 _amount);
23+
modifier onlyAdminOrEIP712Swap() {
24+
require(
25+
hasRole(Roles.ADMIN_ROLE, msg.sender) || hasRole(Roles.ALLOWED_EIP712_SWAP_ROLE, msg.sender),
26+
"Not authorized for swap operations"
27+
);
28+
_;
29+
}
2330

24-
// @notice Emitted when a swap is executed
25-
/// @param _tokenIn The token that was swapped in
26-
/// @param _tokenOut The token that was swapped out
27-
/// @param _amountIn The amount of tokens that were swapped in
28-
/// @param _amountOut The amount of tokens that were swapped out
31+
event LiquidityAdded(address indexed _token, uint256 _amount);
2932
event Swap(address indexed _tokenIn, address indexed _tokenOut, uint256 _amountIn, uint256 _amountOut);
3033

3134
error InsufficientTokenBalance();
3235
error InvalidTokenAddress(address _token);
3336
error InvalidTokenPair(address _tokenIn, address _tokenOut);
3437
error InsufficientLiquidity();
3538
error InsufficientOutputAmount(uint256 expected, uint256 actual);
36-
error ExcessiveInputAmount(uint256 expected, uint256 actual);
3739
error InsufficientAllowance();
3840

3941
constructor(
@@ -50,61 +52,88 @@ contract LiquidityPool is ISwap, FeeManager {
5052
token1Decimals = _token1Decimals;
5153
feeManager = FeeManager(_feeManager);
5254
eip712Swap = EIP712Swap(_eip712Swap);
55+
56+
// Setup roles
57+
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
58+
_grantRole(Roles.ADMIN_ROLE, msg.sender);
59+
60+
// Grant EIP712Swap contract permission to execute swaps
61+
_grantRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _eip712Swap);
62+
63+
// Set role admin relationships
64+
_setRoleAdmin(Roles.ADMIN_ROLE, DEFAULT_ADMIN_ROLE);
65+
_setRoleAdmin(Roles.ALLOWED_EIP712_SWAP_ROLE, DEFAULT_ADMIN_ROLE);
5366
}
5467

55-
// @notice Add liquidity to the pool
56-
/// @param _token The token to add liquidity for
57-
/// @param _amount The amount of tokens to add
58-
function addLiquidity(address _token, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) {
59-
if (_token == token0 || _token == token1) {
68+
/// @notice Add liquidity to the pool (admin only)
69+
function addLiquidity(address _token, uint256 _amount) external onlyRole(Roles.ADMIN_ROLE) {
70+
if (_token != token0 && _token != token1) {
6071
revert InvalidTokenAddress(_token);
6172
}
6273

63-
if (IERC20(_token).balanceOf(address(msg.sender)) < _amount) {
74+
if (IERC20(_token).balanceOf(msg.sender) < _amount) {
6475
revert InsufficientTokenBalance();
6576
}
6677

6778
require(IERC20(_token).transferFrom(msg.sender, address(this), _amount));
6879

6980
if (_token == token0) {
7081
reserveToken0 += _amount;
71-
} else if (_token == token1) {
82+
} else {
7283
reserveToken1 += _amount;
7384
}
7485

7586
emit LiquidityAdded(_token, _amount);
7687
}
7788

78-
// @notice Get the reserves of the pool
79-
/// @return _reserveToken0 The reserve of token0
80-
/// @return _reserveToken1 The reserve of token1
89+
/// @notice Remove liquidity from the pool (admin only)
90+
function removeLiquidity(address _token, uint256 _amount) external onlyRole(Roles.ADMIN_ROLE) {
91+
if (_token != token0 && _token != token1) {
92+
revert InvalidTokenAddress(_token);
93+
}
94+
95+
uint256 currentReserve = _token == token0 ? reserveToken0 : reserveToken1;
96+
require(_amount <= currentReserve, "Insufficient reserves");
97+
98+
require(IERC20(_token).transfer(msg.sender, _amount));
99+
100+
if (_token == token0) {
101+
reserveToken0 -= _amount;
102+
} else {
103+
reserveToken1 -= _amount;
104+
}
105+
}
106+
107+
/// @notice Grant EIP712 swap permission to an address
108+
function grantSwapRole(address _swapper) external onlyRole(DEFAULT_ADMIN_ROLE) {
109+
_grantRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _swapper);
110+
}
111+
112+
/// @notice Revoke EIP712 swap permission from an address
113+
function revokeSwapRole(address _swapper) external onlyRole(DEFAULT_ADMIN_ROLE) {
114+
_revokeRole(Roles.ALLOWED_EIP712_SWAP_ROLE, _swapper);
115+
}
116+
81117
function getReserves() external view returns (uint256 _reserveToken0, uint256 _reserveToken1) {
82118
_reserveToken0 = reserveToken0;
83119
_reserveToken1 = reserveToken1;
84120
}
85121

86-
/// @notice Get the price of the token in the pool
87-
/// @param _tokenIn The token to get the price of
88-
/// @param _tokenOut The token to get the price of
89-
/// @return _price The price of the token in the pool
90122
function getPrice(address _tokenIn, address _tokenOut) external view returns (uint256 _price) {
91123
uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1;
92124
uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1;
93-
uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals;
94-
uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals;
95125

96-
_price = (_reserveTokenIn * 10 ** _tokenInDecimals) / (_reserveTokenOut * 10 ** _tokenOutDecimals);
126+
if (_reserveTokenIn == 0 || _reserveTokenOut == 0) {
127+
return 0;
128+
}
129+
130+
_price = (_reserveTokenOut * 1e18) / _reserveTokenIn;
97131
}
98132

99-
/// @notice Swap tokens in the pool
100-
/// @param _sender The address of the sender
101-
/// @param _tokenIn The token to swap in
102-
/// @param _tokenOut The token to swap out
103-
/// @param _amountIn The amount of tokens to swap in
104-
/// @param _minAmountOut The minimum amount of tokens to swap out
133+
/// @notice Execute a swap (admin or authorized EIP712 contract only)
105134
function swap(address _sender, address _tokenIn, address _tokenOut, uint256 _amountIn, uint256 _minAmountOut)
106135
external
107-
onlyRole(DEFAULT_ADMIN_ROLE)
136+
onlyAdminOrEIP712Swap
108137
{
109138
if (
110139
_tokenIn != token0 && _tokenIn != token1 || _tokenOut != token0 && _tokenOut != token1
@@ -113,25 +142,35 @@ contract LiquidityPool is ISwap, FeeManager {
113142
revert InvalidTokenPair(_tokenIn, _tokenOut);
114143
}
115144

116-
address _msgSender = msg.sender == address(eip712Swap) ? _sender : msg.sender;
145+
address tokenHolder = _sender;
117146

118-
if (IERC20(_tokenIn).allowance(_msgSender, address(this)) < _amountIn) revert InsufficientAllowance();
147+
if (IERC20(_tokenIn).allowance(tokenHolder, address(this)) < _amountIn) revert InsufficientAllowance();
119148

120149
uint256 _reserveTokenIn = _tokenIn == token0 ? reserveToken0 : reserveToken1;
121150
uint256 _reserveTokenOut = _tokenOut == token0 ? reserveToken0 : reserveToken1;
122-
uint256 _tokenInDecimals = _tokenIn == token0 ? token0Decimals : token1Decimals;
123-
uint256 _tokenOutDecimals = _tokenOut == token0 ? token0Decimals : token1Decimals;
124151

125-
uint256 amountOut = (_amountIn * (10 ** _tokenOutDecimals) * _reserveTokenOut)
126-
/ (_reserveTokenIn + _amountIn * (10 ** _tokenOutDecimals));
127-
uint256 _fee = feeManager.getFee(SwapParams(_tokenIn, _tokenOut, _amountIn, _reserveTokenIn, _reserveTokenOut));
128-
amountOut -= (_fee * (10 ** _tokenOutDecimals)) / (10 ** _tokenInDecimals);
152+
if (_reserveTokenIn == 0 || _reserveTokenOut == 0) revert InsufficientLiquidity();
153+
if (_amountIn >= _reserveTokenIn) revert InsufficientLiquidity();
154+
155+
// AMM calculation
156+
uint256 amountOut = (_amountIn * _reserveTokenOut) / (_reserveTokenIn + _amountIn);
157+
158+
// Apply fee using FeeManager
159+
ISwap.SwapParams memory swapParams = ISwap.SwapParams({
160+
token0: _tokenIn,
161+
token1: _tokenOut,
162+
amount0: _amountIn,
163+
reserveToken0: _reserveTokenIn,
164+
reserveToken1: _reserveTokenOut
165+
});
166+
167+
uint256 feeAmount = feeManager.getFee(swapParams);
168+
amountOut = amountOut > feeAmount ? amountOut - feeAmount : 0;
129169

130170
if (amountOut < _minAmountOut) revert InsufficientOutputAmount(_minAmountOut, amountOut);
131-
if (amountOut > _reserveTokenOut) revert InsufficientLiquidity();
132171

133-
require(IERC20(_tokenIn).transferFrom(_msgSender, address(this), _amountIn));
134-
require(IERC20(_tokenOut).transfer(_msgSender, amountOut));
172+
require(IERC20(_tokenIn).transferFrom(tokenHolder, address(this), _amountIn));
173+
require(IERC20(_tokenOut).transfer(tokenHolder, amountOut));
135174

136175
if (_tokenIn == token0) {
137176
reserveToken0 += _amountIn;

src/Roles.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ pragma solidity ^0.8.30;
44
library Roles {
55
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
66
bytes32 public constant MULTISIG_ADMIN_ROLE = keccak256("MULTISIG_ADMIN_ROLE");
7+
bytes32 public constant ALLOWED_EIP712_SWAP_ROLE = keccak256("ALLOWED_EIP712_SWAP_ROLE");
78
}

0 commit comments

Comments
 (0)