Skip to content

Commit 17e9b34

Browse files
committed
Merge remote-tracking branch 'origin/master' into web-vitals
2 parents d0e788e + 4e6aee7 commit 17e9b34

36 files changed

+1347
-460
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright 2019-2024 Rosco Kalis
1+
Copyright 2019-2025 Rosco Kalis
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
44

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ If you want to learn more about (unlimited) token approvals, I wrote an article
1111
## Running locally
1212

1313
```
14-
git clone [email protected]:rkalis/revoke.cash.git
14+
git clone [email protected]:RevokeCash/revoke.cash.git
1515
cd revoke.cash
1616
yarn
1717
yarn dev
@@ -51,11 +51,11 @@ Adding a new network is relatively straightforward as you only need to change th
5151

5252
#### Prerequisites
5353

54-
To add a new network, one of the following needs to be available:
54+
To add a new network, **one** of the following needs to be available:
5555

5656
- A (public or private) RPC endpoint that supports `eth_getLogs` requests for the entire history of the network.
57-
- Support in [CovalentHQ](https://www.covalenthq.com/) for the network.
58-
- A block explorer with an exposed API that is compatible with Etherscan's API (such as Blockscout).
57+
- Or: Support in [CovalentHQ](https://www.covalenthq.com/) for the network.
58+
- Or: A block explorer with an exposed API that is compatible with Etherscan's API (such as Blockscout).
5959

6060
Also make sure that your network is listed in [ethereum-lists/chains](https://github.com/ethereum-lists/chains) (and that it has subsequently been included in [@revoke.cash/chains](https://github.com/RevokeCash/chains)). Besides the earlier requirements, we also require a publicly available RPC endpoint with rate limits that are not too restrictive. It is also helpful if your network is listed (with TVL and volume stats) on DeFiLlama, but this is not required.
6161

app/[locale]/private/approve/page.tsx

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
'use client';
2+
3+
import ContentPageLayout from 'app/layouts/ContentPageLayout';
4+
import Button from 'components/common/Button';
5+
import Input from 'components/common/Input';
6+
import { displayTransactionSubmittedToast } from 'components/common/TransactionSubmittedToast';
7+
import Select from 'components/common/select/Select';
8+
import { ERC20_ABI, ERC721_ABI } from 'lib/abis';
9+
import { writeContractUnlessExcessiveGas } from 'lib/utils';
10+
import { AllowanceType } from 'lib/utils/allowances';
11+
import { parseErrorMessage } from 'lib/utils/errors';
12+
import { permit2Approve } from 'lib/utils/permit2';
13+
import { useState } from 'react';
14+
import { toast } from 'react-toastify';
15+
import { isAddress } from 'viem';
16+
import { useAccount, usePublicClient } from 'wagmi';
17+
import { useWalletClient } from 'wagmi';
18+
19+
const ApprovePage = () => {
20+
const { data: walletClient } = useWalletClient();
21+
const { address: account } = useAccount();
22+
const publicClient = usePublicClient()!;
23+
const [allowanceType, setAllowanceType] = useState<AllowanceType>(AllowanceType.ERC20);
24+
const [tokenAddress, setTokenAddress] = useState<string>('');
25+
const [spenderAddress, setSpenderAddress] = useState<string>('');
26+
const [permit2Address, setPermit2Address] = useState<string>('');
27+
const [amount, setAmount] = useState<string>('');
28+
const [tokenId, setTokenId] = useState<string>('');
29+
const [expiration, setExpiration] = useState<string>('');
30+
const options = Object.values(AllowanceType).map((type) => ({ value: type, label: type }));
31+
32+
const handleApprove = async () => {
33+
try {
34+
if (!tokenAddress || !spenderAddress) {
35+
throw new Error('Token address and spender address are required');
36+
}
37+
38+
if (allowanceType === AllowanceType.ERC721_SINGLE && !tokenId) {
39+
throw new Error('Token ID is required');
40+
}
41+
42+
if (allowanceType === AllowanceType.PERMIT2 && (!permit2Address || !expiration || !isAddress(permit2Address))) {
43+
throw new Error('Permit2 address and expiration are required');
44+
}
45+
46+
if (!isAddress(tokenAddress) || !isAddress(spenderAddress)) {
47+
throw new Error('Invalid address');
48+
}
49+
50+
switch (allowanceType) {
51+
case AllowanceType.ERC20: {
52+
if (!amount) {
53+
throw new Error('Amount is required');
54+
}
55+
56+
const tx = await writeContractUnlessExcessiveGas(publicClient, walletClient!, {
57+
address: tokenAddress,
58+
account: account!,
59+
chain: walletClient!.chain!,
60+
abi: ERC20_ABI,
61+
functionName: 'approve',
62+
args: [spenderAddress, BigInt(amount)],
63+
});
64+
65+
displayTransactionSubmittedToast(walletClient!.chain!.id, tx);
66+
break;
67+
}
68+
case AllowanceType.PERMIT2: {
69+
if (!amount || !expiration || !permit2Address || !isAddress(permit2Address)) {
70+
throw new Error('Amount, expiration, and permit2 address are required');
71+
}
72+
73+
const tokenContract = {
74+
address: tokenAddress,
75+
publicClient,
76+
abi: ERC20_ABI,
77+
};
78+
79+
const tx = await permit2Approve(
80+
permit2Address,
81+
walletClient!,
82+
tokenContract,
83+
spenderAddress,
84+
BigInt(amount),
85+
Number(expiration),
86+
);
87+
88+
displayTransactionSubmittedToast(walletClient!.chain!.id, tx);
89+
break;
90+
}
91+
case AllowanceType.ERC721_SINGLE: {
92+
if (!tokenId) {
93+
throw new Error('Token ID is required');
94+
}
95+
96+
const tx = await writeContractUnlessExcessiveGas(publicClient, walletClient!, {
97+
address: tokenAddress,
98+
account: account!,
99+
chain: walletClient!.chain!,
100+
abi: ERC721_ABI,
101+
functionName: 'approve',
102+
args: [spenderAddress, BigInt(tokenId)],
103+
});
104+
105+
displayTransactionSubmittedToast(walletClient!.chain!.id, tx);
106+
break;
107+
}
108+
case AllowanceType.ERC721_ALL: {
109+
const tx = await writeContractUnlessExcessiveGas(publicClient, walletClient!, {
110+
address: tokenAddress,
111+
account: account!,
112+
chain: walletClient!.chain!,
113+
abi: ERC721_ABI,
114+
functionName: 'setApprovalForAll',
115+
args: [spenderAddress, true],
116+
});
117+
118+
displayTransactionSubmittedToast(walletClient!.chain!.id, tx);
119+
break;
120+
}
121+
}
122+
} catch (e) {
123+
toast.error(e instanceof Error ? parseErrorMessage(e) : 'An error occurred');
124+
}
125+
};
126+
127+
return (
128+
<ContentPageLayout>
129+
<div className="flex flex-col gap-4 max-w-3xl mx-auto">
130+
<h1>Approve Arbitrary Contracts</h1>
131+
<p>For testing purposes only.</p>
132+
<div className="flex flex-col gap-4 border border-gray-200 rounded-md p-4">
133+
<div className="flex flex-col gap-1">
134+
<span>Approval Type</span>
135+
<Select
136+
instanceId="approval-type-select"
137+
aria-label="Select Approval Type"
138+
options={options}
139+
value={options.find((option) => option.value === allowanceType)}
140+
onChange={(value) => setAllowanceType(value?.value as AllowanceType)}
141+
isMulti={false}
142+
isSearchable={false}
143+
/>
144+
</div>
145+
<div className="flex flex-col gap-1">
146+
<span>Token Address</span>
147+
<Input
148+
size="md"
149+
placeholder="Token Address"
150+
value={tokenAddress}
151+
onChange={(e) => setTokenAddress(e.target.value)}
152+
/>
153+
</div>
154+
<div className="flex flex-col gap-1">
155+
<span>Spender Address</span>
156+
<Input
157+
size="md"
158+
placeholder="Spender Address"
159+
value={spenderAddress}
160+
onChange={(e) => setSpenderAddress(e.target.value)}
161+
/>
162+
</div>
163+
{allowanceType === AllowanceType.PERMIT2 && (
164+
<div className="flex flex-col gap-1">
165+
<span>Permit2 Address</span>
166+
<Input
167+
size="md"
168+
placeholder="Permit2 Address"
169+
value={permit2Address}
170+
onChange={(e) => setPermit2Address(e.target.value)}
171+
/>
172+
</div>
173+
)}
174+
{(allowanceType === AllowanceType.ERC20 || allowanceType === AllowanceType.PERMIT2) && (
175+
<div className="flex flex-col gap-1">
176+
<span>Amount</span>
177+
<Input size="md" placeholder="Amount" value={amount} onChange={(e) => setAmount(e.target.value)} />
178+
</div>
179+
)}
180+
{allowanceType === AllowanceType.ERC721_SINGLE && (
181+
<div className="flex flex-col gap-1">
182+
<span>Token ID</span>
183+
<Input size="md" placeholder="Token ID" value={tokenId} onChange={(e) => setTokenId(e.target.value)} />
184+
</div>
185+
)}
186+
{allowanceType === AllowanceType.PERMIT2 && (
187+
<div className="flex flex-col gap-1">
188+
<span>Expiration</span>
189+
<Input
190+
size="md"
191+
placeholder="Expiration"
192+
value={expiration}
193+
onChange={(e) => setExpiration(e.target.value)}
194+
/>
195+
</div>
196+
)}
197+
</div>
198+
<Button size="md" style="secondary" onClick={handleApprove}>
199+
Approve
200+
</Button>
201+
</div>
202+
</ContentPageLayout>
203+
);
204+
};
205+
206+
export default ApprovePage;

app/robots.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
User-agent: *
22
Allow: /
3+
Disallow: /private/
34

45
Sitemap: https://revoke.cash/sitemap.xml

components/allowances/controls/batch-revoke/BatchRevokeControls.tsx

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import Button from 'components/common/Button';
22
import TipSection from 'components/common/donate/TipSection';
33
import { useDonate } from 'lib/hooks/ethereum/useDonate';
44
import { useAddressPageContext } from 'lib/hooks/page-context/AddressPageContext';
5-
import { TokenAllowanceData } from 'lib/utils/allowances';
6-
import { analytics } from 'lib/utils/analytics';
5+
import type { TokenAllowanceData } from 'lib/utils/allowances';
76
import { useTranslations } from 'next-intl';
87
import { useState } from 'react';
98
import ControlsWrapper from '../ControlsWrapper';
@@ -13,38 +12,16 @@ interface Props {
1312
isRevoking: boolean;
1413
isAllConfirmed: boolean;
1514
setOpen: (open: boolean) => void;
16-
revoke: () => Promise<void>;
15+
revoke: (tipAmount: string) => Promise<void>;
1716
}
1817

1918
const BatchRevokeControls = ({ selectedAllowances, isRevoking, isAllConfirmed, setOpen, revoke }: Props) => {
2019
const t = useTranslations();
2120
const { address, selectedChainId } = useAddressPageContext();
21+
const { defaultAmount, nativeToken } = useDonate(selectedChainId, 'batch-revoke-tip');
2222

23-
const { donate, nativeToken, defaultAmount } = useDonate(selectedChainId, 'batch-revoke-tip');
2423
const [tipAmount, setTipAmount] = useState<string | null>(null);
2524

26-
const revokeAndTip = async (tipAmount: string | null) => {
27-
if (!tipAmount) throw new Error('Tip amount is required');
28-
29-
const getTipSelection = () => {
30-
if (tipAmount === '0') return 'none';
31-
if (Number(tipAmount) < Number(defaultAmount)) return 'low';
32-
if (Number(tipAmount) > Number(defaultAmount)) return 'high';
33-
return 'mid';
34-
};
35-
36-
analytics.track('Batch Revoked', {
37-
chainId: selectedChainId,
38-
address,
39-
allowances: selectedAllowances.length,
40-
amount: tipAmount,
41-
tipSelection: getTipSelection(),
42-
});
43-
44-
await revoke();
45-
await donate(tipAmount);
46-
};
47-
4825
const getButtonText = () => {
4926
if (isRevoking) return t('common.buttons.revoking');
5027
if (isAllConfirmed) return t('common.buttons.close');
@@ -53,7 +30,10 @@ const BatchRevokeControls = ({ selectedAllowances, isRevoking, isAllConfirmed, s
5330

5431
const getButtonAction = () => {
5532
if (isAllConfirmed) return () => setOpen(false);
56-
return () => revokeAndTip(tipAmount);
33+
return async () => {
34+
if (!tipAmount) throw new Error('Tip amount is required');
35+
await revoke(tipAmount);
36+
};
5737
};
5838

5939
return (

components/header/WalletIndicatorDropdown.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ interface Props {
1414
const WalletIndicatorDropdown = ({ size, style, className }: Props) => {
1515
const t = useTranslations();
1616

17-
const { address: account } = useAccount();
17+
const { address: account, chainId } = useAccount();
1818
const { domainName } = useNameLookup(account);
1919
const { disconnect } = useDisconnect();
2020

2121
return (
2222
<div className="flex whitespace-nowrap">
2323
{account ? (
2424
<DropdownMenu menuButton={domainName ?? shortenAddress(account, 4)}>
25-
<DropdownMenuItem href={`/address/${account}`} router retainSearchParams={['chainId']}>
25+
<DropdownMenuItem href={`/address/${account}?chainId=${chainId}`} router retainSearchParams={['chainId']}>
2626
{t('common.buttons.my_allowances')}
2727
</DropdownMenuItem>
2828
<DropdownMenuItem onClick={() => disconnect()}>{t('common.buttons.disconnect')}</DropdownMenuItem>

components/signatures/cells/CancelPermitCell.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const CancelPermitCell = ({ token, onCancel }: Props) => {
2121
const { address, selectedChainId } = useAddressPageContext();
2222
const handleTransaction = useHandleTransaction(selectedChainId);
2323

24-
const sendCancelTransaction = async (): Promise<TransactionSubmitted | undefined> => {
25-
if (isErc721Contract(token.contract)) return;
24+
const sendCancelTransaction = async (): Promise<TransactionSubmitted> => {
25+
if (isErc721Contract(token.contract)) throw new Error('Cannot cancel ERC721 tokens');
2626
const hash = await permit(walletClient!, token.contract, DUMMY_ADDRESS, 0n);
2727

2828
analytics.track('Cancelled Permit Signatures', {

content/en/exploits/short/moby.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Close to $2.5m was stolen from DeFi platform Moby Trade and its users. The attacker was able to gain access to the protocol's admin keys, which they used to execute malicious upgrades on the protocol's contracts - allowing them to drain the wallets of any users that had active token approvals for the protocol. Close to $1.5m was recovered by the protocol's team and SEAL911, resulting in a loss of $1m for the users.

cypress/e2e/chains.cy.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ const TEST_ADDRESSES = {
102102
[ChainId.Shibarium]: '0x8fA1F2969082a8d141DA3f0DD06D308C783fe7bB',
103103
[ChainId.Shiden]: '0xD377cFFCc52C16bF6e9840E77F78F42Ddb946568',
104104
[ChainId.ShimmerEVM]: '0xAc4682eF9fE8c62980cd8bd8d8a3Bb100FD652e7',
105+
[ChainId.Soneium]: '0x351F34efCE7BBF960da2ca61130a89bF41471047',
106+
[ChainId.SonicMainnet]: '0xA93093fc1D0343298966E1F971fAE10a7a629296',
105107
[ChainId['SongbirdCanary-Network']]: '0x4E8De52271D3bE18cC972af892198103C1e6AfE8',
106108
[ChainId.StoryOdysseyTestnet]: '0x2343bcb7f864D6e2880b3510492dc3da33E75f14',
107109
[ChainId.SyscoinMainnet]: '0xc594AE94f7C98d759Ed4c792F5DbFB7285184044',
@@ -122,7 +124,7 @@ const TEST_ADDRESSES = {
122124
[ChainId.ZkSyncMainnet]: '0x82FdF36736f3f8eE6f04Ab96eA32213c8d826FaA',
123125
[ChainId.Zora]: '0x061EFb2DF7767D6e63529BA99394037d4dCa39D6',
124126
// Testnets
125-
[ChainId.AbstractTestnet]: '0xe126b3E5d052f1F575828f61fEBA4f4f2603652a',
127+
[ChainId.AbstractSepoliaTestnet]: '0xe126b3E5d052f1F575828f61fEBA4f4f2603652a',
126128
[ChainId.Amoy]: '0x57BD9b2E821d2bF1f8136026ba3A29848eff9e47',
127129
[ChainId.ArbitrumSepolia]: '0xDd3287043493E0a08d2B348397554096728B459c',
128130
[ChainId.AvalancheFujiTestnet]: '0x4D915A2f0a2c94b159b69D36bc26338E0ef8E3F6',

lib/api/logs/EtherscanEventGetter.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,9 @@ const formatEtherscanEvent = (etherscanLog: any) => ({
121121
topics: etherscanLog.topics.filter((topic: string) => !isNullish(topic)),
122122
data: etherscanLog.data,
123123
transactionHash: etherscanLog.transactionHash,
124-
blockNumber: Number.parseInt(etherscanLog.blockNumber, 16),
125-
transactionIndex: Number.parseInt(etherscanLog.transactionIndex, 16),
126-
logIndex: Number.parseInt(etherscanLog.logIndex, 16),
124+
blockNumber: Number.parseInt(etherscanLog.blockNumber, 16) || 0,
125+
transactionIndex: Number.parseInt(etherscanLog.transactionIndex, 16) || 0,
126+
logIndex: Number.parseInt(etherscanLog.logIndex, 16) || 0,
127127
timestamp: Number.parseInt(etherscanLog.timeStamp, 16),
128128
});
129129

0 commit comments

Comments
 (0)