Skip to content

Commit

Permalink
Add copyMemory function (#99)
Browse files Browse the repository at this point in the history
* Added copyMemory function

* Added TODOs

* Completed MemoryUtils library.

* Changed from copyTo to copy interface

* Updated NatSpec

* Removed getSize() for `string memory`
  • Loading branch information
KyrylR authored May 11, 2024
1 parent ef1cd92 commit db62705
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 2 deletions.
80 changes: 80 additions & 0 deletions contracts/libs/utils/MemoryUtils.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/**
* @title MemoryUtils
* @notice A library that provides utility functions for memory manipulation in Solidity.
*/
library MemoryUtils {
/**
* @notice Copies the contents of the source string to the destination string.
*
* @param source_ The source string to copy from.
* @return destination_ The newly allocated string.
*/
function copy(string memory source_) internal view returns (string memory destination_) {
destination_ = new string(bytes(source_).length);

unsafeMemoryCopy(getPointer(source_), getPointer(destination_), bytes(source_).length);
}

/**
* @notice Copies the contents of the source bytes to the destination bytes.
*
* @param source_ The source bytes to copy from.
* @return destination_ The newly allocated bytes.
*/
function copy(bytes memory source_) internal view returns (bytes memory destination_) {
destination_ = new bytes(source_.length);

unsafeMemoryCopy(getPointer(source_), getPointer(destination_), source_.length);
}

/**
* @notice Copies memory from one location to another efficiently via identity precompile.
* @param sourcePointer_ The offset in the memory from which to copy.
* @param destinationPointer_ The offset in the memory where the result will be copied.
* @param size_ The size of the memory to copy.
*
* @dev This function does not account for free memory pointer and should be used with caution.
*
* This signature of calling identity precompile is:
* staticcall(gas(), address(0x04), argsOffset, argsSize, retOffset, retSize)
*/
function unsafeMemoryCopy(
uint256 sourcePointer_,
uint256 destinationPointer_,
uint256 size_
) internal view {
assembly {
pop(
staticcall(
gas(),
4,
add(sourcePointer_, 32),
size_,
add(destinationPointer_, 32),
size_
)
)
}
}

/**
* @notice Returns the memory pointer of the given bytes data.
*/
function getPointer(bytes memory data) internal pure returns (uint256 pointer) {
assembly {
pointer := data
}
}

/**
* @notice Returns the memory pointer of the given string data.
*/
function getPointer(string memory data) internal pure returns (uint256 pointer) {
assembly {
pointer := data
}
}
}
82 changes: 82 additions & 0 deletions contracts/mock/libs/utils/MemoryUtilsMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import {MemoryUtils} from "../../../libs/utils/MemoryUtils.sol";

contract MemoryUtilsMock {
using MemoryUtils for *;

function testStringMemoryCopy(string memory data_) external view {
string memory someString = new string(bytes(data_).length);

require(
keccak256(bytes(data_)) != keccak256(bytes(someString)),
"MemoryUtilsMock: testStringMemoryCopy failed. Initial data and someString are equal"
);

someString = data_.copy();

require(
keccak256(bytes(data_)) == keccak256(bytes(someString)),
"MemoryUtilsMock: testStringMemoryCopy failed. Initial data and someString are not equal"
);
}

function testBytesMemoryCopy(bytes memory data_) external view {
bytes memory someBytes = new bytes(data_.length);

require(
keccak256(data_) != keccak256(someBytes),
"MemoryUtilsMock: testBytesMemoryCopy failed. Initial data and someBytes are equal"
);

someBytes = data_.copy();

require(
keccak256(data_) == keccak256(someBytes),
"MemoryUtilsMock: testBytesMemoryCopy failed. Initial data and someBytes are not equal"
);
}

function testUnsafeMemoryCopy(bytes memory data_) external view {
bytes memory someBytes = new bytes(data_.length);

require(
keccak256(data_) != keccak256(someBytes),
"MemoryUtilsMock: testBigMemory failed. Initial data and someBytes are equal"
);

MemoryUtils.unsafeMemoryCopy(
MemoryUtils.getPointer(someBytes),
MemoryUtils.getPointer(data_),
someBytes.length
);

require(
keccak256(data_) == keccak256(someBytes),
"MemoryUtilsMock: testBigMemory failed. Initial data and someBytes are not equal"
);
}

function testPartialCopy(bytes memory data_) external view {
bytes memory someBytes = new bytes(data_.length / 2);

require(
keccak256(data_) != keccak256(someBytes),
"MemoryUtilsMock: testPartialCopy failed. Initial data and someBytes are equal"
);

MemoryUtils.unsafeMemoryCopy(
MemoryUtils.getPointer(someBytes),
MemoryUtils.getPointer(data_),
someBytes.length
);

for (uint256 i = 0; i < someBytes.length; i++) {
require(
someBytes[i] == data_[i],
"MemoryUtilsMock: testPartialCopy failed. Initial data and someBytes are not equal"
);
}
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions test/libs/utils/MemoryUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect } from "chai";
import { ethers } from "hardhat";

import { Reverter } from "@/test/helpers/reverter";

import { MemoryUtilsMock } from "@ethers-v6";

describe("MemoryUtils", () => {
const reverter = new Reverter();

let mock: MemoryUtilsMock;

before("setup", async () => {
const MemoryUtilsMock = await ethers.getContractFactory("MemoryUtilsMock");
mock = await MemoryUtilsMock.deploy();

await reverter.snapshot();
});

afterEach(reverter.revert);

describe("copyMemory", () => {
it("should copy arbitrary chunks of memory (bytes)", async () => {
await expect(mock.testBytesMemoryCopy(ethers.randomBytes(20))).to.be.eventually.fulfilled;
});

it("should copy arbitrary chunks of memory (string)", async () => {
await expect(mock.testStringMemoryCopy("Hello, world!")).to.be.eventually.fulfilled;
});

it("should copy 20 bytes of memory", async () => {
await expect(mock.testUnsafeMemoryCopy(ethers.randomBytes(20))).to.be.eventually.fulfilled;
});

it("should copy 32 bytes of memory", async () => {
await expect(mock.testUnsafeMemoryCopy(ethers.randomBytes(32))).to.be.eventually.fulfilled;
});

it("should copy 1000 bytes of memory", async () => {
await expect(mock.testUnsafeMemoryCopy(ethers.randomBytes(1000))).to.be.eventually.fulfilled;
});

it("should copy 4096 bytes of memory", async () => {
await expect(mock.testUnsafeMemoryCopy(ethers.randomBytes(4096))).to.be.eventually.fulfilled;
});

it("should copy partial chunks of memory", async () => {
await expect(mock.testPartialCopy(ethers.randomBytes(15))).to.be.eventually.fulfilled;
});
});
});

0 comments on commit db62705

Please sign in to comment.