Skip to content

Commit ccadb18

Browse files
hashpalkericglau
andauthored
Add validation for ERC20 premint amount to prevent "Literal too large" error (#488)
Co-authored-by: Eric Lau <[email protected]>
1 parent c51fc81 commit ccadb18

File tree

7 files changed

+184
-0
lines changed

7 files changed

+184
-0
lines changed

packages/core/solidity/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Add validation for ERC20 premint field. ([#488](https://github.com/OpenZeppelin/contracts-wizard/pull/488))
6+
37
## 0.5.3 (2025-03-13)
48

59
- Add ERC20 Cross-Chain Bridging, SuperchainERC20. ([#436](https://github.com/OpenZeppelin/contracts-wizard/pull/436))

packages/core/solidity/src/erc20.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,43 @@ testERC20('erc20 premint of 0', {
6969
premint: '0',
7070
});
7171

72+
function testPremint(scenario: string, premint: string, expectedError?: string) {
73+
test(`erc20 premint - ${scenario} - ${expectedError ? 'invalid' : 'valid'}`, async t => {
74+
if (expectedError) {
75+
const error = t.throws(() =>
76+
buildERC20({
77+
name: 'MyToken',
78+
symbol: 'MTK',
79+
premint,
80+
}),
81+
);
82+
t.is((error as OptionsError).messages.premint, expectedError);
83+
} else {
84+
const c = buildERC20({
85+
name: 'MyToken',
86+
symbol: 'MTK',
87+
premint,
88+
});
89+
t.snapshot(printContract(c));
90+
}
91+
});
92+
}
93+
94+
testPremint('max literal', '115792089237316195423570985008687907853269984665640564039457.584007913129639935'); // 2^256 - 1, shifted by 18 decimals
95+
testPremint(
96+
'max literal + 1',
97+
'115792089237316195423570985008687907853269984665640564039457.584007913129639936',
98+
'Value is greater than uint256 max value',
99+
);
100+
testPremint('no arithmetic overflow', '115792089237316195423570985008687907853269984665640564039457'); // 2^256 - 1, truncated by 18 decimals
101+
testPremint(
102+
'arithmetic overflow',
103+
'115792089237316195423570985008687907853269984665640564039458',
104+
'Amount would overflow uint256 after applying decimals',
105+
);
106+
testPremint('e notation', '1e59');
107+
testPremint('e notation arithmetic overflow', '1e60', 'Amount would overflow uint256 after applying decimals');
108+
72109
testERC20('erc20 mintable', {
73110
mintable: true,
74111
access: 'ownable',

packages/core/solidity/src/erc20.test.ts.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,69 @@ Generated by [AVA](https://avajs.dev).
235235
}␊
236236
`
237237

238+
## erc20 premint - max literal - valid
239+
240+
> Snapshot 1
241+
242+
`// SPDX-License-Identifier: MIT␊
243+
// Compatible with OpenZeppelin Contracts ^5.0.0␊
244+
pragma solidity ^0.8.22;␊
245+
246+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
247+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
248+
249+
contract MyToken is ERC20, ERC20Permit {␊
250+
constructor(address recipient)␊
251+
ERC20("MyToken", "MTK")␊
252+
ERC20Permit("MyToken")␊
253+
{␊
254+
_mint(recipient, 115792089237316195423570985008687907853269984665640564039457584007913129639935 * 10 ** (decimals() - 18));␊
255+
}␊
256+
}␊
257+
`
258+
259+
## erc20 premint - no arithmetic overflow - valid
260+
261+
> Snapshot 1
262+
263+
`// SPDX-License-Identifier: MIT␊
264+
// Compatible with OpenZeppelin Contracts ^5.0.0␊
265+
pragma solidity ^0.8.22;␊
266+
267+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
268+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
269+
270+
contract MyToken is ERC20, ERC20Permit {␊
271+
constructor(address recipient)␊
272+
ERC20("MyToken", "MTK")␊
273+
ERC20Permit("MyToken")␊
274+
{␊
275+
_mint(recipient, 115792089237316195423570985008687907853269984665640564039457 * 10 ** decimals());␊
276+
}␊
277+
}␊
278+
`
279+
280+
## erc20 premint - e notation - valid
281+
282+
> Snapshot 1
283+
284+
`// SPDX-License-Identifier: MIT␊
285+
// Compatible with OpenZeppelin Contracts ^5.0.0␊
286+
pragma solidity ^0.8.22;␊
287+
288+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
289+
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
290+
291+
contract MyToken is ERC20, ERC20Permit {␊
292+
constructor(address recipient)␊
293+
ERC20("MyToken", "MTK")␊
294+
ERC20Permit("MyToken")␊
295+
{␊
296+
_mint(recipient, 100000000000000000000000000000000000000000000000000000000000 * 10 ** decimals());␊
297+
}␊
298+
}␊
299+
`
300+
238301
## erc20 mintable
239302

240303
> Snapshot 1
180 Bytes
Binary file not shown.

packages/core/solidity/src/erc20.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { ClockMode } from './set-clock-mode';
1313
import { clockModeDefault, setClockMode } from './set-clock-mode';
1414
import { supportsInterface } from './common-functions';
1515
import { OptionsError } from './error';
16+
import { toUint256, UINT256_MAX } from './utils/convert-strings';
1617

1718
export const crossChainBridgingOptions = [false, 'custom', 'superchain'] as const;
1819
export type CrossChainBridging = (typeof crossChainBridgingOptions)[number];
@@ -163,6 +164,14 @@ export function isValidChainId(str: string): boolean {
163164
return chainIdPattern.test(str);
164165
}
165166

167+
function scaleByPowerOfTen(base: bigint, exponent: number): bigint {
168+
if (exponent < 0) {
169+
return base / BigInt(10) ** BigInt(-exponent);
170+
} else {
171+
return base * BigInt(10) ** BigInt(exponent);
172+
}
173+
}
174+
166175
function addPremint(
167176
c: ContractBuilder,
168177
amount: string,
@@ -181,6 +190,9 @@ function addPremint(
181190
const units = integer + decimals + zeroes;
182191
const exp = decimalPlace <= 0 ? 'decimals()' : `(decimals() - ${decimalPlace})`;
183192

193+
const validatedBaseUnits = toUint256(units, 'premint');
194+
checkPotentialPremintOverflow(validatedBaseUnits, decimalPlace);
195+
184196
c.addConstructorArgument({ type: 'address', name: 'recipient' });
185197

186198
const mintLine = `_mint(recipient, ${units} * 10 ** ${exp});`;
@@ -212,6 +224,24 @@ function addPremint(
212224
}
213225
}
214226

227+
/**
228+
* Check for potential premint overflow assuming the user's contract has decimals() = 18
229+
*
230+
* @param baseUnits The base units of the token, before applying power of 10
231+
* @param decimalPlace If positive, the number of assumed decimal places in the least significant digits of `validatedBaseUnits`. Ignored if <= 0.
232+
* @throws OptionsError if the calculated value would overflow uint256
233+
*/
234+
function checkPotentialPremintOverflow(baseUnits: bigint, decimalPlace: number) {
235+
const assumedExp = decimalPlace <= 0 ? 18 : 18 - decimalPlace;
236+
const calculatedValue = scaleByPowerOfTen(baseUnits, assumedExp);
237+
238+
if (calculatedValue > UINT256_MAX) {
239+
throw new OptionsError({
240+
premint: 'Amount would overflow uint256 after applying decimals',
241+
});
242+
}
243+
}
244+
215245
function addMintable(c: ContractBuilder, access: Access) {
216246
requireAccessControl(c, functions.mint, access, 'MINTER', 'minter');
217247
c.addFunctionCode('_mint(to, amount);', functions.mint);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import test from 'ava';
2+
3+
import { toUint256, UINT256_MAX } from './convert-strings';
4+
import { OptionsError } from '../error';
5+
6+
test('toUint256', t => {
7+
t.is(toUint256('123', 'foo'), BigInt(123));
8+
});
9+
10+
test('toUint256 - not number', t => {
11+
const error = t.throws(() => toUint256('abc', 'foo'), { instanceOf: OptionsError });
12+
t.is(error.messages.foo, 'Not a valid number');
13+
});
14+
15+
test('toUint256 - negative', t => {
16+
const error = t.throws(() => toUint256('-1', 'foo'), { instanceOf: OptionsError });
17+
t.is(error.messages.foo, 'Not a valid number');
18+
});
19+
20+
test('toUint256 - too large', t => {
21+
const error = t.throws(() => toUint256(String(UINT256_MAX + BigInt(1)), 'foo'), { instanceOf: OptionsError });
22+
t.is(error.messages.foo, 'Value is greater than uint256 max value');
23+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { OptionsError } from '../error';
2+
3+
export const UINT256_MAX = BigInt(2) ** BigInt(256) - BigInt(1);
4+
5+
/**
6+
* Checks that a string is a valid `uint256` value and converts it to bigint.
7+
*
8+
* @param value The value to check.
9+
* @param field The field name to use in the error if the value is invalid.
10+
* @throws OptionsError if the value is not a valid number or is greater than the maximum value for `uint256`.
11+
* @returns The value as a bigint.
12+
*/
13+
export function toUint256(value: string, field: string): bigint {
14+
const isValidNumber = /^\d+$/.test(value);
15+
if (!isValidNumber) {
16+
throw new OptionsError({
17+
[field]: 'Not a valid number',
18+
});
19+
}
20+
const numValue = BigInt(value);
21+
if (numValue > UINT256_MAX) {
22+
throw new OptionsError({
23+
[field]: 'Value is greater than uint256 max value',
24+
});
25+
}
26+
return numValue;
27+
}

0 commit comments

Comments
 (0)