Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
707da40
feat: integrate CreateX for proxy deployment in RLCAdapter and RLCOFT…
Le-Caignec May 22, 2025
3a4cc1e
feat: update deployment scripts to return proxy addresses and add min…
Le-Caignec May 22, 2025
05e40b5
feat: integrate CreateX for deploying RLCOFT implementation and proxy…
Le-Caignec May 23, 2025
9664fea
feat: add fuzz testing for RLCOFT deployment and address uniqueness
Le-Caignec May 23, 2025
8d2ba71
Merge remote-tracking branch 'origin/main' into feature/use-createX-f…
Le-Caignec May 26, 2025
5aed93c
chore: update openzeppelin-contracts subproject commit reference
Le-Caignec May 26, 2025
cfa4745
feat: remove unnecessary whitespace in RLCOFT.t.sol
Le-Caignec May 26, 2025
58950d5
feat: refactor and consolidate RLCOFT and RLCAdapter test scripts; re…
Le-Caignec May 26, 2025
cd26bae
feat: rename RLCOFT test contracts to RLCAdapter and update deploymen…
Le-Caignec May 26, 2025
9c2fc99
Merge remote-tracking branch 'origin/main' into feature/use-createX-f…
Le-Caignec Jun 3, 2025
54c902d
Merge branch 'main' into feature/use-createX-for-deployment
Le-Caignec Jun 3, 2025
6649103
feat: add CREATE2 factory configuration and new deployment tests
Le-Caignec Jun 3, 2025
7f54cbc
refactor: remove CREATE2 factory configuration and update deployment …
Le-Caignec Jun 4, 2025
a9d5123
feat: integrate CreateX factory for RLCAdapter and RLCOFT deployments
Le-Caignec Jun 4, 2025
f760673
refactor: move token name and symbol to state variables in RLCOFTTest
Le-Caignec Jun 4, 2025
fb01895
feat: update RLCAdapterScriptTest and RLCOFTMock to integrate CreateX…
Le-Caignec Jun 4, 2025
99e7b4a
feat: refactor RLCOFT deployment logic to use RLCOFTDeployer for Crea…
Le-Caignec Jun 4, 2025
315cc38
feat: update RLCOFTScriptTest to use constants for LayerZero endpoint…
Le-Caignec Jun 4, 2025
2b9e957
refactor: update RLCAdapterScriptTest to use constants for token and …
Le-Caignec Jun 4, 2025
783873e
feat: update RLCAdapter deployment logic to utilize CreateX for contr…
Le-Caignec Jun 4, 2025
16301c9
feat: update GitHub Actions workflow and Makefile for improved test c…
Le-Caignec Jun 4, 2025
1465285
clean import
Le-Caignec Jun 4, 2025
bf7703a
fix: stack too deep issue
Le-Caignec Jun 4, 2025
0e36162
chore: add TODO for via_ir implementation in foundry.toml
Le-Caignec Jun 5, 2025
ec098fb
fix: update comments to English for consistency in RLCOFTDeployer lib…
Le-Caignec Jun 5, 2025
624d837
fix: update comments to English for consistency in RLCAdapterScript a…
Le-Caignec Jun 5, 2025
5316cde
fix: improve console logs for proxy deployment in RLCAdapter and RLCO…
Le-Caignec Jun 5, 2025
041ad65
refactor: replace RLCOFTDeployer with UUPSProxyDeployer for contract …
Le-Caignec Jun 5, 2025
debca96
fix: remove createXFactory parameter from deploy functions in RLCAdap…
Le-Caignec Jun 6, 2025
0819cff
fix: remove unused imports from RLCAdapter and RLCOFT scripts
Le-Caignec Jun 6, 2025
0d09cbd
fix: update CREATE_X_FACTORY environment variable setup in RLCOFTTest
Le-Caignec Jun 6, 2025
5b86ee7
fix: update environment variable name for CreateX factory in RLCOFTTest
Le-Caignec Jun 6, 2025
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
6 changes: 5 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# CreateX Factory Address
CREATE_X_FACTORY_ADDRESS=0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed
SALT=0x<salt-value>

# Account to be used for script execution.
# Account name in Foundry keystore
# ACCOUNT=
ACCOUNT=<your-account-name>

