Skip to content

Commit c5aa1e5

Browse files
authoredMar 13, 2025··
Merge pull request #428 from The-Poolz/issue-393
immutable `withdraw` + `split`
2 parents 529fb15 + dd0bc29 commit c5aa1e5

File tree

10 files changed

+7347
-10041
lines changed

10 files changed

+7347
-10041
lines changed
 

‎contracts/SimpleProviders/DealProvider/DealProvider.sol

+23-4
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ contract DealProvider is DealProviderState, BasicProvider {
2626
}
2727

2828
/// @dev Splits a pool into two pools. Used by the LockedDealNFT contract or Provider
29-
function split(uint256 oldPoolId, uint256 newPoolId, uint256 ratio) external override firewallProtected onlyProvider {
30-
uint256 splitAmount = poolIdToAmount[oldPoolId].calcAmount(ratio);
31-
require(poolIdToAmount[oldPoolId] >= splitAmount, "Split amount exceeds the available amount");
32-
poolIdToAmount[oldPoolId] -= splitAmount;
29+
function split(uint256 lockDealNFTPoolId, uint256 newPoolId, uint256 ratio) external override firewallProtected onlyProvider {
30+
uint256 splitAmount = poolIdToAmount[lockDealNFTPoolId].calcAmount(ratio);
31+
require(poolIdToAmount[lockDealNFTPoolId] >= splitAmount, "Split amount exceeds the available amount");
3332
poolIdToAmount[newPoolId] = splitAmount;
33+
// save leftAmount to the newly created pool from the old pool
34+
uint256 copyOldPoolId = _mintNewNFT(lockDealNFTPoolId, lockDealNFT.ownerOf(newPoolId));
35+
poolIdToAmount[copyOldPoolId] = poolIdToAmount[lockDealNFTPoolId] - splitAmount;
36+
// set to 0 to finalize the pool
37+
poolIdToAmount[lockDealNFTPoolId] = 0;
3438
}
3539

3640
/**@dev Providers overrides this function to add additional parameters when creating a pool.
@@ -51,4 +55,19 @@ contract DealProvider is DealProviderState, BasicProvider {
5155
function getWithdrawableAmount(uint256 poolId) public view override returns (uint256) {
5256
return poolIdToAmount[poolId];
5357
}
58+
59+
/// @dev creates a new NFT and clones the vault id from the source pool id.
60+
/// @param sourcePoolId The ID of the source pool.
61+
/// @param to The address of the NFT owner.
62+
/// @return newPoolId The ID of the newly created pool.
63+
/// 0x21de57c4 - represents bytes4(keccak256("_mintNewNFT(uint256,address)"))
64+
function _mintNewNFT(
65+
uint256 sourcePoolId,
66+
address to
67+
) internal firewallProtectedSig(0x21de57c4) returns (uint256 newPoolId) {
68+
IProvider sourceProvider = lockDealNFT.poolIdToProvider(sourcePoolId);
69+
newPoolId = lockDealNFT.mintForProvider(to, sourceProvider);
70+
// clone vault id
71+
lockDealNFT.cloneVaultId(newPoolId, sourcePoolId);
72+
}
5473
}

‎contracts/SimpleProviders/LockProvider/LockDealProvider.sol

+5-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ contract LockDealProvider is BasicProvider, LockDealState {
2525
(withdrawnAmount, isFinal) = provider.withdraw(poolId, amount > withdrawnAmount ? withdrawnAmount : amount);
2626
}
2727

28-
function split(uint256 oldPoolId, uint256 newPoolId, uint256 ratio) external override firewallProtected onlyProvider {
29-
provider.split(oldPoolId, newPoolId, ratio);
30-
poolIdToTime[newPoolId] = poolIdToTime[oldPoolId];
28+
function split(uint256 lockDealNFTPoolId, uint256 newPoolId, uint256 ratio) external override firewallProtected onlyProvider {
29+
provider.split(lockDealNFTPoolId, newPoolId, ratio);
30+
poolIdToTime[newPoolId] = poolIdToTime[lockDealNFTPoolId];
31+
// save startTime to the newly created pool from the old pool
32+
poolIdToTime[newPoolId + 1] = poolIdToTime[lockDealNFTPoolId];
3133
}
3234

3335
function currentParamsTargetLength() public view override(IProvider, ProviderState) returns (uint256) {

‎contracts/SimpleProviders/TimedDealProvider/TimedDealProvider.sol

+54-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import "../LockProvider/LockDealState.sol";
55
import "../DealProvider/DealProviderState.sol";
66
import "../Provider/BasicProvider.sol";
77
import "@poolzfinance/poolz-helper-v2/contracts/CalcUtils.sol";
8+
import "@poolzfinance/poolz-helper-v2/contracts/LastPoolOwnerState.sol";
89

9-
contract TimedDealProvider is LockDealState, DealProviderState, BasicProvider {
10+
contract TimedDealProvider is LockDealState, DealProviderState, BasicProvider, LastPoolOwnerState {
1011
using CalcUtils for uint256;
1112

1213
/**
@@ -20,12 +21,34 @@ contract TimedDealProvider is LockDealState, DealProviderState, BasicProvider {
2021
lockDealNFT = _lockDealNFT;
2122
name = "TimedDealProvider";
2223
}
23-
24+
2425
function _withdraw(
2526
uint256 poolId,
2627
uint256 amount
2728
) internal override firewallProtectedSig(0x9e2bf22c) returns (uint256 withdrawnAmount, bool isFinal) {
28-
(withdrawnAmount, isFinal) = provider.withdraw(poolId, amount);
29+
if (amount == 0) return (0, false);
30+
isFinal = true;
31+
withdrawnAmount = amount;
32+
uint256[] memory params = provider.getParams(poolId);
33+
uint256 remainingAmount = params[0] - amount;
34+
if (remainingAmount > 0 && lastPoolOwner[poolId] != address(0)) {
35+
// create immutable NFT
36+
uint256 newPoolId = lockDealNFT.mintForProvider(
37+
lastPoolOwner[poolId],
38+
lockDealNFT.poolIdToProvider(poolId)
39+
);
40+
// clone vault id
41+
lockDealNFT.cloneVaultId(newPoolId, poolId);
42+
// register new pool
43+
params[0] = remainingAmount;
44+
provider.registerPool(newPoolId, params);
45+
poolIdToTime[newPoolId] = poolIdToTime[poolId];
46+
poolIdToAmount[newPoolId] = poolIdToAmount[poolId];
47+
delete lastPoolOwner[poolId];
48+
}
49+
// Reset and update the original pool
50+
params[0] = 0;
51+
provider.registerPool(poolId, params);
2952
}
3053

3154
function getWithdrawableAmount(uint256 poolId) public view override returns (uint256) {
@@ -44,12 +67,14 @@ contract TimedDealProvider is LockDealState, DealProviderState, BasicProvider {
4467
return debitableAmount - (startAmount - leftAmount);
4568
}
4669

47-
function split(uint256 oldPoolId, uint256 newPoolId, uint256 ratio) external firewallProtected onlyProvider {
48-
provider.split(oldPoolId, newPoolId, ratio);
49-
uint256 newPoolStartAmount = poolIdToAmount[oldPoolId].calcAmount(ratio);
50-
poolIdToAmount[oldPoolId] -= newPoolStartAmount;
70+
function split(uint256 lockDealNFTPoolId, uint256 newPoolId, uint256 ratio) external firewallProtected onlyProvider {
71+
provider.split(lockDealNFTPoolId, newPoolId, ratio);
72+
uint256 newPoolStartAmount = poolIdToAmount[lockDealNFTPoolId].calcAmount(ratio);
5173
poolIdToAmount[newPoolId] = newPoolStartAmount;
52-
poolIdToTime[newPoolId] = poolIdToTime[oldPoolId];
74+
poolIdToTime[newPoolId] = poolIdToTime[lockDealNFTPoolId];
75+
// save startAmount and FinishTime to the newly created pool from the old pool
76+
poolIdToAmount[newPoolId + 1] = poolIdToAmount[lockDealNFTPoolId] - newPoolStartAmount;
77+
poolIdToTime[newPoolId + 1] = poolIdToTime[lockDealNFTPoolId];
5378
}
5479

5580
///@param params[0] = leftAmount = startAmount (leftAmount & startAmount must be same while creating pool)
@@ -76,4 +101,25 @@ contract TimedDealProvider is LockDealState, DealProviderState, BasicProvider {
76101
function currentParamsTargetLength() public view override(IProvider, ProviderState) returns (uint256) {
77102
return 1 + provider.currentParamsTargetLength();
78103
}
104+
105+
/**
106+
* @dev Executes before a transfer, updating state based on the transfer details.
107+
* @param from Sender address.
108+
* @param to Receiver address.
109+
* @param poolId Pool identifier.
110+
*/
111+
function beforeTransfer(
112+
address from,
113+
address to,
114+
uint256 poolId
115+
) external virtual override firewallProtected onlyNFT {
116+
if (to == address(lockDealNFT)) {
117+
// this means it will be withdraw or split
118+
lastPoolOwner[poolId] = from; //this is the only way to know the owner of the pool
119+
}
120+
}
121+
122+
function supportsInterface(bytes4 interfaceId) public view virtual override(BasicProvider, LastPoolOwnerState) returns (bool) {
123+
return BasicProvider.supportsInterface(interfaceId) || LastPoolOwnerState.supportsInterface(interfaceId);
124+
}
79125
}

