Skip to content

Commit 51e17ec

Browse files
committed
Add assets limit and tests
1 parent ec01367 commit 51e17ec

File tree

5 files changed

+269
-8
lines changed

5 files changed

+269
-8
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: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
2323
bytes32 public constant ASSETS_ADJUST_ROLE = "ASSETS_ADJUST_ROLE";
2424

2525
event TotalAssetsAdjustment(uint256 oldAssets, uint256 newAssets);
26+
event AssetsLimitSet(uint256 oldLimit, uint256 newLimit);
2627

2728
error ZeroAddress();
2829
error NotImplemented();
2930
error IncompatibleAssetsAndShares();
31+
error AssetsLimitIsTooBig();
3032

3133
/// @custom:storage-location erc7201:sprinter.storage.LiquidityHub
3234
struct LiquidityHubStorage {
3335
uint256 totalAssets;
36+
uint256 assetsLimit;
3437
}
3538

3639
bytes32 private constant StorageLocation = 0xb877bfaae1674461dd1960c90f24075e3de3265a91f6906fe128ab8da6ba1700;
@@ -47,7 +50,12 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
4750
_disableInitializers();
4851
}
4952

50-
function initialize(IERC20 asset_, address admin, address adjuster) external initializer() {
53+
function initialize(
54+
IERC20 asset_,
55+
address admin,
56+
address adjuster,
57+
uint256 newAssetsLimit
58+
) external initializer() {
5159
ERC4626Upgradeable.__ERC4626_init(asset_);
5260
require(
5361
IERC20Metadata(address(asset_)).decimals() <= IERC20Metadata(address(SHARES)).decimals(),
@@ -57,6 +65,7 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
5765
// functionality is delegated to SHARES.
5866
_grantRole(DEFAULT_ADMIN_ROLE, admin);
5967
_grantRole(ASSETS_ADJUST_ROLE, adjuster);
68+
_setAssetsLimit(newAssetsLimit);
6069
}
6170

6271
function adjustTotalAssets(uint256 amount, bool isIncrease) external onlyRole(ASSETS_ADJUST_ROLE) {
@@ -67,6 +76,18 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
6776
emit TotalAssetsAdjustment(assets, newAssets);
6877
}
6978

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);
89+
}
90+
7091
function name() public pure override(IERC20Metadata, ERC20Upgradeable) returns (string memory) {
7192
revert NotImplemented();
7293
}
@@ -108,6 +129,23 @@ contract LiquidityHub is ERC4626Upgradeable, AccessControlUpgradeable {
108129
return _getStorage().totalAssets;
109130
}
110131

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+
111149
function depositWithPermit(
112150
uint256 assets,
113151
address receiver,

scripts/deploy.ts

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

44
import hre from "hardhat";
5-
import {isAddress} from "ethers";
5+
import {isAddress, MaxUint256, getBigInt} from "ethers";
66
import {getContractAt, getCreateAddress, deploy, ZERO_BYTES32} from "../test/helpers";
77
import {assert, getVerifier} from "./helpers";
88
import {
@@ -14,6 +14,8 @@ async function main() {
1414
const [deployer] = await hre.ethers.getSigners();
1515
const admin: string = isAddress(process.env.ADMIN) ? process.env.ADMIN : deployer.address;
1616
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);
1719
let usdc: string;
1820
if (isAddress(process.env.USDC)) {
1921
usdc = process.env.USDC;
@@ -37,7 +39,9 @@ async function main() {
3739
const liquidityHubImpl = (
3840
await verifier.deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target)
3941
) as LiquidityHub;
40-
const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(usdc, admin, adjuster)).data;
42+
const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(
43+
usdc, admin, adjuster, assetsLimit
44+
)).data;
4145
const liquidityHubProxy = (await verifier.deploy(
4246
"TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2},
4347
liquidityHubImpl.target, admin, liquidityHubInit

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;

test/LiquidityHub.ts

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
} from "@nomicfoundation/hardhat-toolbox/network-helpers";
44
import {expect} from "chai";
55
import hre from "hardhat";
6+
import {Signature, resolveAddress, MaxUint256, getBigInt} from "ethers";
67
import {
78
getCreateAddress, getContractAt, deploy,
89
ZERO_ADDRESS, ZERO_BYTES32,
@@ -38,7 +39,7 @@ describe("LiquidityHub", function () {
3839
await deploy("LiquidityHub", deployer, {nonce: startingNonce + 1}, lpToken.target, liquidityPool.target)
3940
) as LiquidityHub;
4041
const liquidityHubInit = (await liquidityHubImpl.initialize.populateTransaction(
41-
usdc.target, admin.address, admin.address)
42+
usdc.target, admin.address, admin.address, getBigInt(MaxUint256) * USDC / LP)
4243
).data;
4344
const liquidityHubProxy = (await deploy(
4445
"TransparentUpgradeableProxy", deployer, {nonce: startingNonce + 2},
@@ -55,7 +56,7 @@ describe("LiquidityHub", function () {
5556
};
5657

5758
it("Should have default values", async function () {
58-
const {lpToken, liquidityHub, usdc, user, user2, liquidityPool} = await loadFixture(deployAll);
59+
const {lpToken, liquidityHub, usdc, user, user2, liquidityPool, USDC, LP} = await loadFixture(deployAll);
5960

6061
expect(await liquidityHub.SHARES()).to.equal(lpToken.target);
6162
expect(await liquidityHub.LIQUIDITY_POOL()).to.equal(liquidityPool.target);
@@ -64,6 +65,9 @@ describe("LiquidityHub", function () {
6465
expect(await liquidityHub.totalSupply()).to.equal(0n);
6566
expect(await liquidityHub.totalAssets()).to.equal(0n);
6667
expect(await liquidityHub.allowance(user.address, user2.address)).to.equal(0n);
68+
expect(await liquidityHub.assetsLimit()).to.equal(getBigInt(MaxUint256) * USDC / LP);
69+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(getBigInt(MaxUint256) * USDC / LP);
70+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(getBigInt(MaxUint256) * USDC / LP * LP / USDC);
6771

6872
await expect(liquidityHub.name())
6973
.to.be.revertedWithCustomError(liquidityHub, "NotImplemented()");
@@ -507,7 +511,213 @@ describe("LiquidityHub", function () {
507511
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
508512
});
509513

510-
it.skip("Should allow deposits and withdrawals after adjustment with increased assets", async function () {});
514+
it("Should allow deposits and withdrawals after adjustment with increased assets", async function () {
515+
const {
516+
lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP,
517+
liquidityPool, admin,
518+
} = await loadFixture(deployAll);
519+
520+
await usdc.connect(deployer).transfer(user.address, 20n * USDC);
521+
await usdc.connect(deployer).transfer(user2.address, 40n * USDC);
522+
await usdc.connect(user).approve(liquidityHub.target, 20n * USDC);
523+
await usdc.connect(user2).approve(liquidityHub.target, 40n * USDC);
524+
await liquidityHub.connect(user).deposit(10n * USDC, user.address);
525+
await liquidityHub.connect(user2).deposit(20n * USDC, user2.address);
526+
expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP);
527+
expect(await lpToken.balanceOf(user2.address)).to.equal(20n * LP);
528+
expect(await lpToken.totalSupply()).to.equal(30n * LP);
529+
expect(await liquidityHub.totalSupply()).to.equal(30n * LP);
530+
expect(await liquidityHub.totalAssets()).to.equal(30n * USDC);
531+
expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP);
532+
expect(await liquidityHub.balanceOf(user2.address)).to.equal(20n * LP);
533+
expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC);
534+
expect(await usdc.balanceOf(user2.address)).to.equal(20n * USDC);
535+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC);
536+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
537+
538+
await liquidityHub.connect(admin).adjustTotalAssets(12n * USDC, INCREASE);
539+
expect(await liquidityHub.totalAssets()).to.equal(42n * USDC);
540+
await liquidityHub.connect(user).redeem(5n * LP, user.address, user.address);
541+
await liquidityHub.connect(user2).redeem(10n * LP, user2.address, user2.address);
542+
await liquidityHub.connect(user).deposit(7n * USDC, user.address);
543+
await liquidityHub.connect(user2).deposit(14n * USDC, user2.address);
544+
expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP);
545+
expect(await lpToken.balanceOf(user2.address)).to.equal(20n * LP);
546+
expect(await lpToken.totalSupply()).to.equal(30n * LP);
547+
expect(await liquidityHub.totalSupply()).to.equal(30n * LP);
548+
expect(await liquidityHub.totalAssets()).to.equal(42n * USDC);
549+
expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP);
550+
expect(await liquidityHub.balanceOf(user2.address)).to.equal(20n * LP);
551+
expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC);
552+
expect(await usdc.balanceOf(user2.address)).to.equal(20n * USDC);
553+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC);
554+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
555+
});
556+
557+
it("Should allow deposits and withdrawals after adjustment with decreased assets", async function () {
558+
const {
559+
lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP,
560+
liquidityPool, admin,
561+
} = await loadFixture(deployAll);
562+
563+
await usdc.connect(deployer).transfer(user.address, 20n * USDC);
564+
await usdc.connect(deployer).transfer(user2.address, 40n * USDC);
565+
await usdc.connect(user).approve(liquidityHub.target, 20n * USDC);
566+
await usdc.connect(user2).approve(liquidityHub.target, 40n * USDC);
567+
await liquidityHub.connect(user).deposit(10n * USDC, user.address);
568+
await liquidityHub.connect(user2).deposit(20n * USDC, user2.address);
569+
expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP);
570+
expect(await lpToken.balanceOf(user2.address)).to.equal(20n * LP);
571+
expect(await lpToken.totalSupply()).to.equal(30n * LP);
572+
expect(await liquidityHub.totalSupply()).to.equal(30n * LP);
573+
expect(await liquidityHub.totalAssets()).to.equal(30n * USDC);
574+
expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP);
575+
expect(await liquidityHub.balanceOf(user2.address)).to.equal(20n * LP);
576+
expect(await usdc.balanceOf(user.address)).to.equal(10n * USDC);
577+
expect(await usdc.balanceOf(user2.address)).to.equal(20n * USDC);
578+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(30n * USDC);
579+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
511580

512-
it.skip("Should allow deposits and withdrawals after adjustment with decreased assets", async function () {});
581+
await liquidityHub.connect(admin).adjustTotalAssets(27n * USDC, DECREASE);
582+
expect(await liquidityHub.totalAssets()).to.equal(3n * USDC);
583+
await liquidityHub.connect(user).redeem(10n * LP, user.address, user.address);
584+
await liquidityHub.connect(user2).redeem(20n * LP, user2.address, user2.address);
585+
await liquidityHub.connect(user).deposit(6n * USDC, user.address);
586+
await liquidityHub.connect(user2).deposit(12n * USDC, user2.address);
587+
expect(await lpToken.balanceOf(user.address)).to.equal(6n * LP);
588+
expect(await lpToken.balanceOf(user2.address)).to.equal(12n * LP);
589+
expect(await lpToken.totalSupply()).to.equal(18n * LP);
590+
expect(await liquidityHub.totalSupply()).to.equal(18n * LP);
591+
expect(await liquidityHub.totalAssets()).to.equal(18n * USDC);
592+
expect(await liquidityHub.balanceOf(user.address)).to.equal(6n * LP);
593+
expect(await liquidityHub.balanceOf(user2.address)).to.equal(12n * LP);
594+
expect(await usdc.balanceOf(user.address)).to.equal(5n * USDC);
595+
expect(await usdc.balanceOf(user2.address)).to.equal(10n * USDC);
596+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(45n * USDC);
597+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
598+
});
599+
600+
it("Should allow to deposit with permit", async function () {
601+
const {lpToken, liquidityHub, usdc, deployer, user, user2, USDC, LP, liquidityPool} = await loadFixture(deployAll);
602+
603+
const domain = {
604+
name: "Circle USD",
605+
version: "1",
606+
chainId: hre.network.config.chainId,
607+
verifyingContract: await resolveAddress(usdc),
608+
};
609+
610+
const types = {
611+
Permit: [
612+
{name: "owner", type: "address"},
613+
{name: "spender", type: "address"},
614+
{name: "value", type: "uint256"},
615+
{name: "nonce", type: "uint256"},
616+
{name: "deadline", type: "uint256"},
617+
],
618+
};
619+
620+
await usdc.connect(deployer).transfer(user.address, 10n * USDC);
621+
const permitSig = Signature.from(await user.signTypedData(domain, types, {
622+
owner: user.address,
623+
spender: liquidityHub.target,
624+
value: 10n * USDC,
625+
nonce: 0n,
626+
deadline: 2000000000n,
627+
}));
628+
const tx = liquidityHub.connect(user).depositWithPermit(
629+
10n * USDC,
630+
user2.address,
631+
2000000000n,
632+
permitSig.v,
633+
permitSig.r,
634+
permitSig.s,
635+
);
636+
await expect(tx)
637+
.to.emit(lpToken, "Transfer")
638+
.withArgs(ZERO_ADDRESS, user2.address, 10n * LP);
639+
await expect(tx)
640+
.to.emit(usdc, "Transfer")
641+
.withArgs(user.address, liquidityPool.target, 10n * USDC);
642+
expect(await lpToken.balanceOf(user.address)).to.equal(0n);
643+
expect(await lpToken.balanceOf(user2.address)).to.equal(10n * LP);
644+
expect(await lpToken.totalSupply()).to.equal(10n * LP);
645+
expect(await liquidityHub.totalSupply()).to.equal(10n * LP);
646+
expect(await liquidityHub.totalAssets()).to.equal(10n * USDC);
647+
expect(await liquidityHub.balanceOf(user.address)).to.equal(0n);
648+
expect(await liquidityHub.balanceOf(user2.address)).to.equal(10n * LP);
649+
expect(await usdc.balanceOf(user.address)).to.equal(0n);
650+
expect(await usdc.balanceOf(user2.address)).to.equal(0n);
651+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
652+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC);
653+
expect(await usdc.allowance(user.address, liquidityHub.target)).to.equal(0n);
654+
});
655+
656+
it("Should allow admin to set assets limit", async function () {
657+
const {liquidityHub, deployer, admin, user, usdc, lpToken, USDC, LP, liquidityPool} = await loadFixture(deployAll);
658+
659+
const tx = liquidityHub.connect(admin).setAssetsLimit(0n);
660+
await expect(tx)
661+
.to.emit(liquidityHub, "AssetsLimitSet")
662+
.withArgs(getBigInt(MaxUint256) * USDC / LP, 0n);
663+
664+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(0n);
665+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(0n);
666+
667+
await usdc.connect(deployer).transfer(user.address, 10n * USDC);
668+
await usdc.connect(user).approve(liquidityHub.target, 10n * USDC);
669+
670+
await expect(liquidityHub.connect(user).deposit(1n, user.address))
671+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)");
672+
await expect(liquidityHub.connect(user).mint(LP, user.address))
673+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)");
674+
675+
const tx2 = liquidityHub.connect(admin).setAssetsLimit(100n * USDC);
676+
await expect(tx2)
677+
.to.emit(liquidityHub, "AssetsLimitSet")
678+
.withArgs(0n, 100n * USDC);
679+
680+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(100n * USDC);
681+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(100n * LP);
682+
683+
await liquidityHub.connect(user).deposit(10n * USDC, user.address);
684+
685+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(90n * USDC);
686+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(90n * LP);
687+
expect(await lpToken.balanceOf(user.address)).to.equal(10n * LP);
688+
expect(await lpToken.totalSupply()).to.equal(10n * LP);
689+
expect(await liquidityHub.totalSupply()).to.equal(10n * LP);
690+
expect(await liquidityHub.totalAssets()).to.equal(10n * USDC);
691+
expect(await liquidityHub.balanceOf(user.address)).to.equal(10n * LP);
692+
expect(await usdc.balanceOf(user.address)).to.equal(0n);
693+
expect(await usdc.balanceOf(liquidityHub.target)).to.equal(0n);
694+
expect(await usdc.balanceOf(liquidityPool.target)).to.equal(10n * USDC);
695+
696+
await expect(liquidityHub.connect(user).deposit(90n * USDC + 1n, user.address))
697+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)");
698+
await expect(liquidityHub.connect(user).mint(90n * LP + 1n, user.address))
699+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)");
700+
701+
await liquidityHub.connect(admin).adjustTotalAssets(10n * USDC, INCREASE);
702+
703+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(80n * USDC);
704+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(40n * LP);
705+
706+
await expect(liquidityHub.connect(user).deposit(80n * USDC + 1n, user.address))
707+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxDeposit(address,uint256,uint256)");
708+
await expect(liquidityHub.connect(user).mint(40n * LP + 1n, user.address))
709+
.to.be.revertedWithCustomError(liquidityHub, "ERC4626ExceededMaxMint(address,uint256,uint256)");
710+
711+
await liquidityHub.connect(admin).adjustTotalAssets(10n * USDC, DECREASE);
712+
713+
expect(await liquidityHub.maxDeposit(ZERO_ADDRESS)).to.equal(90n * USDC);
714+
expect(await liquidityHub.maxMint(ZERO_ADDRESS)).to.equal(90n * LP);
715+
});
716+
717+
it("Should not allow others to set assets limit", async function () {
718+
const {liquidityHub, user} = await loadFixture(deployAll);
719+
720+
await expect(liquidityHub.connect(user).setAssetsLimit(0n))
721+
.to.be.revertedWithCustomError(liquidityHub, "AccessControlUnauthorizedAccount(address,bytes32)");
722+
});
513723
});

0 commit comments

Comments
 (0)