Skip to content

Commit 828dbc3

Browse files
authored
Add stake management function to ERC4337Utils (#5471)
1 parent 840c974 commit 828dbc3

File tree

4 files changed

+106
-5
lines changed

4 files changed

+106
-5
lines changed

.changeset/chilly-guests-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC4337Utils`: Add functions to manage deposit and stake on the paymaster.

contracts/account/utils/draft-ERC4337Utils.sol

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
pragma solidity ^0.8.20;
55

6-
import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
6+
import {IEntryPoint, PackedUserOperation} from "../../interfaces/draft-IERC4337.sol";
77
import {Math} from "../../utils/math/Math.sol";
88
import {Calldata} from "../../utils/Calldata.sol";
99
import {Packing} from "../../utils/Packing.sol";
@@ -16,6 +16,9 @@ import {Packing} from "../../utils/Packing.sol";
1616
library ERC4337Utils {
1717
using Packing for *;
1818

19+
/// @dev Address of the entrypoint v0.7.0
20+
IEntryPoint internal constant ENTRYPOINT = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032);
21+
1922
/// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success.
2023
uint256 internal constant SIG_VALIDATION_SUCCESS = 0;
2124

@@ -160,4 +163,29 @@ library ERC4337Utils {
160163
function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) {
161164
return self.paymasterAndData.length < 52 ? Calldata.emptyBytes() : self.paymasterAndData[52:];
162165
}
166+
167+
/// @dev Deposit ether into the entrypoint.
168+
function depositTo(address to, uint256 value) internal {
169+
ENTRYPOINT.depositTo{value: value}(to);
170+
}
171+
172+
/// @dev Withdraw ether from the entrypoint.
173+
function withdrawTo(address payable to, uint256 value) internal {
174+
ENTRYPOINT.withdrawTo(to, value);
175+
}
176+
177+
/// @dev Add stake to the entrypoint.
178+
function addStake(uint256 value, uint32 unstakeDelaySec) internal {
179+
ENTRYPOINT.addStake{value: value}(unstakeDelaySec);
180+
}
181+
182+
/// @dev Unlock stake on the entrypoint.
183+
function unlockStake() internal {
184+
ENTRYPOINT.unlockStake();
185+
}
186+
187+
/// @dev Withdraw unlocked stake from the entrypoint.
188+
function withdrawStake(address payable to) internal {
189+
ENTRYPOINT.withdrawStake(to);
190+
}
163191
}

test/account/utils/draft-ERC4337Utils.test.js

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
44

55
const { packValidationData, UserOperation } = require('../../helpers/erc4337');
66
const { MAX_UINT48 } = require('../../helpers/constants');
7+
const time = require('../../helpers/time');
78
const ADDRESS_ONE = '0x0000000000000000000000000000000000000001';
89

910
const fixture = async () => {
10-
const [authorizer, sender, factory, paymaster] = await ethers.getSigners();
11+
const [authorizer, sender, factory, paymaster, other] = await ethers.getSigners();
1112
const utils = await ethers.deployContract('$ERC4337Utils');
1213
const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS();
1314
const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED();
1415

15-
return { utils, authorizer, sender, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
16+
return { utils, authorizer, sender, factory, paymaster, other, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED };
1617
};
1718

1819
describe('ERC4337Utils', function () {
@@ -284,4 +285,68 @@ describe('ERC4337Utils', function () {
284285
});
285286
});
286287
});
288+
289+
describe('stake management', function () {
290+
const unstakeDelaySec = 3600n;
291+
292+
beforeEach(async function () {
293+
await this.authorizer.sendTransaction({ to: this.utils, value: ethers.parseEther('1') });
294+
});
295+
296+
it('deposit & withdraw', async function () {
297+
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(0n);
298+
299+
// deposit
300+
await expect(this.utils.$depositTo(this.utils, 42n)).to.changeEtherBalances(
301+
[this.utils, entrypoint],
302+
[-42n, 42n],
303+
);
304+
305+
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(42n);
306+
307+
// withdraw
308+
await expect(this.utils.$withdrawTo(this.other, 17n)).to.changeEtherBalances(
309+
[entrypoint, this.other],
310+
[-17n, 17n],
311+
);
312+
313+
await expect(entrypoint.balanceOf(this.utils)).to.eventually.equal(25n); // 42 - 17
314+
});
315+
316+
it('stake, unlock & withdraw stake', async function () {
317+
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, false, 0n, 0n, 0n]);
318+
319+
// stake
320+
await expect(this.utils.$addStake(42n, unstakeDelaySec)).to.changeEtherBalances(
321+
[this.utils, entrypoint],
322+
[-42n, 42n],
323+
);
324+
325+
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, true, 42n, unstakeDelaySec, 0n]);
326+
327+
// unlock
328+
const unlockTx = this.utils.$unlockStake();
329+
await expect(unlockTx).to.changeEtherBalances([this.utils, entrypoint], [0n, 0n]); // no ether movement
330+
331+
const timestamp = await time.clockFromReceipt.timestamp(unlockTx);
332+
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([
333+
0n,
334+
false,
335+
42n,
336+
unstakeDelaySec,
337+
timestamp + unstakeDelaySec,
338+
]);
339+
340+
// wait
341+
await time.increaseBy.timestamp(unstakeDelaySec);
342+
343+
// withdraw stake
344+
await expect(this.utils.$withdrawStake(this.other)).to.changeEtherBalances(
345+
[this.utils, entrypoint, this.other],
346+
[0n, -42n, 42n],
347+
);
348+
349+
await expect(entrypoint.deposits(this.utils)).to.eventually.deep.equal([0n, false, 0n, 0n, 0n]);
350+
});
351+
});
287352
});

test/helpers/time.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ const clock = {
77
timestamp: () => time.latest().then(ethers.toBigInt),
88
};
99
const clockFromReceipt = {
10-
blocknumber: receipt => Promise.resolve(ethers.toBigInt(receipt.blockNumber)),
11-
timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => ethers.toBigInt(block.timestamp)),
10+
blocknumber: receipt => Promise.resolve(receipt).then(({ blockNumber }) => ethers.toBigInt(blockNumber)),
11+
timestamp: receipt =>
12+
Promise.resolve(receipt)
13+
.then(({ blockNumber }) => ethers.provider.getBlock(blockNumber))
14+
.then(({ timestamp }) => ethers.toBigInt(timestamp)),
1215
};
1316
const increaseBy = {
1417
blockNumber: mine,

0 commit comments

Comments
 (0)