‎hardhat.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ const config: HardhatUserConfig = {
1515
{
1616
version: '0.8.25',
1717
settings: {
18-
evmVersion: 'istanbul',
18+
evmVersion: 'cancun',
1919
optimizer: {
2020
enabled: true,
2121
runs: 200,
2222
},
23+
viaIR: true,
2324
},
2425
},
2526
],

‎package-lock.json

+7,165-10,000
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
"license": "MIT",
1010
"main": "index.js",
1111
"dependencies": {
12-
"@ironblocks/firewall-consumer": "^1.0.13",
12+
"@ironblocks/firewall-consumer": "^1.0.17",
1313
"@openzeppelin/contracts": "^5.0.2",
14-
"@poolzfinance/poolz-helper-v2": "^3.0.0"
14+
"@poolzfinance/poolz-helper-v2": "^3.0.4"
1515
},
1616
"devDependencies": {
1717
"@ethersproject/bignumber": "^5.7.0",

‎test/DealProvider.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ describe('Deal Provider', function () {
8080
await lockDealNFT
8181
.connect(receiver)
8282
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
83-
const params = [amount / 2];
83+
const params = [0];
8484
const poolData = await lockDealNFT.getData(poolId);
85-
expect(poolData).to.deep.equal([dealProvider.address, name, poolId, vaultId, receiver.address, token, params]);
85+
expect(poolData).to.deep.equal([dealProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
8686
});
8787

8888
it('should check data in new pool after split', async () => {
@@ -103,6 +103,23 @@ describe('Deal Provider', function () {
103103
]);
104104
});
105105

106+
it('should check data in newly copied pool after split', async () => {
107+
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
108+
await lockDealNFT
109+
.connect(receiver)['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
110+
const params = [amount / 2];
111+
const poolData = await lockDealNFT.getData(poolId + 2);
112+
expect(poolData).to.deep.equal([
113+
dealProvider.address,
114+
name,
115+
poolId + 2,
116+
vaultId,
117+
newOwner.address,
118+
token,
119+
params,
120+
]);
121+
});
122+
106123
it('should check data in new pool after selfSplit', async () => {
107124
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256'], [ratio]);
108125
await lockDealNFT

‎test/LockDealNFT.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('LockDealNFT', function () {
6868

6969
it('should set provider address', async () => {
7070
await lockDealNFT.setApprovedContract(dealProvider.address, true);
71-
expect(await lockDealNFT.approvedContracts(dealProvider.address)).to.be.true;
71+
expect(await lockDealNFT.approvedContracts(dealProvider.address)).to.equal(true);
7272
});
7373

7474
it('should return ContractApproved event', async () => {
@@ -316,7 +316,7 @@ describe('LockDealNFT', function () {
316316
});
317317

318318
it('check if the contract supports ILockDealNFT interface', async () => {
319-
expect(await lockDealNFT.supportsInterface('0x1137c976')).to.equal(true);
319+
expect(await lockDealNFT.supportsInterface('0x5f203be1')).to.equal(true);
320320
});
321321

322322
it('shuld return royalty', async () => {

‎test/LockDealProvider.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,19 @@ describe('Lock Deal Provider', function () {
9090
await lockDealNFT
9191
.connect(receiver)
9292
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
93-
const params = [amount / 2, startTime];
93+
const params = [0, startTime];
9494
const poolData = await lockDealNFT.getData(poolId);
95-
expect(poolData).to.deep.equal([lockProvider.address, name, poolId, vaultId, receiver.address, token, params]);
95+
expect(poolData).to.deep.equal([lockProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
96+
});
97+
98+
it('should check data in newly copied pool after split', async () => {
99+
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
100+
await lockDealNFT
101+
.connect(receiver)
102+
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
103+
const params = [amount / 2, startTime];
104+
const poolData = await lockDealNFT.getData(poolId + 2);
105+
expect(poolData).to.deep.equal([lockProvider.address, name, poolId + 2, vaultId, newOwner.address, token, params]);
96106
});
97107

98108
it('should check data in new pool after split', async () => {

‎test/TimedDealProvider.ts

+63-17
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ describe('Timed Deal Provider', function () {
6161
expect(await timedDealProvider.name()).to.equal('TimedDealProvider');
6262
});
6363

64+
it('check if the contract supports IBeforeTransfer interface', async () => {
65+
expect(await timedDealProvider.supportsInterface('0x1ffb811f')).to.equal(true);
66+
});
67+
6468
it('should get timed provider data after creation', async () => {
6569
const poolData = await lockDealNFT.getData(poolId);
6670
const params = [amount, startTime, finishTime, amount];
@@ -140,9 +144,9 @@ describe('Timed Deal Provider', function () {
140144
await lockDealNFT
141145
.connect(receiver)
142146
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
143-
const params = [amount / 2, startTime, finishTime, amount / 2];
147+
const params = [0, startTime, finishTime, amount];
144148
const poolData = await lockDealNFT.getData(poolId);
145-
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, receiver.address, token, params]);
149+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
146150
});
147151

148152
it('should check data in new pool after split', async () => {
@@ -155,6 +159,16 @@ describe('Timed Deal Provider', function () {
155159
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId + 1, vaultId, newOwner.address, token, params]);
156160
});
157161

162+
it('should check data in newly copied pool after split', async () => {
163+
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
164+
await lockDealNFT
165+
.connect(receiver)
166+
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
167+
const params = [amount / 2, startTime, finishTime, amount / 2];
168+
const poolData = await lockDealNFT.getData(poolId + 2);
169+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId + 2, vaultId, newOwner.address, token, params]);
170+
});
171+
158172
it('should check event data after split', async () => {
159173
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
160174
const tx = await lockDealNFT
@@ -166,7 +180,7 @@ describe('Timed Deal Provider', function () {
166180
expect(events[events.length - 1].args.newPoolId).to.equal(poolId + 1);
167181
expect(events[events.length - 1].args.owner).to.equal(receiver.address);
168182
expect(events[events.length - 1].args.newOwner).to.equal(newOwner.address);
169-
expect(events[events.length - 1].args.splitLeftAmount).to.equal(amount / 2);
183+
expect(events[events.length - 1].args.splitLeftAmount).to.equal(0);
170184
expect(events[events.length - 1].args.newSplitLeftAmount).to.equal(amount / 2);
171185
});
172186

@@ -179,9 +193,9 @@ describe('Timed Deal Provider', function () {
179193
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
180194
await lockDealNFT
181195
.connect(receiver)
182-
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
183-
const poolData = await lockDealNFT.getData(poolId);
184-
const newPoolData = await lockDealNFT.getData(poolId + 1);
196+
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId + 1, packedData);
197+
const poolData = await lockDealNFT.getData(poolId + 2);
198+
const newPoolData = await lockDealNFT.getData(poolId + 3);
185199
expect(poolData.params[3].add(newPoolData.params[3])).to.equal(amount);
186200
expect(poolData.params[0].add(newPoolData.params[0])).to.equal(amount - amount / 10);
187201
});
@@ -196,9 +210,9 @@ describe('Timed Deal Provider', function () {
196210
const packedData = ethers.utils.defaultAbiCoder.encode(['uint256', 'address'], [ratio, newOwner.address]);
197211
await lockDealNFT
198212
.connect(receiver)
199-
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId, packedData);
200-
const poolData = await lockDealNFT.getData(poolId);
201-
const newPoolData = await lockDealNFT.getData(poolId + 1);
213+
['safeTransferFrom(address,address,uint256,bytes)'](receiver.address, lockDealNFT.address, poolId + 1, packedData);
214+
const poolData = await lockDealNFT.getData(poolId + 2);
215+
const newPoolData = await lockDealNFT.getData(poolId + 3);
202216

203217
expect(poolData.params[3].add(newPoolData.params[3])).to.equal(amount);
204218
expect(poolData.params[0].add(newPoolData.params[0])).to.equal(amount - amount / 4);
@@ -210,29 +224,29 @@ describe('Timed Deal Provider', function () {
210224
expect(await timedDealProvider.getWithdrawableAmount(poolId)).to.equal(0);
211225
});
212226

213-
it('should withdraw 25% tokens', async () => {
227+
it('should check old pool after withdraw 25% tokens', async () => {
214228
await time.setNextBlockTimestamp(startTime + halfTime / 2);
215229

216230
await lockDealNFT
217231
.connect(receiver)
218232
['safeTransferFrom(address,address,uint256)'](receiver.address, lockDealNFT.address, poolId);
219-
const params = [amount - amount / 4, startTime, finishTime, amount];
233+
const params = [0, startTime, finishTime, amount];
220234
const poolData = await lockDealNFT.getData(poolId);
221-
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, receiver.address, token, params]);
235+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
222236
});
223237

224-
it('should withdraw half tokens', async () => {
238+
it('should check old pool after withdraw half tokens', async () => {
225239
await time.setNextBlockTimestamp(startTime + halfTime);
226240

227241
await lockDealNFT
228242
.connect(receiver)
229243
['safeTransferFrom(address,address,uint256)'](receiver.address, lockDealNFT.address, poolId);
230-
const params = [amount / 2, startTime, finishTime, amount];
244+
const params = [0, startTime, finishTime, amount];
231245
const poolData = await lockDealNFT.getData(poolId);
232-
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, receiver.address, token, params]);
246+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
233247
});
234248

235-
it('should withdraw all tokens', async () => {
249+
it('should check old pool after withdraw all tokens', async () => {
236250
await time.setNextBlockTimestamp(finishTime + 1);
237251

238252
await lockDealNFT
@@ -242,6 +256,38 @@ describe('Timed Deal Provider', function () {
242256
const poolData = await lockDealNFT.getData(poolId);
243257
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, lockDealNFT.address, token, params]);
244258
});
259+
260+
it('should check new pool after withdraw 25% tokens', async () => {
261+
await time.setNextBlockTimestamp(startTime + halfTime / 2);
262+
263+
await lockDealNFT
264+
.connect(receiver)
265+
['safeTransferFrom(address,address,uint256)'](receiver.address, lockDealNFT.address, poolId);
266+
const params = [amount - amount / 4, startTime, finishTime, amount];
267+
const poolData = await lockDealNFT.getData(poolId + 1);
268+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId + 1, vaultId, receiver.address, token, params]);
269+
});
270+
271+
it('should check new pool after withdraw half tokens', async () => {
272+
await time.setNextBlockTimestamp(startTime + halfTime);
273+
274+
await lockDealNFT
275+
.connect(receiver)
276+
['safeTransferFrom(address,address,uint256)'](receiver.address, lockDealNFT.address, poolId);
277+
const params = [amount / 2, startTime, finishTime, amount];
278+
const poolData = await lockDealNFT.getData(poolId + 1);
279+
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId + 1, vaultId, receiver.address, token, params]);
280+
});
281+
282+
it('should not create new pool after withdraw all tokens', async () => {
283+
await time.setNextBlockTimestamp(finishTime + 1);
284+
285+
await lockDealNFT
286+
.connect(receiver)
287+
['safeTransferFrom(address,address,uint256)'](receiver.address, lockDealNFT.address, poolId);
288+
const poolData = await lockDealNFT.getData(poolId + 1);
289+
expect(poolData).to.deep.equal([constants.AddressZero, "", 0, 0, constants.AddressZero, constants.AddressZero, []]);
290+
});
245291
});
246292

247293
describe('test higher cascading providers', () => {
@@ -261,7 +307,7 @@ describe('Timed Deal Provider', function () {
261307
it('should withdraw half tokens with higher mock provider', async () => {
262308
await mockProvider.withdraw(poolId, amount / 2);
263309
const poolData = await lockDealNFT.getData(poolId);
264-
const params = [amount / 2, startTime, finishTime, amount];
310+
const params = [0, startTime, finishTime, amount];
265311
expect(poolData).to.deep.equal([timedDealProvider.address, name, poolId, vaultId, receiver.address, token, params]);
266312
});
267313

0 commit comments

Comments
 (0)
Please sign in to comment.