# Ethereum Sepolia Configuration
# SEPOLIA_RPC_URL is added to Github env "ci".
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ concurrency:
env:
FOUNDRY_PROFILE: ci
SEPOLIA_RPC_URL: secrets.SEPOLIA_RPC_URL
ARBITRUM_SEPOLIA_RPC_URL: secrets.ARBITRUM_SEPOLIA_RPC_URL

jobs:
build-and-test:
Expand All @@ -32,5 +33,8 @@ jobs:
- name: Run Forge build
run: forge build --sizes && cp .env.template .env

- name: Run Forge tests
run: make utest
- name: Run Forge unit tests
run: make unit-test

- name: Run Forge e2e tests
run: make e2e-test
Comment on lines +36 to +40
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Split tests for better issues isolation

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ docs/
.last_deploy.json

.idea
.DS_Store
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@
[submodule "lib/layerzero-v1"]
path = lib/layerzero-v1
url = https://github.com/layerzero-labs/layerzero-v1
[submodule "lib/createx"]
path = lib/createx
url = https://github.com/pcaversaccio/createx
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ fork-sepolia:
fork-arbitrum-sepolia:
anvil --fork-url $(ARBITRUM_SEPOLIA_RPC_URL) --port 8546

utest:
FOUNDRY_PROFILE=test forge test -vvvv
unit-test:
FOUNDRY_PROFILE=test forge test -vvvv --match-path "./test/units/**" --fail-fast

e2e-test:
FOUNDRY_PROFILE=test forge test -vvvv --match-path "./test/e2e/**" --fail-fast

clean:
forge clean
Expand Down
4 changes: 2 additions & 2 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fs_permissions = [{ access = "read-write", path = "./" }]
optimizer = true
optimizer_runs = 200
optimizer_details = { yul = true }
via_ir = true
# TODO: Add via_ir = true and solve issues when building with it

