From 313f66ac823b2a4b42699c6c9c847ebe065cd072 Mon Sep 17 00:00:00 2001 From: fredwes <6827305+fredwes@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:45:27 -0400 Subject: [PATCH 1/4] feat: sip 49 --- helpers/SeamlessAddressBook.sol | 3 + .../sip_36_listing_weETH/TestProposal.t.sol | 1 - proposals/sip_49/DeployProposal.s.sol | 30 +++ proposals/sip_49/Proposal.sol | 58 +++++ proposals/sip_49/TenderlySimulation.s.sol | 159 ++++++++++++ proposals/sip_49/TestProposal.t.sol | 226 ++++++++++++++++++ proposals/sip_49/description.md | 80 +++++++ 7 files changed, 556 insertions(+), 1 deletion(-) delete mode 100644 proposals/sip_36_listing_weETH/TestProposal.t.sol create mode 100644 proposals/sip_49/DeployProposal.s.sol create mode 100644 proposals/sip_49/Proposal.sol create mode 100644 proposals/sip_49/TenderlySimulation.s.sol create mode 100644 proposals/sip_49/TestProposal.t.sol create mode 100644 proposals/sip_49/description.md diff --git a/helpers/SeamlessAddressBook.sol b/helpers/SeamlessAddressBook.sol index 73eb4e7..4335d9e 100644 --- a/helpers/SeamlessAddressBook.sol +++ b/helpers/SeamlessAddressBook.sol @@ -81,4 +81,7 @@ library SeamlessAddressBook { 0x003D47ddDdb070822B35ae5cc4F0066Cf9E89753; address constant BRETT_TRANSFER_STRATEGY = 0xD90EaC90f5f067283954b96BBc3d28E34ebE55Bb; + + address constant BASE_LEVERAGE_MANAGER_PROXY = 0x38Ba21C6Bf31dF1b1798FCEd07B4e9b07C5ec3a8; + address constant BASE_LEVERAGE_TOKEN_FACTORY_PROXY = 0xE0b2e40EDeb53B96C923381509a25a615c1Abe57; } \ No newline at end of file diff --git a/proposals/sip_36_listing_weETH/TestProposal.t.sol b/proposals/sip_36_listing_weETH/TestProposal.t.sol deleted file mode 100644 index 8b13789..0000000 --- a/proposals/sip_36_listing_weETH/TestProposal.t.sol +++ /dev/null @@ -1 +0,0 @@ - diff --git a/proposals/sip_49/DeployProposal.s.sol b/proposals/sip_49/DeployProposal.s.sol new file mode 100644 index 0000000..37d5614 --- /dev/null +++ b/proposals/sip_49/DeployProposal.s.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { Script, console } from "forge-std/Script.sol"; +import { Proposal } from "./Proposal.sol"; +import { IGovernor } from "@openzeppelin/contracts/governance/IGovernor.sol"; +import { SeamlessAddressBook } from "../../helpers/SeamlessAddressBook.sol"; + +contract DeployProposal is Script { + function setUp() public { } + + function run(string memory descriptionPath) public { + Proposal proposal = new Proposal(); + + // Change this to GOVERNOR_LONG if you want to make proposal on the long governor + IGovernor governance = IGovernor(SeamlessAddressBook.GOVERNOR_SHORT); + + string memory description = vm.readFile(descriptionPath); + + address proposerAddress = vm.envAddress("PROPOSER_ADDRESS"); + vm.startBroadcast(proposerAddress); + governance.propose( + proposal.getTargets(), + proposal.getValues(), + proposal.getCalldatas(), + description + ); + vm.stopBroadcast(); + } +} diff --git a/proposals/sip_49/Proposal.sol b/proposals/sip_49/Proposal.sol new file mode 100644 index 0000000..6b8e2e7 --- /dev/null +++ b/proposals/sip_49/Proposal.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { + SeamlessGovProposal, + SeamlessAddressBook +} from "../../helpers/SeamlessGovProposal.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +contract Proposal is SeamlessGovProposal { + address constant NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; + address constant NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = 0xfE9101349354E278970489F935a54905DE2E1856; + + // Access control role required for upgrading + bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); + + constructor() { + _makeProposal(); + } + + /// @dev This contract is not deployed onchain, do not make transactions to other contracts + /// or deploy a contract. Only the view/pure functions of deployed contracts can be called. + function _makeProposal() internal virtual override { + // First, grant the UPGRADER_ROLE to the timelock so it can perform the upgrade + _addAction( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + abi.encodeWithSelector( + IAccessControl.grantRole.selector, + UPGRADER_ROLE, + SeamlessAddressBook.TIMELOCK_SHORT + ) + ); + + // Upgrade the Base Leverage Token implementation (beacon proxy) + _addAction( + SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY, + abi.encodeWithSelector( + UpgradeableBeacon.upgradeTo.selector, + NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION + ) + ); + + // Upgrade the Base Leverage Manager implementation (UUPS proxy) + _addAction( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + abi.encodeWithSelector( + UUPSUpgradeable.upgradeToAndCall.selector, + NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, + "" + ) + ); + + // Note: UPGRADER_ROLE is NOT revoked after the upgrade + // The timelock will retain this role for future upgrades + } +} diff --git a/proposals/sip_49/TenderlySimulation.s.sol b/proposals/sip_49/TenderlySimulation.s.sol new file mode 100644 index 0000000..39919d1 --- /dev/null +++ b/proposals/sip_49/TenderlySimulation.s.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import { Script, console } from "forge-std/Script.sol"; +import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; +import { IVotes } from "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import { Proposal } from "./Proposal.sol"; +import { IGovernor } from "@openzeppelin/contracts/governance/IGovernor.sol"; +import { SeamlessAddressBook } from "../../helpers/SeamlessAddressBook.sol"; +import { IVotes } from "@openzeppelin/contracts/governance/utils/IVotes.sol"; + +contract TenderlySimulation is Script { + // Change this to GOVERNOR_LONG if the proposal is made on the long governor + IGovernor governance = IGovernor(SeamlessAddressBook.GOVERNOR_SHORT); + IVotes seam = IVotes(SeamlessAddressBook.SEAM); + + Proposal proposal = new Proposal(); + + address proposerAddress = 0x67b6dB42115d94Cc3FE27E92a3d12bB224041ac0; + uint256 proposerPk = + 0x82fe25cccae9752b856c8857de74671320277f92e737b2116a5d9739dec59a26; + + function _getProposalData(string memory descriptionPath) + internal + view + returns (uint256 proposalId, bytes32 descriptionHash) + { + string memory description = vm.readFile(descriptionPath); + descriptionHash = keccak256(bytes(description)); + + proposalId = governance.hashProposal( + proposal.getTargets(), + proposal.getValues(), + proposal.getCalldatas(), + descriptionHash + ); + } + + function setupProposer() public { + console.log("Setting up proposer"); + + _fundETH(); + _fundSEAM(); + + moveOneBlockForwardOneSecond(); + } + + function delegateToProposer() public { + vm.startBroadcast(proposerPk); + seam.delegate(proposerAddress); + vm.stopBroadcast(); + } + + function moveOneBlockForwardOneSecond() public { + vm.rpc("evm_increaseTime", "[\"0x1\"]"); + } + + function _fundSEAM() public { + string memory params = string.concat( + "[\"", + Strings.toHexString(address(seam)), + "\",[\"", + Strings.toHexString(proposerAddress), + "\"],\"0x13DA329B6336471800000\"]" // 1.5M SEAM which is quorum + ); + vm.rpc("tenderly_setErc20Balance", params); + } + + function _fundETH() public { + string memory params = string.concat( + "[[\"", + Strings.toHexString(proposerAddress), + "\"],\"0xDE0B6B3A7640000\"]" // 1 ETH + ); + vm.rpc("tenderly_setBalance", params); + } + + function createProposal(string memory descriptionPath) public { + string memory description = vm.readFile(descriptionPath); + + vm.startBroadcast(proposerPk); + governance.propose( + proposal.getTargets(), + proposal.getValues(), + proposal.getCalldatas(), + description + ); + vm.stopBroadcast(); + } + + function increaseTimeVotingDelay() public { + console.log("Increasing voting delay"); + string memory params = string.concat( + "[", + Strings.toString(block.timestamp + governance.votingDelay() + 1), + "]" + ); + vm.rpc("evm_setNextBlockTimestamp", params); + vm.rpc("evm_mine", "[]"); + } + + function castVote(string memory descriptionPath) public { + console.log("Casting vote"); + (uint256 proposalId,) = _getProposalData(descriptionPath); + + vm.startBroadcast(proposerPk); + governance.castVote(proposalId, 1); + vm.stopBroadcast(); + } + + function increaseTimeVotingPeriod() public { + console.log("Increasing voting period"); + string memory params = string.concat( + "[", + Strings.toString(block.timestamp + governance.votingPeriod() + 1), + "]" + ); + vm.rpc("evm_setNextBlockTimestamp", params); + vm.rpc("evm_mine", "[]"); + } + + function queueProposal(string memory descriptionPath) public { + console.log("Queueing proposal"); + (, bytes32 descriptionHash) = _getProposalData(descriptionPath); + + vm.startBroadcast(proposerPk); + governance.queue( + proposal.getTargets(), + proposal.getValues(), + proposal.getCalldatas(), + descriptionHash + ); + vm.stopBroadcast(); + } + + function setTimeToProposalEta(string memory descriptionPath) public { + console.log("Setting time to proposal eta"); + (uint256 proposalId,) = _getProposalData(descriptionPath); + string memory params = string.concat( + "[", Strings.toString(governance.proposalEta(proposalId) + 1), "]" + ); + vm.rpc("evm_setNextBlockTimestamp", params); + vm.rpc("evm_mine", "[]"); + } + + function executeProposal(string memory descriptionPath) public { + console.log("Executing proposal"); + (, bytes32 descriptionHash) = _getProposalData(descriptionPath); + + vm.startBroadcast(proposerPk); + governance.execute( + proposal.getTargets(), + proposal.getValues(), + proposal.getCalldatas(), + descriptionHash + ); + vm.stopBroadcast(); + } +} diff --git a/proposals/sip_49/TestProposal.t.sol b/proposals/sip_49/TestProposal.t.sol new file mode 100644 index 0000000..484c7d6 --- /dev/null +++ b/proposals/sip_49/TestProposal.t.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { GovTestHelper } from "../../helpers/GovTestHelper.sol"; +import { Proposal } from "./Proposal.sol"; +import { SeamlessAddressBook } from "../../helpers/SeamlessAddressBook.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; + +contract TestProposal is GovTestHelper { + // ERC1967 implementation slot for UUPS proxy + bytes32 constant ERC1967_IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + Proposal public proposal; + + // Expected new implementation addresses from the proposal + address constant EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; + address constant EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = 0xfE9101349354E278970489F935a54905DE2E1856; + + function setUp() public { + vm.rollFork(36538639); + proposal = new Proposal(); + } + + function test_baseLeverageTokenImplementationIsUpgraded_afterPassingProposal() public { + // Get the beacon proxy instance + UpgradeableBeacon beacon = UpgradeableBeacon(SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY); + + // Get current implementation from the beacon + address implementationBefore = beacon.implementation(); + + // Verify it's not already the new implementation + assertNotEq( + implementationBefore, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, + "Base Leverage Token implementation should not be upgraded yet" + ); + + // Pass the proposal + _passProposalShortGov(proposal); + + // Get the implementation after the proposal + address implementationAfter = beacon.implementation(); + + // Verify the implementation has been upgraded to the expected address + assertEq( + implementationAfter, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, + "Base Leverage Token implementation should be upgraded to the new implementation" + ); + } + + function test_baseLeverageManagerImplementationIsUpgraded_afterPassingProposal() public { + // Get current implementation from the UUPS proxy storage slot + address implementationBefore = address( + uint160( + uint256( + vm.load( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + ERC1967_IMPLEMENTATION_SLOT + ) + ) + ) + ); + + // Verify it's not already the new implementation + assertNotEq( + implementationBefore, + EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, + "Base Leverage Manager implementation should not be upgraded yet" + ); + + // Pass the proposal + _passProposalShortGov(proposal); + + // Get the implementation after the proposal + address implementationAfter = address( + uint160( + uint256( + vm.load( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + ERC1967_IMPLEMENTATION_SLOT + ) + ) + ) + ); + + // Verify the implementation has been upgraded to the expected address + assertEq( + implementationAfter, + EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, + "Base Leverage Manager implementation should be upgraded to the new implementation" + ); + } + + function test_bothImplementationsAreUpgraded_afterPassingProposal() public { + // Get beacon for Base Leverage Token + UpgradeableBeacon beacon = UpgradeableBeacon(SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY); + + // Store both implementations before the proposal + address tokenImplementationBefore = beacon.implementation(); + address managerImplementationBefore = address( + uint160( + uint256( + vm.load( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + ERC1967_IMPLEMENTATION_SLOT + ) + ) + ) + ); + + // Verify neither implementation is already upgraded + assertNotEq(tokenImplementationBefore, EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION); + assertNotEq(managerImplementationBefore, EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION); + + // Pass the proposal + _passProposalShortGov(proposal); + + // Get both implementations after the proposal + address tokenImplementationAfter = beacon.implementation(); + address managerImplementationAfter = address( + uint160( + uint256( + vm.load( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + ERC1967_IMPLEMENTATION_SLOT + ) + ) + ) + ); + + // Verify both implementations have been upgraded correctly + assertEq( + tokenImplementationAfter, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, + "Base Leverage Token implementation should be upgraded" + ); + + assertEq( + managerImplementationAfter, + EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, + "Base Leverage Manager implementation should be upgraded" + ); + } + + function test_proposalActionsMatchExpected() public view { + // The proposal now needs to read the UPGRADER_ROLE from itself + bytes32 upgraderRole = proposal.UPGRADER_ROLE(); + + // Verify the proposal has exactly 3 actions (no revoke action) + assertEq(proposal.getTargets().length, 3, "Proposal should have exactly 3 actions"); + + // Action 1: Grant UPGRADER_ROLE to timelock + assertEq( + proposal.getTargets()[0], + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + "First target should be Base Leverage Manager Proxy (grant role)" + ); + bytes memory expectedCalldata1 = abi.encodeWithSelector( + bytes4(keccak256("grantRole(bytes32,address)")), + upgraderRole, + SeamlessAddressBook.TIMELOCK_SHORT + ); + assertEq( + proposal.getCalldatas()[0], + expectedCalldata1, + "First action should grant UPGRADER_ROLE to timelock" + ); + + // Action 2: Upgrade Base Leverage Token Factory + assertEq( + proposal.getTargets()[1], + SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY, + "Second target should be Base Leverage Token Factory Proxy" + ); + bytes memory expectedCalldata2 = abi.encodeWithSelector( + UpgradeableBeacon.upgradeTo.selector, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION + ); + assertEq( + proposal.getCalldatas()[1], + expectedCalldata2, + "Second action calldata should match expected upgradeTo call" + ); + + // Action 3: Upgrade Base Leverage Manager + assertEq( + proposal.getTargets()[2], + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY, + "Third target should be Base Leverage Manager Proxy (upgrade)" + ); + bytes memory expectedCalldata3 = abi.encodeWithSelector( + bytes4(keccak256("upgradeToAndCall(address,bytes)")), + EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, + "" + ); + assertEq( + proposal.getCalldatas()[2], + expectedCalldata3, + "Third action calldata should match expected upgradeToAndCall call" + ); + + // Note: The timelock will retain this role for future upgrades + } + + function test_accessControlIsHandledCorrectly() public { + bytes32 upgraderRole = proposal.UPGRADER_ROLE(); + + // Check that the timelock doesn't have the UPGRADER_ROLE initially + bool hasRoleBefore = IAccessControl(SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY) + .hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); + assertFalse(hasRoleBefore, "Timelock should not have UPGRADER_ROLE before proposal"); + + // Pass the proposal + _passProposalShortGov(proposal); + + // After the proposal, the timelock should have been granted the role + // and it should STILL have the role (not revoked) + bool hasRoleAfter = IAccessControl(SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY) + .hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); + assertTrue(hasRoleAfter, "Timelock should have UPGRADER_ROLE after proposal (not revoked)"); + } +} diff --git a/proposals/sip_49/description.md b/proposals/sip_49/description.md new file mode 100644 index 0000000..8a011fb --- /dev/null +++ b/proposals/sip_49/description.md @@ -0,0 +1,80 @@ +# [SIP-49] Upgrade Base Leverage Token and Manager Implementations + +## Summary + +This proposal upgrades the Seamless Leverage Token infrastructure on Base by updating both the Base Leverage Token implementation and the Base Leverage Manager implementation to their latest versions. These upgrades bring improvements in user experience, protocol integration capabilities, gas optimizations, and alignment with the Ethereum mainnet deployment. + +## Context and Motivation + +Seamless Leverage Tokens are ERC20 tokenized representations of leveraged positions between two assets, providing users with simple, liquid exposure to leveraged strategies without managing complex DeFi positions directly. Since the initial Base deployment in June 2025, the protocol has undergone optimization based on user feedback, and preparation for the Ethereum mainnet launch. + +The upgrades proposed in this SIP represent four months of development work ([base-deploy-jun-02-2025](https://github.com/seamless-protocol/leverage-tokens/tree/base-deploy-jun-02-2025) → [base-deploy-oct-7-2025](https://github.com/seamless-protocol/leverage-tokens/tree/base-deploy-oct-7-2025)) and are essential for: + +1. **Cross-chain consistency**: Aligning Base deployment with the upcoming Ethereum mainnet launch +2. **Performance improvements**: Reducing gas costs for common operations +3. **Feature parity**: Ensuring Base users have access to the latest protocol capabilities deployed on Ethereum + +## Technical Overview + +### Contracts Being Upgraded + +1. **Base Leverage Token Implementation** + - Current: Previous implementation from June 2025 deployment + - New: `0x057A2a1CC13A9Af430976af912A27A05DE537673` + - Upgrade mechanism: Beacon Proxy pattern via `UpgradeableBeacon` + +2. **Base Leverage Manager Implementation** + - Current: Previous implementation from June 2025 deployment + - New: `0xeb0221bf6cdaa74c94129771d5b0c9a994bb2b7c` + - Upgrade mechanism: UUPS Proxy pattern + +### Key Improvements + +1. Enhanced user experience and third party integration ease + +2. Fee calculation improvements + +3. Gas optimizations + +4. Code quality and edge cases + +### Breaking Changes + +While these upgrades introduce breaking changes to the smart contract interfaces, they are fully backward compatible from a user perspective: + +- **UI Compatibility**: The Seamless UI has been updated to support the new interfaces +- **Token Compatibility**: Existing leverage tokens remain fully functional +- **Position Safety**: All existing positions are preserved and protected during the upgrade + +## Security Considerations + +### Audit Status +The upgraded implementations have undergone comprehensive security review by Cantina (September, 2025). Full audit report available at [Cantina Security Report](https://cantina.xyz/portfolio/6291d7fa-62ac-4e18-9c2d-1403bfdd3c6c) + +## Implementation Details + +### Proposal Actions + +The proposal will execute the following actions through the Seamless governance timelock: + +1. **Grant UPGRADER_ROLE** to the timelock (required for UUPS upgrade) +2. **Upgrade Base Leverage Token** implementation via beacon proxy +3. **Upgrade Base Leverage Manager** implementation via UUPS proxy + +Note: The `UPGRADER_ROLE` is retained by the timelock after execution to facilitate future upgrades if needed. + +## Conclusion + +This upgrade represents an important step in the evolution of Seamless Leverage Tokens on Base. By implementing these improvements, we ensure that Base users have access to an optimized, user-friendly, and feature-complete version of the protocol that matches the Ethereum mainnet deployment. The upgrades have been thoroughly tested and audited, with careful consideration given to maintaining backward compatibility. + +We encourage the community to review the technical details and participate in the governance vote to advance the Seamless protocol on Base. + +## References + +- [Seamless Leverage Tokens Repository](https://github.com/seamless-protocol/leverage-tokens) +- [Old Implementation (June 2025)](https://github.com/seamless-protocol/leverage-tokens/tree/base-deploy-jun-02-2025) +- [New Implementation (October 2025)](https://github.com/seamless-protocol/leverage-tokens/tree/base-deploy-oct-7-2025) +- [Upgrade Diff: base-deploy-jun-02-2025 → base-deploy-oct-7-2025](https://github.com/seamless-protocol/leverage-tokens/compare/base-deploy-jun-02-2025...base-deploy-oct-7-2025) +- [Cantina Security Audit (September 2025)](https://cantina.xyz/portfolio/6291d7fa-62ac-4e18-9c2d-1403bfdd3c6c) +- [Audit Reports](https://github.com/seamless-protocol/leverage-tokens/tree/main/audits) +- [Technical Documentation](https://github.com/seamless-protocol/leverage-tokens/tree/main/docs) \ No newline at end of file From b8d0ea3f568b56647aa58c6caa54cd9f1277a066 Mon Sep 17 00:00:00 2001 From: fredwes <6827305+fredwes@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:47:19 -0400 Subject: [PATCH 2/4] chore: fmt --- proposals/sip_49/Proposal.sol | 19 +++-- proposals/sip_49/TestProposal.t.sol | 103 ++++++++++++++++++---------- 2 files changed, 79 insertions(+), 43 deletions(-) diff --git a/proposals/sip_49/Proposal.sol b/proposals/sip_49/Proposal.sol index 6b8e2e7..5db9b9b 100644 --- a/proposals/sip_49/Proposal.sol +++ b/proposals/sip_49/Proposal.sol @@ -5,14 +5,19 @@ import { SeamlessGovProposal, SeamlessAddressBook } from "../../helpers/SeamlessGovProposal.sol"; -import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; -import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { UpgradeableBeacon } from + "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { UUPSUpgradeable } from + "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; +import { IAccessControl } from + "@openzeppelin/contracts/access/IAccessControl.sol"; contract Proposal is SeamlessGovProposal { - address constant NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; - address constant NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = 0xfE9101349354E278970489F935a54905DE2E1856; - + address constant NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = + 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; + address constant NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = + 0xfE9101349354E278970489F935a54905DE2E1856; + // Access control role required for upgrading bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); @@ -51,7 +56,7 @@ contract Proposal is SeamlessGovProposal { "" ) ); - + // Note: UPGRADER_ROLE is NOT revoked after the upgrade // The timelock will retain this role for future upgrades } diff --git a/proposals/sip_49/TestProposal.t.sol b/proposals/sip_49/TestProposal.t.sol index 484c7d6..7cde78b 100644 --- a/proposals/sip_49/TestProposal.t.sol +++ b/proposals/sip_49/TestProposal.t.sol @@ -4,9 +4,12 @@ pragma solidity ^0.8.25; import { GovTestHelper } from "../../helpers/GovTestHelper.sol"; import { Proposal } from "./Proposal.sol"; import { SeamlessAddressBook } from "../../helpers/SeamlessAddressBook.sol"; -import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import { ERC1967Utils } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; -import { IAccessControl } from "@openzeppelin/contracts/access/IAccessControl.sol"; +import { UpgradeableBeacon } from + "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { ERC1967Utils } from + "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; +import { IAccessControl } from + "@openzeppelin/contracts/access/IAccessControl.sol"; contract TestProposal is GovTestHelper { // ERC1967 implementation slot for UUPS proxy @@ -16,24 +19,29 @@ contract TestProposal is GovTestHelper { Proposal public proposal; // Expected new implementation addresses from the proposal - address constant EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; - address constant EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = 0xfE9101349354E278970489F935a54905DE2E1856; + address constant EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION = + 0x603Da735780e6bC7D04f3FB85C26dccCd4Ff0a82; + address constant EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION = + 0xfE9101349354E278970489F935a54905DE2E1856; function setUp() public { vm.rollFork(36538639); proposal = new Proposal(); } - function test_baseLeverageTokenImplementationIsUpgraded_afterPassingProposal() public { + function test_baseLeverageTokenImplementationIsUpgraded_afterPassingProposal( + ) public { // Get the beacon proxy instance - UpgradeableBeacon beacon = UpgradeableBeacon(SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY); - + UpgradeableBeacon beacon = UpgradeableBeacon( + SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY + ); + // Get current implementation from the beacon address implementationBefore = beacon.implementation(); - + // Verify it's not already the new implementation assertNotEq( - implementationBefore, + implementationBefore, EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, "Base Leverage Token implementation should not be upgraded yet" ); @@ -43,7 +51,7 @@ contract TestProposal is GovTestHelper { // Get the implementation after the proposal address implementationAfter = beacon.implementation(); - + // Verify the implementation has been upgraded to the expected address assertEq( implementationAfter, @@ -52,7 +60,8 @@ contract TestProposal is GovTestHelper { ); } - function test_baseLeverageManagerImplementationIsUpgraded_afterPassingProposal() public { + function test_baseLeverageManagerImplementationIsUpgraded_afterPassingProposal( + ) public { // Get current implementation from the UUPS proxy storage slot address implementationBefore = address( uint160( @@ -64,10 +73,10 @@ contract TestProposal is GovTestHelper { ) ) ); - + // Verify it's not already the new implementation assertNotEq( - implementationBefore, + implementationBefore, EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, "Base Leverage Manager implementation should not be upgraded yet" ); @@ -86,7 +95,7 @@ contract TestProposal is GovTestHelper { ) ) ); - + // Verify the implementation has been upgraded to the expected address assertEq( implementationAfter, @@ -95,10 +104,14 @@ contract TestProposal is GovTestHelper { ); } - function test_bothImplementationsAreUpgraded_afterPassingProposal() public { + function test_bothImplementationsAreUpgraded_afterPassingProposal() + public + { // Get beacon for Base Leverage Token - UpgradeableBeacon beacon = UpgradeableBeacon(SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY); - + UpgradeableBeacon beacon = UpgradeableBeacon( + SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY + ); + // Store both implementations before the proposal address tokenImplementationBefore = beacon.implementation(); address managerImplementationBefore = address( @@ -113,8 +126,14 @@ contract TestProposal is GovTestHelper { ); // Verify neither implementation is already upgraded - assertNotEq(tokenImplementationBefore, EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION); - assertNotEq(managerImplementationBefore, EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION); + assertNotEq( + tokenImplementationBefore, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION + ); + assertNotEq( + managerImplementationBefore, + EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION + ); // Pass the proposal _passProposalShortGov(proposal); @@ -138,7 +157,7 @@ contract TestProposal is GovTestHelper { EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, "Base Leverage Token implementation should be upgraded" ); - + assertEq( managerImplementationAfter, EXPECTED_NEW_BASE_LEVERAGE_MANAGER_IMPLEMENTATION, @@ -149,10 +168,14 @@ contract TestProposal is GovTestHelper { function test_proposalActionsMatchExpected() public view { // The proposal now needs to read the UPGRADER_ROLE from itself bytes32 upgraderRole = proposal.UPGRADER_ROLE(); - + // Verify the proposal has exactly 3 actions (no revoke action) - assertEq(proposal.getTargets().length, 3, "Proposal should have exactly 3 actions"); - + assertEq( + proposal.getTargets().length, + 3, + "Proposal should have exactly 3 actions" + ); + // Action 1: Grant UPGRADER_ROLE to timelock assertEq( proposal.getTargets()[0], @@ -169,7 +192,7 @@ contract TestProposal is GovTestHelper { expectedCalldata1, "First action should grant UPGRADER_ROLE to timelock" ); - + // Action 2: Upgrade Base Leverage Token Factory assertEq( proposal.getTargets()[1], @@ -185,7 +208,7 @@ contract TestProposal is GovTestHelper { expectedCalldata2, "Second action calldata should match expected upgradeTo call" ); - + // Action 3: Upgrade Base Leverage Manager assertEq( proposal.getTargets()[2], @@ -202,25 +225,33 @@ contract TestProposal is GovTestHelper { expectedCalldata3, "Third action calldata should match expected upgradeToAndCall call" ); - + // Note: The timelock will retain this role for future upgrades } function test_accessControlIsHandledCorrectly() public { bytes32 upgraderRole = proposal.UPGRADER_ROLE(); - + // Check that the timelock doesn't have the UPGRADER_ROLE initially - bool hasRoleBefore = IAccessControl(SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY) - .hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); - assertFalse(hasRoleBefore, "Timelock should not have UPGRADER_ROLE before proposal"); - + bool hasRoleBefore = IAccessControl( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY + ).hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); + assertFalse( + hasRoleBefore, + "Timelock should not have UPGRADER_ROLE before proposal" + ); + // Pass the proposal _passProposalShortGov(proposal); - + // After the proposal, the timelock should have been granted the role // and it should STILL have the role (not revoked) - bool hasRoleAfter = IAccessControl(SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY) - .hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); - assertTrue(hasRoleAfter, "Timelock should have UPGRADER_ROLE after proposal (not revoked)"); + bool hasRoleAfter = IAccessControl( + SeamlessAddressBook.BASE_LEVERAGE_MANAGER_PROXY + ).hasRole(upgraderRole, SeamlessAddressBook.TIMELOCK_SHORT); + assertTrue( + hasRoleAfter, + "Timelock should have UPGRADER_ROLE after proposal (not revoked)" + ); } } From d5e60ac66a9e04d33f3e9298fea323a49fa9dcf5 Mon Sep 17 00:00:00 2001 From: fredwes <6827305+fredwes@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:08:34 -0400 Subject: [PATCH 3/4] test: add test for LT beacon implementation --- proposals/sip_49/TestProposal.t.sol | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/proposals/sip_49/TestProposal.t.sol b/proposals/sip_49/TestProposal.t.sol index 7cde78b..2febd0c 100644 --- a/proposals/sip_49/TestProposal.t.sol +++ b/proposals/sip_49/TestProposal.t.sol @@ -254,4 +254,50 @@ contract TestProposal is GovTestHelper { "Timelock should have UPGRADER_ROLE after proposal (not revoked)" ); } + + function test_specificLeverageTokenImplementationIsUpgraded() public { + // Specific leverage token instance to check + address leverageToken = 0xA2fceEAe99d2cAeEe978DA27bE2d95b0381dBB8c; + + // Get the beacon address from the leverage token + // Beacon proxy stores the beacon address at a specific storage slot + bytes32 beaconSlot = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + address beacon = address( + uint160( + uint256( + vm.load(leverageToken, beaconSlot) + ) + ) + ); + + // Verify the beacon is the expected Base Leverage Token Factory Proxy + assertEq( + beacon, + SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY, + "Leverage token should be using the Base Leverage Token Factory beacon" + ); + + // Get the current implementation from the beacon + address implementationBefore = UpgradeableBeacon(beacon).implementation(); + + // Verify it's not already the new implementation + assertNotEq( + implementationBefore, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, + "Leverage token implementation should not be upgraded yet" + ); + + // Pass the proposal + _passProposalShortGov(proposal); + + // Get the implementation after the proposal + address implementationAfter = UpgradeableBeacon(beacon).implementation(); + + // Verify the implementation has been upgraded to the expected address + assertEq( + implementationAfter, + EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, + "Leverage token at 0xA2fceEAe99d2cAeEe978DA27bE2d95b0381dBB8c should have upgraded implementation" + ); + } } From a15f4ec8706aafc839ddc4bb07ebdae00d3a71ac Mon Sep 17 00:00:00 2001 From: fredwes <6827305+fredwes@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:15:28 -0400 Subject: [PATCH 4/4] chore: fmt --- proposals/sip_49/TestProposal.t.sol | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/proposals/sip_49/TestProposal.t.sol b/proposals/sip_49/TestProposal.t.sol index 2febd0c..dffadb2 100644 --- a/proposals/sip_49/TestProposal.t.sol +++ b/proposals/sip_49/TestProposal.t.sol @@ -258,41 +258,38 @@ contract TestProposal is GovTestHelper { function test_specificLeverageTokenImplementationIsUpgraded() public { // Specific leverage token instance to check address leverageToken = 0xA2fceEAe99d2cAeEe978DA27bE2d95b0381dBB8c; - + // Get the beacon address from the leverage token // Beacon proxy stores the beacon address at a specific storage slot - bytes32 beaconSlot = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - address beacon = address( - uint160( - uint256( - vm.load(leverageToken, beaconSlot) - ) - ) - ); - + bytes32 beaconSlot = + 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + address beacon = + address(uint160(uint256(vm.load(leverageToken, beaconSlot)))); + // Verify the beacon is the expected Base Leverage Token Factory Proxy assertEq( beacon, SeamlessAddressBook.BASE_LEVERAGE_TOKEN_FACTORY_PROXY, "Leverage token should be using the Base Leverage Token Factory beacon" ); - + // Get the current implementation from the beacon - address implementationBefore = UpgradeableBeacon(beacon).implementation(); - + address implementationBefore = + UpgradeableBeacon(beacon).implementation(); + // Verify it's not already the new implementation assertNotEq( implementationBefore, EXPECTED_NEW_BASE_LEVERAGE_TOKEN_IMPLEMENTATION, "Leverage token implementation should not be upgraded yet" ); - + // Pass the proposal _passProposalShortGov(proposal); - + // Get the implementation after the proposal address implementationAfter = UpgradeableBeacon(beacon).implementation(); - + // Verify the implementation has been upgraded to the expected address assertEq( implementationAfter,