Skip to content

Commit b29ca78

Browse files
authored
chore(protocol-contracts): add withdraw function to oapp receiver (#1306)
1 parent cc37dcc commit b29ca78

File tree

2 files changed

+64
-2
lines changed

2 files changed

+64
-2
lines changed

protocol-contracts/governance/contracts/GovernanceOAppReceiver.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ contract GovernanceOAppReceiver is OAppReceiver {
2525
error AdminSafeModuleNotSet();
2626
/// @notice Thrown when trying to set the adminSafeModule to the null address.
2727
error AdminSafeModuleIsNull();
28+
/// @notice Thrown when failing to withdraw ETH from the contract.
29+
error FailedToWithdrawETH();
30+
/// @notice Thrown when recipient is the null address.
31+
error InvalidNullRecipient();
2832

2933
/// @notice Emitted when a new Safe AdminModule has been setup.
3034
event AdminSafeModuleUpdated(address indexed oldModule, address indexed newModule);
@@ -38,6 +42,8 @@ contract GovernanceOAppReceiver is OAppReceiver {
3842
bytes[] datas,
3943
Operation[] operations
4044
);
45+
/// @notice Emitted when ETH has been successfully withdrawn from the contract.
46+
event WithdrawnETH(address indexed recipient, uint256 amount);
4147

4248
/// @notice Initialize with Endpoint V2 and owner address.
4349
/// @param endpoint The local chain's LayerZero Endpoint V2 address.
@@ -90,4 +96,17 @@ contract GovernanceOAppReceiver is OAppReceiver {
9096

9197
emit ProposalExecuted(origin, guid, targets, values, functionSignatures, datas, operations);
9298
}
99+
100+
/// @dev Allows the owner to withdraw ETH held by the contract.
101+
/// @param amount Amount of withdrawn ETH.
102+
/// @param recipient Receiver of the withdrawn ETH, should be non-null.
103+
function withdrawETH(uint256 amount, address recipient) external onlyOwner {
104+
if (recipient == address(0)) revert InvalidNullRecipient();
105+
106+
(bool success, ) = recipient.call{ value: amount }("");
107+
if (!success) {
108+
revert FailedToWithdrawETH();
109+
}
110+
emit WithdrawnETH(recipient, amount);
111+
}
93112
}

protocol-contracts/governance/test/GovernanceOApp.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ describe('GovernanceOApp Test', function () {
237237
expect(BigInt(await gatewayConfigMockBis.value())).to.equal(2n)
238238
})
239239

240-
it('owner can wihdraw ETH from prefunded governanceOAppSender', async function () {
240+
it('owner can wihdraw ETH from prefunded GovernanceOAppSender', async function () {
241241
await owner.sendTransaction({
242242
to: governanceOAppSender.address,
243243
value: ethers.utils.parseEther('1'),
@@ -259,7 +259,7 @@ describe('GovernanceOApp Test', function () {
259259
expect(diff <= tolerance).to.equal(true)
260260
})
261261

262-
it('should not send recovered funds to null address', async function () {
262+
it('should not send recovered funds from GovernanceOAppSender to null address', async function () {
263263
await owner.sendTransaction({
264264
to: governanceOAppSender.address,
265265
value: ethers.utils.parseEther('1'),
@@ -278,6 +278,49 @@ describe('GovernanceOApp Test', function () {
278278
}
279279
})
280280

281+
it('owner can wihdraw ETH from prefunded GovernanceOAppReceiver', async function () {
282+
await ethers.provider.send('hardhat_setBalance', [
283+
// on a real network, funds could be sent to GovernanceOAppReceiver contract via the payable lzReceive method
284+
governanceOAppReceiver.address,
285+
'0xde0b6b3a7640000', // 1 ETH in hex
286+
])
287+
288+
const balanceOwnerBefore = await ethers.provider.getBalance(owner.address)
289+
const balanceGovReceiverBefore = await ethers.provider.getBalance(governanceOAppReceiver.address)
290+
expect(balanceGovReceiverBefore.toBigInt()).to.equal(ethers.utils.parseEther('1').toBigInt())
291+
292+
await governanceOAppReceiver.withdrawETH(ethers.utils.parseEther('1'), owner.address)
293+
294+
const balanceOwnerAfter = await ethers.provider.getBalance(owner.address)
295+
const balanceGovReceiverAfter = await ethers.provider.getBalance(governanceOAppReceiver.address)
296+
expect(balanceGovReceiverAfter.toBigInt()).to.equal(0n)
297+
const received = balanceOwnerAfter.sub(balanceOwnerBefore).toBigInt()
298+
const expected = ethers.utils.parseEther('1').toBigInt()
299+
const tolerance = ethers.utils.parseEther('0.0001').toBigInt() // account gas used for the tx
300+
const diff = received > expected ? received - expected : expected - received
301+
expect(diff <= tolerance).to.equal(true)
302+
})
303+
304+
it('should not send recovered funds from GovernanceOAppReceiver to null address', async function () {
305+
await ethers.provider.send('hardhat_setBalance', [
306+
// on a real network, funds could be sent to GovernanceOAppReceiver contract via the payable lzReceive method
307+
governanceOAppReceiver.address,
308+
'0xde0b6b3a7640000', // 1 ETH in hex
309+
])
310+
const tx = governanceOAppReceiver
311+
.connect(owner)
312+
.withdrawETH(ethers.utils.parseEther('1'), ethers.constants.AddressZero)
313+
try {
314+
await tx
315+
expect.fail('withdrawETH should have reverted with InvalidNullRecipient')
316+
} catch (err: any) {
317+
const data = err.data
318+
const selector = data.slice(0, 10)
319+
const expected = governanceOAppReceiver.interface.getSighash('InvalidNullRecipient()')
320+
expect(selector).to.equal(expected)
321+
}
322+
})
323+
281324
it('should send a payable remote proposal', async function () {
282325
expect(BigInt(await gatewayConfigMock.value())).to.equal(0n)
283326
await owner.sendTransaction({

0 commit comments

Comments
 (0)