diff --git a/helpers/SeamlessAddressBook.sol b/helpers/SeamlessAddressBook.sol index b9e4233..73eb4e7 100644 --- a/helpers/SeamlessAddressBook.sol +++ b/helpers/SeamlessAddressBook.sol @@ -36,6 +36,7 @@ library SeamlessAddressBook { address constant SEAM = 0x1C7a460413dD4e964f96D8dFC56E7223cE88CD85; address constant USDC = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913; address constant ESSEAM = 0x998e44232BEF4F8B033e5A5175BDC97F2B10d5e5; + address constant BRETT = 0x532f27101965dd16442E59d40670FaF5eBB142E4; address constant CBETH = 0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22; address constant CBBTC = 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf; address constant EURC = 0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42; @@ -71,4 +72,13 @@ library SeamlessAddressBook { address constant SEAMLESS_WETH_MORPHO_VAULT_FEE_SPLITTER = 0xF070598338defd70068732290617c98CDb8adD30; address constant SEAM_GOVERNOR_V2_IMPLEMENTATION = 0xC3A36d72bE57866EC4751D709b5bEF67efA9bAef; + + address constant SEAM_TRANSFER_STRATEGY = + 0x2b1bdeFCe33f34128759f71076eBd62637FD154C; + address constant ESSEAM_TRANSFER_STRATEGY = + 0x2181be388ced00754E7c1Ee33DBcF78397DD89aC; + address constant USDC_TRANSFER_STRATEGY = + 0x003D47ddDdb070822B35ae5cc4F0066Cf9E89753; + address constant BRETT_TRANSFER_STRATEGY = + 0xD90EaC90f5f067283954b96BBc3d28E34ebE55Bb; } \ No newline at end of file diff --git a/proposals/sip_48/DeployProposal.s.sol b/proposals/sip_48/DeployProposal.s.sol new file mode 100644 index 0000000..37d5614 --- /dev/null +++ b/proposals/sip_48/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_48/Proposal.sol b/proposals/sip_48/Proposal.sol new file mode 100644 index 0000000..578edfb --- /dev/null +++ b/proposals/sip_48/Proposal.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { + SeamlessGovProposal, + SeamlessAddressBook +} from "../../helpers/SeamlessGovProposal.sol"; +import { ITransferStrategyBase } from + "@seamless-governance/interfaces/ITransferStrategyBase.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; + +contract Proposal is SeamlessGovProposal { + 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 { + uint256 seamTransferStrategySeamBalance = IERC20( + SeamlessAddressBook.SEAM + ).balanceOf(SeamlessAddressBook.SEAM_TRANSFER_STRATEGY); + + _addAction( + SeamlessAddressBook.SEAM_TRANSFER_STRATEGY, + abi.encodeWithSelector( + ITransferStrategyBase.emergencyWithdrawal.selector, + SeamlessAddressBook.SEAM, + SeamlessAddressBook.TIMELOCK_SHORT, + seamTransferStrategySeamBalance + ) + ); + + uint256 esSeamTransferStrategySeamBalance = IERC20( + SeamlessAddressBook.SEAM + ).balanceOf(SeamlessAddressBook.ESSEAM_TRANSFER_STRATEGY); + + _addAction( + SeamlessAddressBook.ESSEAM_TRANSFER_STRATEGY, + abi.encodeWithSelector( + ITransferStrategyBase.emergencyWithdrawal.selector, + SeamlessAddressBook.SEAM, + SeamlessAddressBook.TIMELOCK_SHORT, + esSeamTransferStrategySeamBalance + ) + ); + + uint256 usdcTransferStrategyUsdcBalance = IERC20( + SeamlessAddressBook.USDC + ).balanceOf(SeamlessAddressBook.USDC_TRANSFER_STRATEGY); + + _addAction( + SeamlessAddressBook.USDC_TRANSFER_STRATEGY, + abi.encodeWithSelector( + ITransferStrategyBase.emergencyWithdrawal.selector, + SeamlessAddressBook.USDC, + SeamlessAddressBook.TIMELOCK_SHORT, + usdcTransferStrategyUsdcBalance + ) + ); + + uint256 brettTransferStrategySeamBalance = IERC20( + SeamlessAddressBook.BRETT + ).balanceOf(SeamlessAddressBook.BRETT_TRANSFER_STRATEGY); + + _addAction( + SeamlessAddressBook.BRETT_TRANSFER_STRATEGY, + abi.encodeWithSelector( + ITransferStrategyBase.emergencyWithdrawal.selector, + SeamlessAddressBook.BRETT, + SeamlessAddressBook.TIMELOCK_SHORT, + brettTransferStrategySeamBalance + ) + ); + } +} diff --git a/proposals/sip_48/TenderlySimulation.s.sol b/proposals/sip_48/TenderlySimulation.s.sol new file mode 100644 index 0000000..39919d1 --- /dev/null +++ b/proposals/sip_48/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_48/TestProposal.t.sol b/proposals/sip_48/TestProposal.t.sol new file mode 100644 index 0000000..19af1c3 --- /dev/null +++ b/proposals/sip_48/TestProposal.t.sol @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import { GovTestHelper } from "../../helpers/GovTestHelper.sol"; +import { Proposal } from "./Proposal.sol"; +import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import { SeamlessAddressBook } from "../../helpers/SeamlessGovProposal.sol"; + +contract TestProposal is GovTestHelper { + Proposal public proposal; + + function setUp() public { + vm.rollFork(32430994); + proposal = new Proposal(); + } + + function test_seamAndEsseamClaimedAndTransferedToTimelock_afterPassingProposal( + ) public { + IERC20 seam = IERC20(SeamlessAddressBook.SEAM); + + uint256 seamTransferStrategySeamBalance = IERC20( + SeamlessAddressBook.SEAM + ).balanceOf(SeamlessAddressBook.SEAM_TRANSFER_STRATEGY); + + uint256 esseamTransferStrategyEsseamBalance = IERC20( + SeamlessAddressBook.SEAM + ).balanceOf(SeamlessAddressBook.ESSEAM_TRANSFER_STRATEGY); + + uint256 timelockBalanceBefore = + seam.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + _passProposalShortGov(proposal); + + uint256 timelockBalanceAfter = + seam.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + assertEq( + timelockBalanceAfter, + timelockBalanceBefore + seamTransferStrategySeamBalance + + esseamTransferStrategyEsseamBalance + ); + assertEq(seam.balanceOf(SeamlessAddressBook.SEAM_TRANSFER_STRATEGY), 0); + assertEq( + seam.balanceOf(SeamlessAddressBook.ESSEAM_TRANSFER_STRATEGY), 0 + ); + } + + function test_usdcClaimedAndTransferedToTimelock_afterPassingProposal() + public + { + IERC20 usdc = IERC20(SeamlessAddressBook.USDC); + + uint256 usdcTransferStrategyUsdcBalance = IERC20( + SeamlessAddressBook.USDC + ).balanceOf(SeamlessAddressBook.USDC_TRANSFER_STRATEGY); + + uint256 timelockBalanceBefore = + usdc.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + _passProposalShortGov(proposal); + + uint256 timelockBalanceAfter = + usdc.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + assertEq( + timelockBalanceAfter, + timelockBalanceBefore + usdcTransferStrategyUsdcBalance + ); + assertEq(usdc.balanceOf(SeamlessAddressBook.USDC_TRANSFER_STRATEGY), 0); + } + + function test_brettClaimedAndTransferedToTimelock_afterPassingProposal() + public + { + IERC20 brett = IERC20(SeamlessAddressBook.BRETT); + + uint256 brettTransferStrategyBrettBalance = IERC20( + SeamlessAddressBook.BRETT + ).balanceOf(SeamlessAddressBook.BRETT_TRANSFER_STRATEGY); + + uint256 timelockBalanceBefore = + brett.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + _passProposalShortGov(proposal); + + uint256 timelockBalanceAfter = + brett.balanceOf(SeamlessAddressBook.TIMELOCK_SHORT); + + assertEq( + timelockBalanceAfter, + timelockBalanceBefore + brettTransferStrategyBrettBalance + ); + assertEq( + brett.balanceOf(SeamlessAddressBook.BRETT_TRANSFER_STRATEGY), 0 + ); + } +} diff --git a/proposals/sip_48/description.md b/proposals/sip_48/description.md new file mode 100644 index 0000000..8dcb407 --- /dev/null +++ b/proposals/sip_48/description.md @@ -0,0 +1,33 @@ +# [SIP-48] Clawback any unclaimed and unemitted Legacy Platform and ILMv1 related rewards to the Seamless DAO treasury + +## Summary + +Following the successful adoption of [\[GP-9\] Maximizing Borrower Value: Why the Move to Morpho Matters](https://snapshot.box/#/s:seamlessprotocol.eth/proposal/0x901e2aed03877d64a0414496f69b7febb76272cf4a1526c6d3b8813dac455074), this proposal seeks DAO approval for the onchain return of unclaimed and unemitted rewards from the Legacy Platform and ILM v1 to the Seamless DAO treasury. By clawing back rewards, these funds will be available and reused for future governance-approved initiatives. + + +## Context and motivation + +With the approval of GP-9, the Seamless DAO officially entered its “platformless” era – transitioning away from its legacy Aave-based architecture and moving toward building Vaults on Morpho. This transition allows Seamless contributors to prioritize innovation and product development — most recently demonstrated by the launch of [Leverage Tokens](https://app.seamlessprotocol.com/#/?tab=LeverageTokens). + +A key aspect of GP-9 was the structured sunsetting of the Legacy Platform, executed in four clearly defined phases, culminating on June 30, 2025. + +![Sunset schedule](https://canada1.discourse-cdn.com/flex011/uploads/seamlessprotocol/original/1X/c21ee5f279d2e2e7e6b6a1fd772fbfe5b9436bd0.jpeg) + +As part of this wind-down, users were made aware of this transition and were provided with ~3 months to claim any rewards earned via Legacy Platform activity and ILM v1 participation. +Per GP-9, any rewards remaining unclaimed or unemitted past this date are to be returned to the Seamless DAO for future use: + +![Sunset phases table](https://canada1.discourse-cdn.com/flex011/uploads/seamlessprotocol/original/1X/919afeb0c0e8811d1a718671d462fdb839843735.png) + +*Reference Image pasted from GP-9 + +Returning these unclaimed and unemitted rewards requires an explicit onchain execution step, which this proposal seeks to facilitate. + +This proposal seeks DAO approval for the following action: +- Execute an onchain transaction that returns any unclaimed and unemitted rewards associated with the Legacy Platform and ILM v1 programs back to the Seamless DAO treasury. + +## Resources & References + +- [Snapshot vote](https://snapshot.box/#/s:seamlessprotocol.eth/proposal/0x901e2aed03877d64a0414496f69b7febb76272cf4a1526c6d3b8813dac455074) +- [Governance discussion](https://seamlessprotocol.discourse.group/t/gp-maximizing-borrower-value-why-the-move-to-morpho-matters/930) +- [Proposal Implementation](https://github.com/seamless-protocol/gov-proposals/tree/main/proposals/sip_48) +- Built using [Seamless Governance Proposals tools](https://github.com/seamless-protocol/gov-proposals) \ No newline at end of file