Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependency on solidity-stringutils #91

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "lib/solidity-stringutils"]
path = lib/solidity-stringutils
url = https://github.com/Arachnid/solidity-stringutils
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## Unreleased

- Remove dependency on `solidity-stringutils`.

## 0.3.7 (2025-01-13)

- Update documentation links. ([#88](https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades/pull/88))
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,18 @@ Follow the steps above, but instead of running `forge install OpenZeppelin/openz
npm install @openzeppelin/foundry-upgrades
```

Then add the following additional lines to `remappings.txt`, in addition to the ones described above:
Then add the following additional line to `remappings.txt`, in addition to the ones described above:
```
openzeppelin-foundry-upgrades/=node_modules/@openzeppelin/foundry-upgrades/src/
solidity-stringutils/=node_modules/@openzeppelin/foundry-upgrades/lib/solidity-stringutils/
```

#### Soldeer

Follow the steps above, but instead of running `forge install OpenZeppelin/openzeppelin-foundry-upgrades`, use one of the install commands described in https://soldeer.xyz/project/openzeppelin-foundry-upgrades

Then add the following additional lines to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
Then add the following additional line to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
```
openzeppelin-foundry-upgrades/=dependencies/openzeppelin-foundry-upgrades-0.3.6/src/
solidity-stringutils/=dependencies/openzeppelin-foundry-upgrades-0.3.6/lib/solidity-stringutils/
```

## OpenZeppelin Defender integration
Expand All @@ -76,7 +74,7 @@ See [DEFENDER.md](DEFENDER.md)

## Foundry Requirements

This library requires [forge-std](https://github.com/foundry-rs/forge-std) version 1.8.0 or higher.
This library requires [forge-std](https://github.com/foundry-rs/forge-std) version 1.9.5 or higher.

## Before Running

Expand Down
6 changes: 2 additions & 4 deletions docs/modules/pages/foundry-upgrades.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,20 @@ Follow the steps above, but instead of running `forge install OpenZeppelin/openz
npm install @openzeppelin/foundry-upgrades
----

Then add the following additional lines to `remappings.txt`, in addition to the ones described above:
Then add the following additional line to `remappings.txt`, in addition to the ones described above:
[source,console]
----
openzeppelin-foundry-upgrades/=node_modules/@openzeppelin/foundry-upgrades/src/
solidity-stringutils/=node_modules/@openzeppelin/foundry-upgrades/lib/solidity-stringutils/
----

==== Soldeer

Follow the steps above, but instead of running `forge install OpenZeppelin/openzeppelin-foundry-upgrades`, use one of the install commands described in https://soldeer.xyz/project/openzeppelin-foundry-upgrades

Then add the following additional lines to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
Then add the following additional line to `remappings.txt`, in addition to the ones described above (replace `0.3.6` with the version of the plugin that you installed):
[source,console]
----
openzeppelin-foundry-upgrades/=dependencies/openzeppelin-foundry-upgrades-0.3.6/src/
solidity-stringutils/=dependencies/openzeppelin-foundry-upgrades-0.3.6/lib/solidity-stringutils/
----

== Foundry Requirements
Expand Down
1 change: 0 additions & 1 deletion lib/solidity-stringutils
Submodule solidity-stringutils deleted from 4b2fcc
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
"description": "Foundry library for deploying and managing upgradeable contracts",
"license": "MIT",
"files": [
"src/**/*",
"lib/solidity-stringutils/**/*"
"src/**/*"
],
"repository": {
"type": "git",
Expand Down
17 changes: 10 additions & 7 deletions src/internal/Core.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";
import {strings} from "solidity-stringutils/src/strings.sol";

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

import {Options} from "../Options.sol";
import {Versions} from "./Versions.sol";
Expand Down Expand Up @@ -61,6 +62,8 @@ library Core {
upgradeProxy(proxy, contractName, data, opts);
}

using Strings for *;

/**
* @dev Upgrades a proxy to a new implementation contract. Only supported for UUPS or transparent proxies.
*
Expand All @@ -74,15 +77,15 @@ library Core {
bytes32 adminSlot = vm.load(proxy, ADMIN_SLOT);
if (adminSlot == bytes32(0)) {
string memory upgradeInterfaceVersion = getUpgradeInterfaceVersion(proxy);
if (upgradeInterfaceVersion.toSlice().equals("5.0.0".toSlice()) || data.length > 0) {
if (upgradeInterfaceVersion.equal("5.0.0") || data.length > 0) {
IUpgradeableProxy(proxy).upgradeToAndCall(newImpl, data);
} else {
IUpgradeableProxy(proxy).upgradeTo(newImpl);
}
} else {
address admin = address(uint160(uint256(adminSlot)));
string memory upgradeInterfaceVersion = getUpgradeInterfaceVersion(admin);
if (upgradeInterfaceVersion.toSlice().equals("5.0.0".toSlice()) || data.length > 0) {
if (upgradeInterfaceVersion.equal("5.0.0") || data.length > 0) {
IProxyAdmin(admin).upgradeAndCall(proxy, newImpl, data);
} else {
IProxyAdmin(admin).upgrade(proxy, newImpl);
Expand Down Expand Up @@ -300,8 +303,6 @@ library Core {
*/
bytes32 private constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

using strings for *;

/**
* @dev Gets the upgrade interface version string from a proxy or admin contract using the `UPGRADE_INTERFACE_VERSION()` getter.
* If the contract does not have the getter or the return data does not look like a string, this function returns an empty string.
Expand Down Expand Up @@ -346,7 +347,8 @@ library Core {

// CLI validate command uses exit code to indicate if the validation passed or failed.
// As an extra precaution, we also check stdout for "SUCCESS" to ensure it actually ran.
if (result.exitCode == 0 && stdout.toSlice().contains("SUCCESS".toSlice())) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (result.exitCode == 0 && vm.contains(stdout, "SUCCESS")) {
return;
} else if (result.stderr.length > 0) {
// Validations failed to run
Expand All @@ -361,7 +363,7 @@ library Core {
string memory contractName,
Options memory opts,
bool requireReference
) internal view returns (string[] memory) {
) internal returns (string[] memory) {
string memory outDir = Utils.getOutDir();

string[] memory inputBuilder = new string[](2 ** 16);
Expand Down Expand Up @@ -456,6 +458,7 @@ library Core {

function _deployFromBytecode(bytes memory bytecode) private returns (address) {
address addr;
/// @solidity memory-safe-assembly
assembly {
addr := create(0, add(bytecode, 32), mload(bytecode))
}
Expand Down
72 changes: 38 additions & 34 deletions src/internal/DefenderDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {console} from "forge-std/console.sol";
import {strings} from "solidity-stringutils/src/strings.sol";

import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";

Expand All @@ -18,8 +17,6 @@ import {ProposeUpgradeResponse, ApprovalProcessResponse} from "../Defender.sol";
* WARNING: DO NOT USE DIRECTLY. Use Defender.sol instead.
*/
library DefenderDeploy {
using strings for *;

function deploy(
string memory contractName,
bytes memory constructorData,
Expand Down Expand Up @@ -54,7 +51,7 @@ library DefenderDeploy {
) internal view returns (string[] memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);

if (!(defenderOpts.licenseType).toSlice().empty()) {
if (bytes(defenderOpts.licenseType).length != 0) {
if (defenderOpts.skipVerifySourceCode) {
revert("The `licenseType` option cannot be used when the `skipVerifySourceCode` option is `true`");
} else if (defenderOpts.skipLicenseType) {
Expand Down Expand Up @@ -86,14 +83,14 @@ library DefenderDeploy {
if (defenderOpts.skipVerifySourceCode) {
inputBuilder[i++] = "--verifySourceCode";
inputBuilder[i++] = "false";
} else if (!(defenderOpts.licenseType).toSlice().empty()) {
} else if (bytes(defenderOpts.licenseType).length != 0) {
inputBuilder[i++] = "--licenseType";
inputBuilder[i++] = string(abi.encodePacked('"', defenderOpts.licenseType, '"'));
} else if (!defenderOpts.skipLicenseType && !(contractInfo.license).toSlice().empty()) {
} else if (!defenderOpts.skipLicenseType && bytes(contractInfo.license).length != 0) {
inputBuilder[i++] = "--licenseType";
inputBuilder[i++] = string(abi.encodePacked('"', _toLicenseType(contractInfo), '"'));
}
if (!(defenderOpts.relayerId).toSlice().empty()) {
if (bytes(defenderOpts.relayerId).length != 0) {
inputBuilder[i++] = "--relayerId";
inputBuilder[i++] = defenderOpts.relayerId;
}
Expand All @@ -117,7 +114,7 @@ library DefenderDeploy {
inputBuilder[i++] = "--maxPriorityFeePerGas";
inputBuilder[i++] = Strings.toString(defenderOpts.txOverrides.maxPriorityFeePerGas);
}
if (!(defenderOpts.metadata).toSlice().empty()) {
if (bytes(defenderOpts.metadata).length != 0) {
inputBuilder[i++] = "--metadata";
inputBuilder[i++] = string(abi.encodePacked('"', vm.replace(defenderOpts.metadata, '"', '\\"'), '"'));
}
Expand All @@ -133,35 +130,37 @@ library DefenderDeploy {
return inputs;
}

using Strings for string;

function _toLicenseType(ContractInfo memory contractInfo) private pure returns (string memory) {
strings.slice memory id = contractInfo.license.toSlice();
if (id.equals("UNLICENSED".toSlice())) {
string memory id = contractInfo.license;
if (id.equal("UNLICENSED")) {
return "None";
} else if (id.equals("Unlicense".toSlice())) {
} else if (id.equal("Unlicense")) {
return "Unlicense";
} else if (id.equals("MIT".toSlice())) {
} else if (id.equal("MIT")) {
return "MIT";
} else if (id.equals("GPL-2.0-only".toSlice()) || id.equals("GPL-2.0-or-later".toSlice())) {
} else if (id.equal("GPL-2.0-only") || id.equal("GPL-2.0-or-later")) {
return "GNU GPLv2";
} else if (id.equals("GPL-3.0-only".toSlice()) || id.equals("GPL-3.0-or-later".toSlice())) {
} else if (id.equal("GPL-3.0-only") || id.equal("GPL-3.0-or-later")) {
return "GNU GPLv3";
} else if (id.equals("LGPL-2.1-only".toSlice()) || id.equals("LGPL-2.1-or-later".toSlice())) {
} else if (id.equal("LGPL-2.1-only") || id.equal("LGPL-2.1-or-later")) {
return "GNU LGPLv2.1";
} else if (id.equals("LGPL-3.0-only".toSlice()) || id.equals("LGPL-3.0-or-later".toSlice())) {
} else if (id.equal("LGPL-3.0-only") || id.equal("LGPL-3.0-or-later")) {
return "GNU LGPLv3";
} else if (id.equals("BSD-2-Clause".toSlice())) {
} else if (id.equal("BSD-2-Clause")) {
return "BSD-2-Clause";
} else if (id.equals("BSD-3-Clause".toSlice())) {
} else if (id.equal("BSD-3-Clause")) {
return "BSD-3-Clause";
} else if (id.equals("MPL-2.0".toSlice())) {
} else if (id.equal("MPL-2.0")) {
return "MPL-2.0";
} else if (id.equals("OSL-3.0".toSlice())) {
} else if (id.equal("OSL-3.0")) {
return "OSL-3.0";
} else if (id.equals("Apache-2.0".toSlice())) {
} else if (id.equal("Apache-2.0")) {
return "Apache-2.0";
} else if (id.equals("AGPL-3.0-only".toSlice()) || id.equals("AGPL-3.0-or-later".toSlice())) {
} else if (id.equal("AGPL-3.0-only") || id.equal("AGPL-3.0-or-later")) {
return "GNU AGPLv3";
} else if (id.equals("BUSL-1.1".toSlice())) {
} else if (id.equal("BUSL-1.1")) {
return "BSL 1.1";
} else {
revert(
Expand Down Expand Up @@ -217,7 +216,7 @@ library DefenderDeploy {
return parseProposeUpgradeResponse(stdout);
}

function parseProposeUpgradeResponse(string memory stdout) internal pure returns (ProposeUpgradeResponse memory) {
function parseProposeUpgradeResponse(string memory stdout) internal returns (ProposeUpgradeResponse memory) {
ProposeUpgradeResponse memory response;
response.proposalId = _parseLine("Proposal ID: ", stdout, true);
response.url = _parseLine("Proposal URL: ", stdout, false);
Expand All @@ -228,15 +227,20 @@ library DefenderDeploy {
string memory expectedPrefix,
string memory stdout,
bool required
) private pure returns (string memory) {
strings.slice memory delim = expectedPrefix.toSlice();
if (stdout.toSlice().contains(delim)) {
strings.slice memory slice = stdout.toSlice().copy().find(delim).beyond(delim);
) private returns (string memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (vm.contains(stdout, expectedPrefix)) {
// Get the substring after the prefix
string[] memory segments = vm.split(stdout, expectedPrefix);
string memory suffix = "";
for (uint256 i = 1; i < segments.length; i++) {
suffix = string(abi.encodePacked(suffix, segments[i]));
}
ericglau marked this conversation as resolved.
Show resolved Hide resolved
// Remove any following lines
if (slice.contains("\n".toSlice())) {
slice = slice.split("\n".toSlice());
if (vm.contains(suffix, "\n")) {
suffix = vm.split(suffix, "\n")[0];
}
return slice.toString();
return suffix;
} else if (required) {
revert(
string(abi.encodePacked("Failed to find line with prefix '", expectedPrefix, "' in output: ", stdout))
Expand Down Expand Up @@ -276,7 +280,7 @@ library DefenderDeploy {
inputBuilder[i++] = "--proxyAdminAddress";
inputBuilder[i++] = vm.toString(proxyAdminAddress);
}
if (!(opts.defender.upgradeApprovalProcessId).toSlice().empty()) {
if (bytes(opts.defender.upgradeApprovalProcessId).length != 0) {
inputBuilder[i++] = "--approvalProcessId";
inputBuilder[i++] = opts.defender.upgradeApprovalProcessId;
}
Expand All @@ -303,15 +307,15 @@ library DefenderDeploy {
return parseApprovalProcessResponse(stdout);
}

function parseApprovalProcessResponse(string memory stdout) internal pure returns (ApprovalProcessResponse memory) {
function parseApprovalProcessResponse(string memory stdout) internal returns (ApprovalProcessResponse memory) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);

ApprovalProcessResponse memory response;

response.approvalProcessId = _parseLine("Approval process ID: ", stdout, true);

string memory viaString = _parseLine("Via: ", stdout, false);
if (viaString.toSlice().len() != 0) {
if (bytes(viaString).length != 0) {
response.via = vm.parseAddress(viaString);
}

Expand Down
52 changes: 52 additions & 0 deletions src/internal/StringFinder.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {Vm} from "forge-std/Vm.sol";
import {Utils} from "./Utils.sol";

/**
* String finder functions using Forge's string cheatcodes.
* For internal use only.
*/
library StringFinder {
/**
* Returns whether the subject string contains the search string.
*/
function contains(string memory subject, string memory search) internal returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
Copy link

@Amxx Amxx Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thing that can be defined only once as a library constant:

library StringFinder {
    Vm private constant VM = Vm(Utils.CHEATCODE_ADDRESS);
    
    function contains(string memory subject, string memory search) internal returns (bool) {
        return VM.contains(subject, search);
    }
    
    function startsWith(string memory subject, string memory search) internal pure returns (bool) {
        return VM.indexOf(subject, search) == 0;
    }
    
    // ...
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work on some older versions of Solidity 0.8.x which should be supported for previous versions of OpenZeppelin Contracts.

return vm.contains(subject, search);
}

/**
* Returns whether the subject string starts with the search string.
*/
function startsWith(string memory subject, string memory search) internal pure returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
uint256 index = vm.indexOf(subject, search);
return index == 0;
Comment on lines +24 to +26
Copy link

@Amxx Amxx Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do without the Vm:

return return bytes(subject).length >= bytes(search).length
    && string(bytes(subject).slice(0, bytes(search).length)).equal(search);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that it would be pure, without would allow all the previously view function to remain view

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't use Bytes.slice here because we want to support OpenZeppelin Contracts 4.x and 5.x, and Bytes isn't available in some older versions. We also can't just copy in the Bytes.slice code because older versions of Solidity don't support mcopy.

}

/**
* Returns whether the subject string ends with the search string.
*/
function endsWith(string memory subject, string memory search) internal returns (bool) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (!vm.contains(subject, search)) {
return false;
}
string[] memory tokens = vm.split(subject, search);
return bytes(tokens[tokens.length - 1]).length == 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same:

return bytes(subject).length >= bytes(search).length
 && string(bytes(subject).slice(bytes(subject).length - bytes(search).length)).equal(search);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as my other comment

Copy link
Member Author

@ericglau ericglau Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the use of vm.contains, similar to the suggestion in count(). This allows this to be pure, and reverted some other functions' mutability to view.

}

/**
* Returns the number of occurrences of the search string in the subject string.
*/
function count(string memory subject, string memory search) internal returns (uint256) {
Vm vm = Vm(Utils.CHEATCODE_ADDRESS);
if (!vm.contains(subject, search)) {
return 0;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is the contains really needed ?

I'm expecting vm.split(subject, search) to return a string[1] that contains everything when search is not present in subject.

So tokens.length is 1, and tokens.length - 1 is 0

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that neither this is not a correct implementation of count. It will not return the correct value when the instances overlap. For example, count("aaa", "aa") should return 2

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed vm.split. Changed function documentation to say that this is for non-overlapping occurrences, as we currently don't have a use case for counting overlaps.

string[] memory tokens = vm.split(subject, search);
return tokens.length - 1;
}
}
Loading
Loading