## Needed by openzepplin upgrade plugin
ffi = true
Expand All @@ -23,6 +23,7 @@ remappings = [
'@layerzerolabs/test-devtools-evm-foundry/=lib/devtools/packages/test-devtools-evm-foundry/',
'@openzeppelin/foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/',
'@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/',
'@createx/contracts/=lib/createx/src/',

# Used internally by contracts within library dependencies
'@layerzerolabs/lz-evm-protocol-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/protocol/',
Expand All @@ -36,4 +37,3 @@ remappings = [

[profile.test]
optimizer = false
via_ir = false
1 change: 1 addition & 0 deletions lib/createx
Submodule createx added at cbac80
32 changes: 17 additions & 15 deletions script/RLCAdapter.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Script} from "forge-std/Script.sol";
import {ICreateX} from "@createx/contracts/ICreateX.sol";
import {RLCAdapter} from "../src/RLCAdapter.sol";
import {EnvUtils} from "./UpdateEnvUtils.sol";
import {UUPSProxyDeployer} from "./lib/UUPSProxyDeployer.sol";

contract Deploy is Script {
function run() external returns (address) {
Expand All @@ -14,23 +15,24 @@ contract Deploy is Script {
address rlcToken = vm.envAddress("RLC_SEPOLIA_ADDRESS"); // RLC token address on sepolia testnet
address lzEndpoint = vm.envAddress("LAYER_ZERO_SEPOLIA_ENDPOINT_ADDRESS"); // LayerZero sepolia endpoint
address owner = vm.envAddress("OWNER_ADDRESS"); // Your actual wallet address

// Deploy the RLCAdapter contract
RLCAdapter rlcAdapterImplementation = new RLCAdapter(rlcToken, lzEndpoint);
console.log("RLCAdapter implementation deployed at:", address(rlcAdapterImplementation));
bytes32 salt = vm.envBytes32("SALT");

// Deploy the proxy contract
address rlcAdapterProxyAddress = address(
new ERC1967Proxy(
address(rlcAdapterImplementation),
abi.encodeWithSelector(rlcAdapterImplementation.initialize.selector, owner)
)
);
console.log("RLCAdapter proxy deployed at:", rlcAdapterProxyAddress);
address rlcAdapterProxy = deploy(lzEndpoint, owner, salt, rlcToken);

vm.stopBroadcast();

EnvUtils.updateEnvVariable("RLC_SEPOLIA_ADAPTER_ADDRESS", rlcAdapterProxyAddress);
return rlcAdapterProxyAddress;
EnvUtils.updateEnvVariable("RLC_SEPOLIA_ADAPTER_ADDRESS", rlcAdapterProxy);
return rlcAdapterProxy;
}

function deploy(address lzEndpoint, address owner, bytes32 salt, address rlcToken) public returns (address) {
address createXFactory = vm.envAddress("CREATE_X_FACTORY_ADDRESS");
bytes memory constructorData = abi.encode(rlcToken, lzEndpoint);
bytes memory initializeData = abi.encodeWithSelector(RLCAdapter.initialize.selector, owner);
return UUPSProxyDeployer.deployUUPSProxyWithCreateX(
"RLCAdapter", constructorData, initializeData, createXFactory, salt
);
}
}

Expand Down
39 changes: 24 additions & 15 deletions script/RLCOFT.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;

import {Script, console} from "forge-std/Script.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {Script} from "forge-std/Script.sol";
import {ICreateX} from "@createx/contracts/ICreateX.sol";
import {RLCOFT} from "../src/RLCOFT.sol";
import {UUPSProxyDeployer} from "./lib/UUPSProxyDeployer.sol";
import {EnvUtils} from "./UpdateEnvUtils.sol";

contract Deploy is Script {
Expand All @@ -16,23 +17,31 @@ contract Deploy is Script {
address lzEndpoint = vm.envAddress("LAYER_ZERO_ARBITRUM_SEPOLIA_ENDPOINT_ADDRESS");
address owner = vm.envAddress("OWNER_ADDRESS");
address pauser = vm.envAddress("PAUSER_ADDRESS");
bytes32 salt = vm.envBytes32("SALT");

RLCOFT rlcOFTImplementation = new RLCOFT(lzEndpoint);
console.log("RLCOFT implementation deployed at:", address(rlcOFTImplementation));

// Deploy the proxy contract
address rlcOFTProxyAddress = address(
new ERC1967Proxy(
address(rlcOFTImplementation),
abi.encodeWithSelector(rlcOFTImplementation.initialize.selector, name, symbol, owner, pauser)
)
);
console.log("RLCOFT proxy deployed at:", rlcOFTProxyAddress);
address rlcOFTProxy = deploy(lzEndpoint, name, symbol, owner, pauser, salt);

vm.stopBroadcast();

EnvUtils.updateEnvVariable("RLC_ARBITRUM_SEPOLIA_OFT_ADDRESS", rlcOFTProxyAddress);
return rlcOFTProxyAddress;
EnvUtils.updateEnvVariable("RLC_ARBITRUM_SEPOLIA_OFT_ADDRESS", rlcOFTProxy);
return rlcOFTProxy;
}

function deploy(
address lzEndpoint,
string memory name,
string memory symbol,
address owner,
address pauser,
bytes32 salt
) public returns (address) {
address createXFactory = vm.envAddress("CREATE_X_FACTORY_ADDRESS");

bytes memory constructorData = abi.encode(lzEndpoint);
bytes memory initializeData = abi.encodeWithSelector(RLCOFT.initialize.selector, name, symbol, owner, pauser);
return UUPSProxyDeployer.deployUUPSProxyWithCreateX(
"RLCOFT", constructorData, initializeData, createXFactory, salt
);
}
}

Expand Down
65 changes: 65 additions & 0 deletions script/lib/UUPSProxyDeployer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <[email protected]>
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;

import {console} from "forge-std/console.sol";
import {Vm} from "forge-std/Vm.sol";
import {StdConstants} from "forge-std/StdConstants.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ICreateX} from "@createx/contracts/ICreateX.sol";

/// @notice Utility library for deploying UUPS proxy contracts and their implementations using the CreateX Factory
library UUPSProxyDeployer {
/// @dev Reference to the VM cheat codes from forge-std
Vm private constant vm = StdConstants.VM;

/// @notice Deploys a UUPS proxy contract and its implementation using the CreateX Factory
/// @param contractName The name of the contract to deploy (used to fetch creation code)
/// @param constructorData The constructor arguments for the implementation contract
/// @param initializeData The initialization data for the proxy contract
/// @param createXFactory The address of the CreateX factory
/// @param salt The salt for deterministic deployment
/// @return The address of the deployed proxy
function deployUUPSProxyWithCreateX(
string memory contractName,
bytes memory constructorData,
bytes memory initializeData,
address createXFactory,
bytes32 salt
) internal returns (address) {
// CreateX Factory instance
ICreateX createX = ICreateX(createXFactory);
address implementation = deployImplementationWithCreateX(contractName, constructorData, createX, salt);

// Deploy the proxy contract using CreateX Factory
address proxy = createX.deployCreate2AndInit(
salt, // salt
abi.encodePacked(type(ERC1967Proxy).creationCode, abi.encode(implementation, "")), // initCode
initializeData, // data for initialize
ICreateX.Values({constructorAmount: 0, initCallAmount: 0}) // values for CreateX
);
console.log("UUPS Proxy deployed at:", proxy);

return proxy;
}

/// @notice Deploys the implementation contract using the CreateX Factory
/// @param contractName The name of the contract to deploy (used to fetch creation code)
/// @param constructorData The constructor arguments for the implementation contract
/// @param createXFactory The address of the CreateX factory
/// @param salt The salt for deterministic deployment
/// @return The address of the deployed implementation contract
function deployImplementationWithCreateX(
string memory contractName,
bytes memory constructorData,
ICreateX createXFactory,
bytes32 salt
) internal returns (address) {
// Deploy the implementation contract using CreateX Factory
bytes memory creationCode = StdConstants.VM.getCode(contractName);
address implementation = createXFactory.deployCreate2(salt, abi.encodePacked(creationCode, constructorData));
console.log("Implementation deployed at:", implementation);

return implementation;
}
}
55 changes: 37 additions & 18 deletions test/e2e/RLCAdapterScript.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,52 @@
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Deploy as RLCAdapterDeploy, Configure as RLCAdapterConfigure} from "../../script/RLCAdapter.s.sol";
import {Deploy as RLCAdapterDeploy} from "../../script/RLCAdapter.s.sol";
import {RLCAdapter} from "../../src/RLCAdapter.sol";

contract RLCAdapterScriptTest is Test {
RLCAdapter public rlcAdapter;

// Unique instance of the deployment script
address lzEndpoint = 0x6EDCE65403992e310A62460808c4b910D972f10f; // LayerZero Arbitrum Sepolia endpoint
address owner = makeAddr("OWNER_ADDRESS");
address pauser = makeAddr("PAUSER_ADDRESS");
address RLC_TOKEN = 0x26A738b6D33EF4D94FF084D3552961b8f00639Cd;

RLCAdapterDeploy public deployer;

function setUp() public {
vm.createSelectFork("https://ethereum-sepolia-rpc.publicnode.com"); // use public node
vm.createSelectFork(vm.envString("SEPOLIA_RPC_URL"));
deployer = new RLCAdapterDeploy();
vm.setEnv("CREATE_X_FACTORY", "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed");
}

// ============ Deployment Tests ============
function testFork_CheckDeployment() public {
bytes32 salt = keccak256("RLCOFT_SALT");
RLCAdapter rlcAdapter = RLCAdapter(deployer.deploy(lzEndpoint, owner, salt, RLC_TOKEN));

vm.setEnv("RLC_OFT_TOKEN_NAME", "RLC OFT Token");
vm.setEnv("RLC_TOKEN_SYMBOL", "RLC");
vm.setEnv("LAYER_ZERO_ARBITRUM_SEPOLIA_ENDPOINT_ADDRESS", "0x6EDCE65403992e310A62460808c4b910D972f10f");
vm.setEnv("OWNER_ADDRESS", vm.toString(owner));
vm.setEnv("PAUSER_ADDRESS", vm.toString(pauser));
assertEq(rlcAdapter.owner(), owner);
assertEq(rlcAdapter.token(), RLC_TOKEN);
//TODO: check roles
}

function testForkFuzz_DifferentSaltsProduceDifferentAddresses(bytes32 salt1, bytes32 salt2) public {
vm.assume(salt1 != salt2); // ensure they are different

rlcAdapter = RLCAdapter(new RLCAdapterDeploy().run());
address addr1 = deployer.deploy(lzEndpoint, owner, salt1, RLC_TOKEN);
address addr2 = deployer.deploy(lzEndpoint, owner, salt2, RLC_TOKEN);

assertTrue(addr1 != addr2, "Fuzz test failed: different salts produced same address");
}

/**
* Deployment
*/
function test_CheckDeployment() public view {
assertEq(rlcAdapter.owner(), vm.envAddress("OWNER_ADDRESS"));
assertEq(rlcAdapter.token(), vm.envAddress("RLC_SEPOLIA_ADDRESS"));
// TODO check roles
function testForkFuzz_RevertIfSecondDeploymentWithSameSalt(bytes32 salt) public {
// First deployment
address addr = deployer.deploy(lzEndpoint, owner, salt, RLC_TOKEN);
assertTrue(addr != address(0), "First deployment should succeed");

// Attempt redeployment with the same salt
try deployer.deploy(lzEndpoint, owner, salt, RLC_TOKEN) returns (address) {
revert("Expected revert on redeployment with same salt but no revert occurred");
} catch {
// Expected: revert due to CREATE2 address collision
}
}
}
53 changes: 37 additions & 16 deletions test/e2e/RLCOFTScript.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,53 @@
pragma solidity ^0.8.13;

import {Test} from "forge-std/Test.sol";
import {Deploy as RLCOFTDeploy, Configure as RLCOFTConfigure} from "../../script/RLCOFT.s.sol";
import {Deploy as RLCOFTDeploy} from "../../script/RLCOFT.s.sol";
import {RLCOFT} from "../../src/RLCOFT.sol";

contract RLCOFTScriptTest is Test {
RLCOFT public rlcOft;

// Unique instance of the deployment script
string name = "RLC OFT Token";
string symbol = "RLC";
address owner = makeAddr("OWNER_ADDRESS");
address pauser = makeAddr("PAUSER_ADDRESS");
address LAYERZERO_ENDPOINT = 0x6EDCE65403992e310A62460808c4b910D972f10f; // LayerZero Arbitrum Sepolia endpoint

RLCOFTDeploy public deployer;

function setUp() public {
vm.createSelectFork("https://arbitrum-sepolia-rpc.publicnode.com"); // use public node
vm.createSelectFork(vm.envString("ARBITRUM_SEPOLIA_RPC_URL")); // use public node
deployer = new RLCOFTDeploy();
vm.setEnv("CREATE_X_FACTORY", "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed");
}

// ============ Deployment Tests ============
function testFork_CheckDeployment() public {
bytes32 salt = keccak256("RLCOFT_SALT");
RLCOFT rlcoft = RLCOFT(deployer.deploy(LAYERZERO_ENDPOINT, name, symbol, owner, pauser, salt));

vm.setEnv("RLC_OFT_TOKEN_NAME", "RLC OFT Token");
vm.setEnv("RLC_TOKEN_SYMBOL", "RLC");
vm.setEnv("LAYER_ZERO_ARBITRUM_SEPOLIA_ENDPOINT_ADDRESS", "0x6EDCE65403992e310A62460808c4b910D972f10f");
vm.setEnv("OWNER_ADDRESS", vm.toString(owner));
vm.setEnv("PAUSER_ADDRESS", vm.toString(pauser));
assertEq(rlcoft.owner(), owner);
assertEq(rlcoft.token(), address(rlcoft));
}

function testForkFuzz_DifferentSaltsProduceDifferentAddresses(bytes32 salt1, bytes32 salt2) public {
vm.assume(salt1 != salt2); // ensure they are different

rlcOft = RLCOFT(new RLCOFTDeploy().run());
address addr1 = deployer.deploy(LAYERZERO_ENDPOINT, name, symbol, owner, pauser, salt1);
address addr2 = deployer.deploy(LAYERZERO_ENDPOINT, name, symbol, owner, pauser, salt2);

assertTrue(addr1 != addr2, "Fuzz test failed: different salts produced same address");
}

/**
* Deployment
*/
function test_CheckDeployment() public view {
assertEq(rlcOft.owner(), vm.envAddress("OWNER_ADDRESS"));
assertEq(rlcOft.token(), address(rlcOft));
function testForkFuzz_RevertIfSecondDeploymentWithSameSalt(bytes32 salt) public {
// First deployment
address addr = deployer.deploy(LAYERZERO_ENDPOINT, name, symbol, owner, pauser, salt);
assertTrue(addr != address(0), "First deployment should succeed");

// Attempt redeployment with the same salt
try deployer.deploy(LAYERZERO_ENDPOINT, name, symbol, owner, pauser, salt) returns (address) {
revert("Expected revert on redeployment with same salt but no revert occurred");
} catch {
// Expected: revert due to CREATE2 address collision
}
}
}
Loading