-
Notifications
You must be signed in to change notification settings - Fork 39
an adapter contract to support withdrawal from weETH #287
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
Open
seongyun-ko
wants to merge
5
commits into
master
Choose a base branch
from
syko/feature/weETH-withdraw-adapter
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+627
−0
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
c30dbce
add a simple adapter contract to support withdrawal from weETH
seongyun-ko 27bb6aa
add a deployment script, move file to a new dir
seongyun-ko 5bb5607
use explicit imports, fix deployment script
seongyun-ko 97cd063
fix initialize to accept the owner, fix deploy script
seongyun-ko f9f7b15
deployed
seongyun-ko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"contractName": "WeETHWithdrawAdapter", | ||
"deploymentParameters": { | ||
"factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", | ||
"salt": "0x97cd063025712908c02476f03a21565e2fd4f801000000000000000000000000", | ||
"constructorArgsEncoded": "0x000000000000000000000000cd5fe23c85820f7b72d0926fc9b05b43e359b7ee00000000000000000000000035fa164735182de50811e8e2e824cfb9b6118ac2000000000000000000000000308861a430be4cce5502d0a12724771fc6daf2160000000000000000000000007d5706f6ef3f89b3951e23e557cdfbc3239d4e2c00000000000000000000000062247d29b4b9becf4bb73e0c722cf6445cfc7ce9" | ||
}, | ||
"deployedAddress": "0x3f313F0A856aE12b0A16178e29B6Ada84c256952" | ||
} |
9 changes: 9 additions & 0 deletions
9
deployment/WeETHWithdrawAdapter_Proxy/2025-11-07-12-54-47.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"contractName": "WeETHWithdrawAdapter_Proxy", | ||
"deploymentParameters": { | ||
"factory": "0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99", | ||
"salt": "0x97cd063025712908c02476f03a21565e2fd4f801000000000000000000000000", | ||
"constructorArgsEncoded": "0x0000000000000000000000003f313f0a856ae12b0a16178e29b6ada84c25695200000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000024c4d66de80000000000000000000000009f26d4c958fd811a1f59b01b86be7dffc9d2076100000000000000000000000000000000000000000000000000000000" | ||
}, | ||
"deployedAddress": "0xFbfe6b9cEe0E555Bad7e2E7309EFFC75200cBE38" | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,327 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.13; | ||
|
||
import "forge-std/Script.sol"; | ||
import "forge-std/console.sol"; | ||
import "forge-std/StdJson.sol"; | ||
import "../src/helpers/WeETHWithdrawAdapter.sol"; | ||
import "../src/UUPSProxy.sol"; | ||
|
||
interface ICreate2Factory { | ||
function deploy(bytes memory code, bytes32 salt) external payable returns (address); | ||
function verify(address addr, bytes32 salt, bytes memory code) external view returns (bool); | ||
function computeAddress(bytes32 salt, bytes memory code) external view returns (address); | ||
} | ||
|
||
/** | ||
* @title Deploy WeETH Withdraw Adapter | ||
* @notice Deploys the WeETHWithdrawAdapter implementation and UUPS proxy using Create2Factory | ||
* | ||
* This script will: | ||
* 1. Compute and display predicted deployment addresses (deterministic across all chains) | ||
* 2. Deploy the WeETHWithdrawAdapter implementation using Create2 | ||
* 3. Deploy the UUPSProxy with initialization using Create2 | ||
* 4. Transfer ownership to the EtherFi Timelock | ||
* 5. Verify the deployment | ||
* 6. Save deployment logs to ./deployment/{contractName}/{timestamp}.json | ||
* | ||
* Key Features: | ||
* - Uses Create2Factory (0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99) for deterministic addresses | ||
* - Same addresses across all EVM chains (Mainnet, Base, Arbitrum, etc.) | ||
* - Commit hash salt: 5bb56076faac983d51c2145b4de117335f6e4fa5 | ||
* | ||
* Usage: | ||
* | ||
* 1. Dry run (compute addresses without deploying): | ||
* forge script ./script/DeployWeETHWithdrawAdapter.s.sol:DeployWeETHWithdrawAdapter | ||
* Note: Will fail at deployment step but shows predicted addresses | ||
* | ||
* 2. Mainnet deployment: | ||
* source .env && forge script ./script/DeployWeETHWithdrawAdapter.s.sol:DeployWeETHWithdrawAdapter \ | ||
* --rpc-url $MAINNET_RPC_URL \ | ||
* --broadcast \ | ||
* --verify \ | ||
* --etherscan-api-key $ETHERSCAN_API_KEY \ | ||
* --slow \ | ||
* -vvvv | ||
* | ||
* 3. Other chains (Base, Arbitrum, etc.): | ||
* forge script ./script/DeployWeETHWithdrawAdapter.s.sol:DeployWeETHWithdrawAdapter \ | ||
* --rpc-url $CHAIN_RPC_URL \ | ||
* --broadcast \ | ||
* --verify \ | ||
* -vvvv | ||
* | ||
* Important: The contract addresses MUST match across all chains for the deployment to work. | ||
* Update the constants if deploying to chains with different addresses. | ||
*/ | ||
contract DeployWeETHWithdrawAdapter is Script { | ||
using stdJson for string; | ||
|
||
// Create2Factory address (same as in DeployV3Prelude.s.sol) | ||
ICreate2Factory constant factory = ICreate2Factory(0x356d1B83970CeF2018F2c9337cDdb67dff5AEF99); | ||
|
||
// Mainnet contract addresses | ||
address constant weETHAddress = 0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee; | ||
address constant eETHAddress = 0x35fA164735182de50811E8e2E824cFb9B6118ac2; | ||
address constant liquidityPoolAddress = 0x308861A430be4cce5502d0A12724771Fc6DaF216; | ||
address constant withdrawRequestNFTAddress = 0x7d5706f6ef3F89B3951E23e557CDFBC3239D4E2c; | ||
address constant roleRegistryAddress = 0x62247D29B4B9BECf4BB73E0c722cf6445cfC7cE9; | ||
address constant timelockAddress = 0x9f26d4C958fD811A1F59B01B86Be7dFFc9d20761; | ||
|
||
// Commit hash salt | ||
bytes32 constant commitHashSalt = bytes32(bytes20(hex"97cd063025712908c02476f03a21565e2fd4f801")); | ||
|
||
function run() external { | ||
console.log("\n========================================"); | ||
console.log("WeETH Withdraw Adapter Deployment"); | ||
console.log("Using Create2Factory for deterministic addresses"); | ||
console.log("========================================\n"); | ||
|
||
// Load deployer private key | ||
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); | ||
address deployer = vm.addr(deployerPrivateKey); | ||
console.log("Deployer address:", deployer); | ||
|
||
// Display addresses for verification | ||
displayAddresses(); | ||
|
||
// Compute and display predicted addresses | ||
console.log("\n========================================"); | ||
console.log("Predicted Deployment Addresses"); | ||
console.log("========================================"); | ||
computePredictedAddresses(); | ||
|
||
// Start deployment | ||
vm.startBroadcast(deployerPrivateKey); | ||
|
||
// Deploy WeETHWithdrawAdapter implementation | ||
{ | ||
string memory contractName = "WeETHWithdrawAdapter"; | ||
bytes memory constructorArgs = abi.encode( | ||
weETHAddress, | ||
eETHAddress, | ||
liquidityPoolAddress, | ||
withdrawRequestNFTAddress, | ||
roleRegistryAddress | ||
); | ||
bytes memory bytecode = abi.encodePacked( | ||
type(WeETHWithdrawAdapter).creationCode, | ||
constructorArgs | ||
); | ||
address deployedAddress = deployContract(contractName, constructorArgs, bytecode, commitHashSalt); | ||
verifyContract(deployedAddress, bytecode, commitHashSalt); | ||
} | ||
|
||
// Deploy UUPSProxy | ||
{ | ||
string memory contractName = "WeETHWithdrawAdapter_Proxy"; | ||
|
||
// Get implementation address (we need to recalculate it) | ||
bytes memory implConstructorArgs = abi.encode( | ||
weETHAddress, | ||
eETHAddress, | ||
liquidityPoolAddress, | ||
withdrawRequestNFTAddress, | ||
roleRegistryAddress | ||
); | ||
bytes memory implBytecode = abi.encodePacked( | ||
type(WeETHWithdrawAdapter).creationCode, | ||
implConstructorArgs | ||
); | ||
address implementationAddress = factory.computeAddress(commitHashSalt, implBytecode); | ||
|
||
// Prepare initialization data with timelock as initial owner | ||
bytes memory initializerData = abi.encodeWithSelector( | ||
WeETHWithdrawAdapter.initialize.selector, | ||
timelockAddress | ||
); | ||
|
||
bytes memory constructorArgs = abi.encode( | ||
implementationAddress, | ||
initializerData | ||
); | ||
bytes memory bytecode = abi.encodePacked( | ||
type(UUPSProxy).creationCode, | ||
constructorArgs | ||
); | ||
address proxyAddress = deployContract(contractName, constructorArgs, bytecode, commitHashSalt); | ||
verifyContract(proxyAddress, bytecode, commitHashSalt); | ||
|
||
// Wrap proxy in implementation interface | ||
WeETHWithdrawAdapter adapter = WeETHWithdrawAdapter(proxyAddress); | ||
|
||
// Verify ownership is already set to timelock (no transfer needed) | ||
console.log("\nVerifying ownership..."); | ||
address currentOwner = adapter.owner(); | ||
require(currentOwner == timelockAddress, "Owner should be timelock"); | ||
console.log("Owner correctly set to timelock:", currentOwner); | ||
|
||
// Final verification | ||
console.log("\nVerifying deployment..."); | ||
verifyDeployment(adapter, implementationAddress); | ||
} | ||
|
||
vm.stopBroadcast(); | ||
} | ||
|
||
function displayAddresses() internal pure { | ||
console.log("\nUsing Mainnet Contract Addresses:"); | ||
console.log("-------------------"); | ||
console.log("WeETH: ", weETHAddress); | ||
console.log("EETH: ", eETHAddress); | ||
console.log("LiquidityPool: ", liquidityPoolAddress); | ||
console.log("WithdrawRequestNFT: ", withdrawRequestNFTAddress); | ||
console.log("RoleRegistry: ", roleRegistryAddress); | ||
console.log("EtherFiTimelock: ", timelockAddress); | ||
console.log("\nCreate2Factory: ", address(factory)); | ||
console.log("Commit Hash Salt: ", vm.toString(commitHashSalt)); | ||
} | ||
|
||
function computePredictedAddresses() internal { | ||
// Compute implementation address | ||
bytes memory implConstructorArgs = abi.encode( | ||
weETHAddress, | ||
eETHAddress, | ||
liquidityPoolAddress, | ||
withdrawRequestNFTAddress, | ||
roleRegistryAddress | ||
); | ||
bytes memory implBytecode = abi.encodePacked( | ||
type(WeETHWithdrawAdapter).creationCode, | ||
implConstructorArgs | ||
); | ||
address predictedImpl = factory.computeAddress(commitHashSalt, implBytecode); | ||
console.log("Implementation: ", predictedImpl); | ||
|
||
// Compute proxy address (with timelock as initial owner) | ||
bytes memory initializerData = abi.encodeWithSelector( | ||
WeETHWithdrawAdapter.initialize.selector, | ||
timelockAddress | ||
); | ||
bytes memory proxyConstructorArgs = abi.encode( | ||
predictedImpl, | ||
initializerData | ||
); | ||
bytes memory proxyBytecode = abi.encodePacked( | ||
type(UUPSProxy).creationCode, | ||
proxyConstructorArgs | ||
); | ||
address predictedProxy = factory.computeAddress(commitHashSalt, proxyBytecode); | ||
console.log("Proxy: ", predictedProxy); | ||
console.log("Initial Owner: ", timelockAddress); | ||
console.log("-------------------"); | ||
console.log("Note: These addresses are deterministic across all EVM chains"); | ||
} | ||
|
||
function deployContract( | ||
string memory contractName, | ||
bytes memory constructorArgs, | ||
bytes memory bytecode, | ||
bytes32 salt | ||
) internal returns (address) { | ||
address predictedAddress = factory.computeAddress(salt, bytecode); | ||
console.log("\nDeploying", contractName); | ||
console.log("Predicted address:", predictedAddress); | ||
|
||
address deployedAddress = factory.deploy(bytecode, salt); | ||
require(deployedAddress == predictedAddress, "Deployment address mismatch"); | ||
|
||
console.log("Deployed at:", deployedAddress); | ||
|
||
// Save deployment log | ||
saveDeploymentLog(contractName, deployedAddress, constructorArgs, salt); | ||
|
||
return deployedAddress; | ||
} | ||
|
||
function verifyContract(address addr, bytes memory bytecode, bytes32 salt) internal view returns (bool) { | ||
bool verified = factory.verify(addr, salt, bytecode); | ||
require(verified, "Contract verification failed"); | ||
console.log("Verification: PASS"); | ||
return verified; | ||
} | ||
|
||
function saveDeploymentLog( | ||
string memory contractName, | ||
address deployedAddress, | ||
bytes memory constructorArgs, | ||
bytes32 salt | ||
) internal { | ||
string memory deployLog = string.concat( | ||
"{\n", | ||
' "contractName": "', contractName, '",\n', | ||
' "deploymentParameters": {\n', | ||
' "factory": "', vm.toString(address(factory)), '",\n', | ||
' "salt": "', vm.toString(salt), '",\n', | ||
' "constructorArgsEncoded": "', vm.toString(constructorArgs), '"\n', | ||
' },\n', | ||
' "deployedAddress": "', vm.toString(deployedAddress), '"\n', | ||
"}" | ||
); | ||
|
||
string memory root = vm.projectRoot(); | ||
string memory logFileDir = string.concat(root, "/deployment/", contractName); | ||
vm.createDir(logFileDir, true); | ||
|
||
string memory logFileName = string.concat( | ||
logFileDir, | ||
"/", | ||
getTimestampString(), | ||
".json" | ||
); | ||
vm.writeFile(logFileName, deployLog); | ||
|
||
console.log("Deployment log saved to:", logFileName); | ||
} | ||
|
||
function verifyDeployment(WeETHWithdrawAdapter adapter, address implementationAddress) internal view { | ||
console.log("-------------------"); | ||
|
||
// Verify immutable variables | ||
require(address(adapter.weETH()) == weETHAddress, "WeETH address mismatch"); | ||
console.log("[PASS] WeETH address verified"); | ||
|
||
require(address(adapter.eETH()) == eETHAddress, "EETH address mismatch"); | ||
console.log("[PASS] EETH address verified"); | ||
|
||
require(address(adapter.liquidityPool()) == liquidityPoolAddress, "LiquidityPool address mismatch"); | ||
console.log("[PASS] LiquidityPool address verified"); | ||
|
||
require(address(adapter.withdrawRequestNFT()) == withdrawRequestNFTAddress, "WithdrawRequestNFT address mismatch"); | ||
console.log("[PASS] WithdrawRequestNFT address verified"); | ||
|
||
require(address(adapter.roleRegistry()) == roleRegistryAddress, "RoleRegistry address mismatch"); | ||
console.log("[PASS] RoleRegistry address verified"); | ||
|
||
// Verify initialization state | ||
require(!adapter.paused(), "Contract should not be paused"); | ||
console.log("[PASS] Contract is not paused"); | ||
|
||
require(adapter.owner() == timelockAddress, "Owner should be timelock"); | ||
console.log("[PASS] Owner is timelock"); | ||
|
||
// Verify implementation | ||
address actualImpl = adapter.getImplementation(); | ||
require(actualImpl == implementationAddress, "Implementation address mismatch"); | ||
console.log("[PASS] Implementation address verified:", actualImpl); | ||
|
||
console.log("\n[PASS] All verifications passed!"); | ||
} | ||
|
||
function getTimestampString() internal view returns (string memory) { | ||
uint256 ts = block.timestamp; | ||
string memory year = vm.toString((ts / 31536000) + 1970); | ||
string memory month = pad(vm.toString(((ts % 31536000) / 2592000) + 1)); | ||
string memory day = pad(vm.toString(((ts % 2592000) / 86400) + 1)); | ||
string memory hour = pad(vm.toString((ts % 86400) / 3600)); | ||
string memory minute = pad(vm.toString((ts % 3600) / 60)); | ||
string memory second = pad(vm.toString(ts % 60)); | ||
return string.concat(year, "-", month, "-", day, "-", hour, "-", minute, "-", second); | ||
} | ||
|
||
function pad(string memory n) internal pure returns (string memory) { | ||
return bytes(n).length == 1 ? string.concat("0", n) : n; | ||
} | ||
} | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Timestamp Calculation Errors in Deployment Logs
The
getTimestampString
function calculates deployment log timestamps. It incorrectly assumes fixed durations for months (30 days) and years (365 days, ignoring leap years), leading to mathematically inaccurate month and day values in the generated filenames.