Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/core/solidity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Add validation for ERC20 premint field. ([#488](https://github.com/OpenZeppelin/contracts-wizard/pull/488))

## 0.5.3 (2025-03-13)

- Add ERC20 Cross-Chain Bridging, SuperchainERC20. ([#436](https://github.com/OpenZeppelin/contracts-wizard/pull/436))
Expand Down
37 changes: 37 additions & 0 deletions packages/core/solidity/src/erc20.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,43 @@ testERC20('erc20 premint of 0', {
premint: '0',
});

function testPremint(scenario: string, premint: string, expectedError?: string) {
test(`erc20 premint - ${scenario} - ${expectedError ? 'invalid' : 'valid'}`, async t => {
if (expectedError) {
const error = t.throws(() =>
buildERC20({
name: 'MyToken',
symbol: 'MTK',
premint,
}),
);
t.is((error as OptionsError).messages.premint, expectedError);
} else {
const c = buildERC20({
name: 'MyToken',
symbol: 'MTK',
premint,
});
t.snapshot(printContract(c));
}
});
}

testPremint('max literal', '115792089237316195423570985008687907853269984665640564039457.584007913129639935'); // 2^256 - 1, shifted by 18 decimals
testPremint(
'max literal + 1',
'115792089237316195423570985008687907853269984665640564039457.584007913129639936',
'Value is greater than uint256 max value',
);
testPremint('no arithmetic overflow', '115792089237316195423570985008687907853269984665640564039457'); // 2^256 - 1, truncated by 18 decimals
testPremint(
'arithmetic overflow',
'115792089237316195423570985008687907853269984665640564039458',
'Amount would overflow uint256 after applying decimals',
);
testPremint('e notation', '1e59');
testPremint('e notation arithmetic overflow', '1e60', 'Amount would overflow uint256 after applying decimals');

testERC20('erc20 mintable', {
mintable: true,
access: 'ownable',
Expand Down
63 changes: 63 additions & 0 deletions packages/core/solidity/src/erc20.test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,69 @@ Generated by [AVA](https://avajs.dev).
}␊
`

## erc20 premint - max literal - valid

> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.0.0␊
pragma solidity ^0.8.22;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
contract MyToken is ERC20, ERC20Permit {␊
constructor(address recipient)␊
ERC20("MyToken", "MTK")␊
ERC20Permit("MyToken")␊
{␊
_mint(recipient, 115792089237316195423570985008687907853269984665640564039457584007913129639935 * 10 ** (decimals() - 18));␊
}␊
}␊
`

## erc20 premint - no arithmetic overflow - valid

> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.0.0␊
pragma solidity ^0.8.22;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
contract MyToken is ERC20, ERC20Permit {␊
constructor(address recipient)␊
ERC20("MyToken", "MTK")␊
ERC20Permit("MyToken")␊
{␊
_mint(recipient, 115792089237316195423570985008687907853269984665640564039457 * 10 ** decimals());␊
}␊
}␊
`

## erc20 premint - e notation - valid

> Snapshot 1

`// SPDX-License-Identifier: MIT␊
// Compatible with OpenZeppelin Contracts ^5.0.0␊
pragma solidity ^0.8.22;␊
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊
contract MyToken is ERC20, ERC20Permit {␊
constructor(address recipient)␊
ERC20("MyToken", "MTK")␊
ERC20Permit("MyToken")␊
{␊
_mint(recipient, 100000000000000000000000000000000000000000000000000000000000 * 10 ** decimals());␊
}␊
}␊
`

## erc20 mintable

> Snapshot 1
Expand Down
Binary file modified packages/core/solidity/src/erc20.test.ts.snap
Binary file not shown.
30 changes: 30 additions & 0 deletions packages/core/solidity/src/erc20.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ClockMode } from './set-clock-mode';
import { clockModeDefault, setClockMode } from './set-clock-mode';
import { supportsInterface } from './common-functions';
import { OptionsError } from './error';
import { toUint256, UINT256_MAX } from './utils/convert-strings';

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

function scaleByPowerOfTen(base: bigint, exponent: number): bigint {
if (exponent < 0) {
return base / BigInt(10) ** BigInt(-exponent);
} else {
return base * BigInt(10) ** BigInt(exponent);
}
}

function addPremint(
c: ContractBuilder,
amount: string,
Expand All @@ -181,6 +190,9 @@ function addPremint(
const units = integer + decimals + zeroes;
const exp = decimalPlace <= 0 ? 'decimals()' : `(decimals() - ${decimalPlace})`;

const validatedBaseUnits = toUint256(units, 'premint');
checkPotentialPremintOverflow(validatedBaseUnits, decimalPlace);

c.addConstructorArgument({ type: 'address', name: 'recipient' });

const mintLine = `_mint(recipient, ${units} * 10 ** ${exp});`;
Expand Down Expand Up @@ -212,6 +224,24 @@ function addPremint(
}
}

/**
* Check for potential premint overflow assuming the user's contract has decimals() = 18
*
* @param baseUnits The base units of the token, before applying power of 10
* @param decimalPlace If positive, the number of assumed decimal places in the least significant digits of `validatedBaseUnits`. Ignored if <= 0.
* @throws OptionsError if the calculated value would overflow uint256
*/
function checkPotentialPremintOverflow(baseUnits: bigint, decimalPlace: number) {
const assumedExp = decimalPlace <= 0 ? 18 : 18 - decimalPlace;
const calculatedValue = scaleByPowerOfTen(baseUnits, assumedExp);

if (calculatedValue > UINT256_MAX) {
throw new OptionsError({
premint: 'Amount would overflow uint256 after applying decimals',
});
}
}

function addMintable(c: ContractBuilder, access: Access) {
requireAccessControl(c, functions.mint, access, 'MINTER', 'minter');
c.addFunctionCode('_mint(to, amount);', functions.mint);
Expand Down
23 changes: 23 additions & 0 deletions packages/core/solidity/src/utils/convert-strings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import test from 'ava';

import { toUint256, UINT256_MAX } from './convert-strings';
import { OptionsError } from '../error';

test('toUint256', t => {
t.is(toUint256('123', 'foo'), BigInt(123));
});

test('toUint256 - not number', t => {
const error = t.throws(() => toUint256('abc', 'foo'), { instanceOf: OptionsError });
t.is(error.messages.foo, 'Not a valid number');
});

test('toUint256 - negative', t => {
const error = t.throws(() => toUint256('-1', 'foo'), { instanceOf: OptionsError });
t.is(error.messages.foo, 'Not a valid number');
});

test('toUint256 - too large', t => {
const error = t.throws(() => toUint256(String(UINT256_MAX + BigInt(1)), 'foo'), { instanceOf: OptionsError });
t.is(error.messages.foo, 'Value is greater than uint256 max value');
});
27 changes: 27 additions & 0 deletions packages/core/solidity/src/utils/convert-strings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { OptionsError } from '../error';

export const UINT256_MAX = BigInt(2) ** BigInt(256) - BigInt(1);

/**
* Checks that a string is a valid `uint256` value and converts it to bigint.
*
* @param value The value to check.
* @param field The field name to use in the error if the value is invalid.
* @throws OptionsError if the value is not a valid number or is greater than the maximum value for `uint256`.
* @returns The value as a bigint.
*/
export function toUint256(value: string, field: string): bigint {
const isValidNumber = /^\d+$/.test(value);
if (!isValidNumber) {
throw new OptionsError({
[field]: 'Not a valid number',
});
}
const numValue = BigInt(value);
if (numValue > UINT256_MAX) {
throw new OptionsError({
[field]: 'Value is greater than uint256 max value',
});
}
return numValue;
}
Loading