Skip to content

Commit 0316ebe

Browse files
authored
Merge pull request #10 from sprintertech/feat-hub-mvp
feat: Liquidity Hub MVP features.
2 parents 12c325d + 51e17ec commit 0316ebe

File tree

10 files changed

+685
-56
lines changed

10 files changed

+685
-56
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
ADMIN=
33
# USDC token address. Leave empty to deploy a test one instead.
44
USDC=
5+
# Deposits to Liquidity Hub are only allowed till this limit is reached.
6+
ASSETS_LIMIT=
57
BASETEST_PRIVATE_KEY=
68
VERIFY=false

contracts/LiquidityHub.sol

Lines changed: 120 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,40 +6,56 @@ import {
66
IERC20Metadata,
77
ERC20Upgradeable,
88
ERC4626Upgradeable,
9+
SafeERC20,
910
Math
1011
} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/ERC4626Upgradeable.sol";
12+
import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
1113
import {AccessControlUpgradeable} from '@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol';
1214
import {ERC7201Helper} from './utils/ERC7201Helper.sol';
1315
import {IManagedToken} from './interfaces/IManagedToken.sol';
16+
import {ILiquidityPool} from './interfaces/ILiquidityPool.sol';
1417

1518
contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
1619
using Math for uint256;
1720

1821
IManagedToken immutable public SHARES;
19-
bytes32 public constant ASSETS_UPDATE_ROLE = "ASSETS_UPDATE_ROLE";
22+
ILiquidityPool immutable public LIQUIDITY_POOL;
23+
bytes32 public constant ASSETS_ADJUST_ROLE = "ASSETS_ADJUST_ROLE";
24+
25+
event TotalAssetsAdjustment(uint256 oldAssets, uint256 newAssets);
26+
event AssetsLimitSet(uint256 oldLimit, uint256 newLimit);
2027

2128
error ZeroAddress();
2229
error NotImplemented();
2330
error IncompatibleAssetsAndShares();
31+
error AssetsLimitIsTooBig();
2432

2533
/// @custom:storage-location erc7201:sprinter.storage.LiquidityHub
2634
struct LiquidityHubStorage {
2735
uint256 totalAssets;
36+
uint256 assetsLimit;
2837
}
2938

3039
bytes32 private constant StorageLocation = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700;
3140

