Skip to content

Commit 3d2d071

Browse files
authored
chore(protocol-contracts): check for null address when withdrawing (#1282)
* chore(protocol-contracts): check for null address when withdrawing * chore(protocol-contracts): fix typo
1 parent efd8bbf commit 3d2d071

File tree

4 files changed

+55
-6
lines changed

4 files changed

+55
-6
lines changed

protocol-contracts/governance/contracts/GovernanceOAppSender.sol

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ contract GovernanceOAppSender is OAppSender, OAppOptionsType3 {
2222
error FailedToWithdrawETH();
2323
/// @notice Thrown when trying to send cross-chain tx if contract holds unsufficient ETH to pay the LZ fees.
2424
error InsufficientBalanceForFee();
25+
/// @notice Thrown when recipient is the null address.
26+
error InvalidNullRecipient();
2527
/// @notice Thrown when trying to deploy this contract on an unsupported blockchain.
2628
error UnsupportedChainID();
2729

@@ -167,8 +169,10 @@ contract GovernanceOAppSender is OAppSender, OAppOptionsType3 {
167169

168170
/// @dev Allows the owner to withdraw ETH held by the contract.
169171
/// @param amount Amount of withdrawn ETH.
170-
/// @param recipient Receiver of the withdrawn ETH.
172+
/// @param recipient Receiver of the withdrawn ETH, should be non-null.
171173
function withdrawETH(uint256 amount, address recipient) external onlyOwner {
174+
if (recipient == address(0)) revert InvalidNullRecipient();
175+
172176
(bool success, ) = recipient.call{ value: amount }("");
173177
if (!success) {
174178
revert FailedToWithdrawETH();

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
22
import { expect } from 'chai'
33
import { Contract, ContractFactory } from 'ethers'
44
import { deployments, ethers } from 'hardhat'
5-
import { execTransaction } from './utils/execTransaction.ts'
5+
import { execTransaction } from './utils/execTransaction'
66

77
import { Options } from '@layerzerolabs/lz-v2-utilities'
88
import { EndpointId } from '@layerzerolabs/lz-definitions'
@@ -259,6 +259,25 @@ 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 () {
263+
await owner.sendTransaction({
264+
to: governanceOAppSender.address,
265+
value: ethers.utils.parseEther('1'),
266+
})
267+
const tx = governanceOAppSender
268+
.connect(owner)
269+
.withdrawETH(ethers.utils.parseEther('1'), ethers.constants.AddressZero)
270+
try {
271+
await tx
272+
expect.fail('withdrawETH should have reverted with InvalidNullRecipient')
273+
} catch (err: any) {
274+
const data = err.data
275+
const selector = data.slice(0, 10)
276+
const expected = governanceOAppSender.interface.getSighash('InvalidNullRecipient()')
277+
expect(selector).to.equal(expected)
278+
}
279+
})
280+
262281
it('should send a payable remote proposal', async function () {
263282
expect(BigInt(await gatewayConfigMock.value())).to.equal(0n)
264283
await owner.sendTransaction({

protocol-contracts/token/contracts/ZamaERC20.sol

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ contract ZamaERC20 is ERC20, ERC20Permit, ERC1363, ERC20Burnable, AccessControl,
2121
event ERC20Recovered(address indexed token, address indexed recipient, uint256 amount);
2222
event ERC721Recovered(address indexed token, uint256 tokenId, address indexed recipient);
2323

24-
error FailedToSendEther();
2524
error AmountsReceiversLengthMismatch();
25+
error FailedToSendEther();
26+
error InvalidNullRecipient();
2627

2728
/**
2829
* @param name Name of the token.
@@ -79,10 +80,11 @@ contract ZamaERC20 is ERC20, ERC20Permit, ERC1363, ERC20Burnable, AccessControl,
7980
/**
8081
* @dev Allows the sender to recover Ether held by the contract.
8182
* @param amount Amount of recovered ETH.
82-
* @param recipient Receiver of the recovered ETH.
83+
* @param recipient Receiver of the recovered ETH, should be non-null.
8384
* @dev Emits an EtherRecovered event upon success.
8485
*/
8586
function recoverEther(uint256 amount, address recipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
87+
if (recipient == address(0)) revert InvalidNullRecipient();
8688
(bool success, ) = recipient.call{ value: amount }("");
8789
if (!success) {
8890
revert FailedToSendEther();
@@ -94,10 +96,11 @@ contract ZamaERC20 is ERC20, ERC20Permit, ERC1363, ERC20Burnable, AccessControl,
9496
* @dev Allows the sender to recover ERC20 tokens held by the contract.
9597
* @param token The address of the ERC20 token to recover.
9698
* @param amount The amount of the ERC20 token to recover.
97-
* @param recipient Receiver of the recovered tokens.
99+
* @param recipient Receiver of the recovered tokens, should be non-null.
98100
* @dev Emits an ERC20Recovered event upon success.
99101
*/
100102
function recoverERC20(address token, uint256 amount, address recipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
103+
if (recipient == address(0)) revert InvalidNullRecipient();
101104
IERC20(token).safeTransfer(recipient, amount);
102105
emit ERC20Recovered(token, recipient, amount);
103106
}
@@ -106,10 +109,11 @@ contract ZamaERC20 is ERC20, ERC20Permit, ERC1363, ERC20Burnable, AccessControl,
106109
* @dev Allows the sender to recover ERC721 tokens held by the contract.
107110
* @param token The address of the ERC721 token to recover.
108111
* @param tokenId The token ID of the ERC721 token to recover.
109-
* @param recipient Receiver of the recovered ERC721 token.
112+
* @param recipient Receiver of the recovered ERC721 token, should be non-null.
110113
* @dev Emits an ERC721Recovered event upon success.
111114
*/
112115
function recoverERC721(address token, uint256 tokenId, address recipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
116+
if (recipient == address(0)) revert InvalidNullRecipient();
113117
IERC721(token).safeTransferFrom(address(this), recipient, tokenId);
114118
emit ERC721Recovered(token, tokenId, recipient);
115119
}

protocol-contracts/token/test/ZamaERC20.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,12 @@ describe('ZamaERC20 - Unit Test', () => {
679679
expect(etherAliceBalanceAfter).to.eq(etherAliceBalanceBefore.add(SEND_AMOUNT))
680680
})
681681

682+
it('should not send recovered ETH to null address', async () => {
683+
await expect(
684+
zamaERC20.connect(admin).recoverEther(SEND_AMOUNT, ethers.constants.AddressZero)
685+
).to.be.revertedWithCustomError(zamaERC20, 'InvalidNullRecipient')
686+
})
687+
682688
it('should fail when trying to recover more than available balance', async () => {
683689
await expect(
684690
zamaERC20.connect(admin).recoverEther(SEND_AMOUNT.mul(2), alice.address)
@@ -732,6 +738,14 @@ describe('ZamaERC20 - Unit Test', () => {
732738
expect(mockERC20AliceBalanceAfter).to.eq(mockERC20AliceBalanceBefore.add(SEND_AMOUNT))
733739
})
734740

741+
it('should not send recovered ERC20 to null address', async () => {
742+
await expect(
743+
zamaERC20
744+
.connect(admin)
745+
.recoverERC20(zamaERC20.address, SEND_AMOUNT, ethers.constants.AddressZero)
746+
).to.be.revertedWithCustomError(zamaERC20, 'InvalidNullRecipient')
747+
})
748+
735749
it('should let DEFAULT_ADMIN_ROLE recover 0 $ZAMA from contract while unpaused', async () => {
736750
const mockERC20ContractBalanceBefore = await zamaERC20.balanceOf(zamaERC20.address)
737751
const mockERC20AliceBalanceBefore = await zamaERC20.balanceOf(alice.address)
@@ -909,6 +923,14 @@ describe('ZamaERC20 - Unit Test', () => {
909923
expect(await ERC721Mock.ownerOf(TOKEN_ID)).to.eq(alice.address)
910924
})
911925

926+
it('should not send recovered ERC721 to null address', async () => {
927+
await expect(
928+
zamaERC20
929+
.connect(admin)
930+
.recoverERC721(ERC721Mock.address, TOKEN_ID, ethers.constants.AddressZero)
931+
).to.be.revertedWithCustomError(zamaERC20, 'InvalidNullRecipient')
932+
})
933+
912934
it('should not let MINTER_ROLE recover ERC721Mock from contract while paused', async () => {
913935
await expect(
914936
zamaERC20.connect(alice).recoverERC721(ERC721Mock.address, TOKEN_ID, alice.address)

0 commit comments

Comments
 (0)