32-
constructor(address shares) {
41+
constructor(address shares, address liquidityPool) {
3342
ERC7201Helper.validateStorageLocation(
3443
StorageLocation,
3544
'sprinter.storage.LiquidityHub'
3645
);
3746
if (shares == address(0)) revert ZeroAddress();
47+
if (liquidityPool == address(0)) revert ZeroAddress();
3848
SHARES = IManagedToken(shares);
49+
LIQUIDITY_POOL = ILiquidityPool(liquidityPool);
3950
_disableInitializers();
4051
}
4152

42-
function initialize(IERC20 asset_, address admin) external initializer() {
53+
function initialize(
54+
IERC20 asset_,
55+
address admin,
56+
address adjuster,
57+
uint256 newAssetsLimit
58+
) external initializer() {
4359
ERC4626Upgradeable.__ERC4626_init(asset_);
4460
require(
4561
IERC20Metadata(address(asset_)).decimals() <= IERC20Metadata(address(SHARES)).decimals(),
@@ -48,6 +64,28 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
4864
// Deliberately not initializing ERC20Upgradable because its
4965
// functionality is delegated to SHARES.
5066
_grantRole(DEFAULT_ADMIN_ROLE, admin);
67+
_grantRole(ASSETS_ADJUST_ROLE, adjuster);
68+
_setAssetsLimit(newAssetsLimit);
69+
}
70+
71+
function adjustTotalAssets(uint256 amount, bool isIncrease) external onlyRole(ASSETS_ADJUST_ROLE) {
72+
LiquidityHubStorage storage $ = _getStorage();
73+
uint256 assets = $.totalAssets;
74+
uint256 newAssets = isIncrease ? assets + amount : assets - amount;
75+
$.totalAssets = newAssets;
76+
emit TotalAssetsAdjustment(assets, newAssets);
77+
}
78+
79+
function setAssetsLimit(uint256 newAssetsLimit) external onlyRole(DEFAULT_ADMIN_ROLE) {
80+
_setAssetsLimit(newAssetsLimit);
81+
}
82+
83+
function _setAssetsLimit(uint256 newAssetsLimit) internal {
84+
require(newAssetsLimit <= type(uint256).max / 10 ** _decimalsOffset(), AssetsLimitIsTooBig());
85+
LiquidityHubStorage storage $ = _getStorage();
86+
uint256 oldLimit = $.assetsLimit;
87+
$.assetsLimit = newAssetsLimit;
88+
emit AssetsLimitSet(oldLimit, newAssetsLimit);
5189
}
5290

5391
function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
@@ -91,26 +129,79 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
91129
return _getStorage().totalAssets;
92130
}
93131

94-
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
95-
(uint256 supplyShares, uint256 supplyAssets) = _getTotals();
132+
function assetsLimit() public view returns (uint256) {
133+
return _getStorage().assetsLimit;
134+
}
135+
136+
function maxDeposit(address) public view virtual override returns (uint256) {
137+
uint256 total = totalAssets();
138+
uint256 limit = assetsLimit();
139+
if (total >= limit) {
140+
return 0;
141+
}
142+
return limit - total;
143+
}
144+
145+
function maxMint(address) public view virtual override returns (uint256) {
146+
return _convertToShares(maxDeposit(address(0)), Math.Rounding.Floor);
147+
}
148+
149+
function depositWithPermit(
150+
uint256 assets,
151+
address receiver,
152+
uint256 deadline,
153+
uint8 v,
154+
bytes32 r,
155+
bytes32 s
156+
) external {
157+
IERC20Permit(asset()).permit(
158+
_msgSender(),
159+
address(this),
160+
assets,
161+
deadline,
162+
v,
163+
r,
164+
s
165+
);
166+
deposit(assets, receiver);
167+
}
168+
169+
function _toShares(
170+
uint256 assets,
171+
uint256 supplyShares,
172+
uint256 supplyAssets,
173+
Math.Rounding rounding
174+
) internal view returns (uint256) {
175+
(supplyShares, supplyAssets) = _getTotals(supplyShares, supplyAssets);
96176
return assets.mulDiv(supplyShares, supplyAssets, rounding);
97177
}
98178

99-
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
100-
(uint256 supplyShares, uint256 supplyAssets) = _getTotals();
179+
function _toAssets(
180+
uint256 shares,
181+
uint256 supplyShares,
182+
uint256 supplyAssets,
183+
Math.Rounding rounding
184+
) internal view returns (uint256) {
185+
(supplyShares, supplyAssets) = _getTotals(supplyShares, supplyAssets);
101186
return shares.mulDiv(supplyAssets, supplyShares, rounding);
102187
}
103188

104-
function _getTotals() internal view returns (uint256 supply, uint256 assets) {
105-
supply = totalSupply();
106-
if (supply == 0) {
107-
supply = 10 ** _decimalsOffset();
189+
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual override returns (uint256) {
190+
return _toShares(assets, totalSupply(), totalAssets(), rounding);
191+
}
192+
193+
function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual override returns (uint256) {
194+
return _toAssets(shares, totalSupply(), totalAssets(), rounding);
195+
}
196+
197+
function _getTotals(uint256 supplyShares, uint256 supplyAssets) internal view returns (uint256, uint256) {
198+
if (supplyShares == 0) {
199+
supplyShares = 10 ** _decimalsOffset();
108200
}
109-
assets = totalAssets();
110-
if (assets == 0) {
111-
assets = 1;
201+
if (supplyAssets == 0) {
202+
supplyAssets = 1;
112203
}
113-
return (supply, assets);
204+
return (supplyShares, supplyAssets);
114205
}
115206

116207
function _update(address from, address to, uint256 value) internal virtual override {
@@ -128,8 +219,12 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
128219
}
129220

130221
function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override {
131-
super._deposit(caller, receiver, assets, shares);
132-
_getStorage().totalAssets += assets;
222+
LiquidityHubStorage storage $ = _getStorage();
223+
SafeERC20.safeTransferFrom(IERC20(asset()), caller, address(LIQUIDITY_POOL), assets);
224+
_mint(receiver, shares);
225+
$.totalAssets += assets;
226+
LIQUIDITY_POOL.deposit();
227+
emit Deposit(caller, receiver, assets, shares);
133228
}
134229

135230
function _withdraw(
@@ -139,8 +234,14 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
139234
uint256 assets,
140235
uint256 shares
141236
) internal virtual override {
142-
_getStorage().totalAssets -= assets;
143-
super._withdraw(caller, receiver, owner, assets, shares);
237+
LiquidityHubStorage storage $ = _getStorage();
238+
if (caller != owner) {
239+
_spendAllowance(owner, caller, shares);
240+
}
241+
$.totalAssets -= assets;
242+
_burn(owner, shares);
243+
LIQUIDITY_POOL.withdraw(receiver, assets);
244+
emit Withdraw(caller, receiver, owner, assets, shares);
144245
}
145246

146247
function _decimalsOffset() internal view virtual override returns (uint8) {
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity 0.8.28;
3+
4+
interface ILiquidityPool {
5+
function deposit() external;
6+
7+
function withdraw(address to, uint256 amount) external;
8+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: LGPL-3.0-only
2+
pragma solidity 0.8.28;
3+
4+
import {IERC20, SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
5+
import {ILiquidityPool} from "../interfaces/ILiquidityPool.sol";
6+
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
7+
8+
contract TestLiquidityPool is ILiquidityPool, AccessControl {
9+
IERC20 private immutable ASSET;
10+
11+
event Deposit();
12+
13+
constructor(IERC20 asset) {
14+
ASSET = asset;
15+
_grantRole(DEFAULT_ADMIN_ROLE, _msgSender());
16+
}
17+
18+
function deposit() external override {
19+
emit Deposit();
20+
}
21+
function withdraw(address to, uint256 amount) external override onlyRole(DEFAULT_ADMIN_ROLE) {
22+
SafeERC20.safeTransfer(ASSET, to, amount);
23+
}
24+
}

contracts/testing/TestUSDC.sol

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
pragma solidity 0.8.28;
33

44
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
56

6-
contract TestUSDC is ERC20 {
7-
constructor() ERC20("Circle USD", "USDC") {
7+
contract TestUSDC is ERC20, ERC20Permit {
8+
constructor() ERC20("Circle USD", "USDC") ERC20Permit("Circle USD") {
89
_mint(msg.sender, 1000 * 10 ** decimals());
910
}
1011

hardhat.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import {HardhatUserConfig} from "hardhat/config";
22
import "@nomicfoundation/hardhat-toolbox";
33

4+
function isSet(param?: string) {
5+
return param && param.length > 0;
6+
}
7+
48
const config: HardhatUserConfig = {
59
solidity: {
610
version: "0.8.28",
@@ -15,6 +19,12 @@ const config: HardhatUserConfig = {
1519
localhost: {
1620
url: "http://127.0.0.1:8545/",
1721
},
22+
basetest: {
23+
chainId: 84532,
24+
url: "https://sepolia.base.org",
25+
accounts:
26+
isSet(process.env.BASETEST_PRIVATE_KEY) ? [process.env.BASETEST_PRIVATE_KEY || ""] : [],
27+
},
1828
},
1929
sourcify: {
2030
enabled: true,

scripts/deploy.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ import dotenv from "dotenv";
22
dotenv.config();
33

44
import hre from "hardhat";
5-
import {isAddress} from "ethers";
6-
import {getContractAt, getCreateAddress, deploy} from "../test/helpers";
5+
import {isAddress, MaxUint256, getBigInt} from "ethers";
6+
import {getContractAt, getCreateAddress, deploy, ZERO_BYTES32} from "../test/helpers";
77
import {assert, getVerifier} from "./helpers";
88
import {
9-
TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin
9+
TestUSDC, SprinterUSDCLPShare, LiquidityHub, TransparentUpgradeableProxy, ProxyAdmin,
10+
TestLiquidityPool,
1011
} from "../typechain-types";
1112

1213
async function main() {
1314
const [deployer] = await hre.ethers.getSigners();
1415
const admin: string = isAddress(process.env.ADMIN) ? process.env.ADMIN : deployer.address;
16+
const adjuster: string = isAddress(process.env.ADJUSTER) ? process.env.ADJUSTER : deployer.address;
17+
const maxLimit: bigint = MaxUint256 / 10n ** 12n;
18+
const assetsLimit: bigint = getBigInt(process.env.ASSETS_LIMIT || maxLimit);
1519
let usdc: string;
1620
if (isAddress(process.env.USDC)) {
1721
usdc = process.env.USDC;
@@ -20,6 +24,9 @@ async function main() {
2024
usdc = await testUSDC.getAddress();
2125
}
2226

27+
console.log("TEST: Using TEST Liquidity Pool");
28+
const liquidityPool = (await deploy("TestLiquidityPool", deployer, {}, usdc)) as TestLiquidityPool;
29+
2330
const startingNonce = await deployer.getNonce();
2431

2532
const verifier = getVerifier();
@@ -30,9 +37,11 @@ async function main() {
3037
) as SprinterUSDCLPShare;
3138

3239
const liquidityHubImpl = (
33-
await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target)
40+
await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target)
3441
) as LiquidityHub;
35-
const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(usdc, admin)).data;
42+
const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(
43+
usdc, admin, adjuster, assetsLimit
44+
)).data;
3645
const liquidityHubProxy = (await verifier.deploy(
3746
"TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2},
3847
liquidityHubImpl.target, admin, liquidityHubInit
@@ -43,6 +52,11 @@ async function main() {
4352

4453
assert(liquidityHubAddress == liquidityHubProxy.target, "LiquidityHub address mismatch");
4554

55+
const DEFAULT_ADMIN_ROLE = ZERO_BYTES32;
56+
57+
console.log("TEST: Using default admin role for Hub on Pool");
58+
await liquidityPool.grantRole(DEFAULT_ADMIN_ROLE, liquidityHub.target);
59+
4660
console.log();
4761
console.log(`Admin: ${admin}`);
4862
console.log(`SprinterUSDCLPShare: ${lpToken.target}`);

scripts/helpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@ export function assert(condition: boolean, message: string): void {
77
throw new Error(message);
88
};
99

10-
function sleep(msec: number): Promise<boolean> {
10+
export function sleep(msec: number): Promise<boolean> {
1111
return new Promise((resolve) => {
1212
setTimeout(() => resolve(true), msec);
1313
});
1414
};
1515

16+
export function isSet(input?: string): boolean {
17+
if (input) {
18+
return input.length > 0;
19+
}
20+
return false;
21+
};
22+
1623
export function getVerifier() {
1724
interface VerificationInput {
1825
address: string;

0 commit comments

Comments
 (0)