diff --git a/protocol-units/settlement/mcr/contracts/foundry.toml b/protocol-units/settlement/mcr/contracts/foundry.toml index 64fe5756d..de3f07a15 100644 --- a/protocol-units/settlement/mcr/contracts/foundry.toml +++ b/protocol-units/settlement/mcr/contracts/foundry.toml @@ -1,6 +1,4 @@ [profile.default] -via_ir = true -optimizer = true src = "src" out = "out" libs = ["lib"] @@ -8,6 +6,8 @@ ffi = true gas_limit = 9223372036854775807 # this is only needed for the multiround settlement test build_info = true extra_output = ["storageLayout"] +optimizer = true +optimizer_runs = 200 solc = "0.8.26" evm_version = "cancun" diff --git a/protocol-units/settlement/mcr/contracts/script/CoreDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/CoreDeployer.s.sol new file mode 100644 index 000000000..52a1e67f4 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/CoreDeployer.s.sol @@ -0,0 +1,65 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {MOVEToken} from "../src/token/MOVEToken.sol"; +import { Helper } from "./helpers/Helper.sol"; +import { MCRDeployer } from "./MCRDeployer.s.sol"; +import { MovementStakingDeployer } from "./MovementStakingDeployer.s.sol"; +import { StlMoveDeployer } from "./StlMoveDeployer.s.sol"; +import { MOVETokenDeployer } from "./MOVETokenDeployer.s.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; + +contract CoreDeployer is MCRDeployer, MovementStakingDeployer, StlMoveDeployer, MOVETokenDeployer { + + function run() external override(MCRDeployer, MovementStakingDeployer, StlMoveDeployer, MOVETokenDeployer) { + + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + // Deploy or upgrade contracts conditionally + deployment.moveAdmin == ZERO && deployment.move == ZERO ? + _deployMove() : deployment.moveAdmin != ZERO && deployment.move != ZERO ? + // if move is already deployed, upgrade it + _upgradeMove() : revert("MOVE: both admin and proxy should be registered"); + + // requires move to be deployed + deployment.stakingAdmin == ZERO && deployment.staking == ZERO && deployment.move != ZERO ? + _deployStaking() : deployment.stakingAdmin != ZERO && deployment.staking != ZERO ? + // if staking is already deployed, upgrade it + _upgradeStaking() : revert("STAKING: both admin and proxy should be registered"); + + // requires move to be deployed + deployment.stlMoveAdmin == ZERO && deployment.stlMove == ZERO && deployment.move != ZERO ? + _deployStlMove() : deployment.stlMoveAdmin != ZERO && deployment.stlMove != ZERO ? + // if stlMove is already deployed, upgrade it + _upgradeStlMove() : revert("STL: both admin and proxy should be registered"); + + // requires staking and move to be deployed + deployment.mcrAdmin == ZERO && deployment.mcr == ZERO && deployment.move != ZERO && deployment.staking != ZERO ? + _deployMCR() : deployment.mcrAdmin != ZERO && deployment.mcr != ZERO ? + // if mcr is already deployed, upgrade it + _upgradeMCR() : revert("MCR: both admin and proxy should be registered"); + + // Only write to file if chainid is not running a foundry local chain and if broadcasting + if (block.chainid == foundryChainId) { + _allowSameContract(); + _upgradeMove(); + _upgradeStaking(); + _upgradeStlMove(); + _upgradeMCR(); + } else { + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + vm.stopBroadcast(); + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/DeployMOVETokenDev.s.sol b/protocol-units/settlement/mcr/contracts/script/DeployMOVETokenDev.s.sol new file mode 100644 index 000000000..85a2911ee --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/DeployMOVETokenDev.s.sol @@ -0,0 +1,29 @@ +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import "../src/token/MOVETokenDev.sol"; +import {IMintableToken, MintableToken} from "../src/token/base/MintableToken.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {Helper} from "./helpers/Helper.sol"; + +contract DeployMOVETokenDev is Helper { + address public manager = 0x5A368EDEbF574162B84f8ECFE48e9De4f520E087; + uint256 public signer = vm.envUint("TEST_1"); + function run() external { + vm.startBroadcast(signer); + + MOVETokenDev moveTokenImplementation = new MOVETokenDev(); + TransparentUpgradeableProxy moveTokenProxy = new TransparentUpgradeableProxy( + address(moveTokenImplementation), + manager, + abi.encodeWithSignature("initialize(address)", manager) + ); + + console.log("Move Token Proxy: %s", address(moveTokenProxy)); + + vm.stopBroadcast(); + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/MCRDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/MCRDeployer.s.sol new file mode 100644 index 000000000..87d73dc11 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/MCRDeployer.s.sol @@ -0,0 +1,80 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {MCR} from "../src/settlement/MCR.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import { Helper } from "./helpers/Helper.sol"; + +contract MCRDeployer is Helper { + + function run() external virtual { + + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + deployment.mcrAdmin == ZERO && deployment.mcr == ZERO && deployment.move != ZERO && deployment.staking != ZERO ? + _deployMCR() : deployment.mcrAdmin != ZERO && deployment.mcr != ZERO ? + _upgradeMCR() : revert("MCR: both admin and proxy should be registered"); + + vm.stopBroadcast(); + + // Only write to file if chainid is not running a foundry local chain + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + // •☽────✧˖°˖DANGER ZONE˖°˖✧────☾• +// Modifications to the following functions have to be throughly tested + + function _deployMCR() internal { + console.log("MCR: deploying"); + MCR mcrImplementation = new MCR(); + vm.recordLogs(); + mcrProxy = new TransparentUpgradeableProxy( + address(mcrImplementation), + address(timelock), + abi.encodeWithSignature( + mcrSignature, + address(stakingProxy), + 128, + 100 ether, + 100 ether, + config.signersLabs + ) + ); + console.log("MCR deployment records:"); + console.log("proxy", address(mcrProxy)); + deployment.mcr = address(mcrProxy); + deployment.mcrAdmin = _storeAdminDeployment(); + } + + function _upgradeMCR() internal { + console.log("MCR: upgrading"); + MCR newMCRImplementation = new MCR(); + _checkBytecodeDifference(address(newMCRImplementation), deployment.mcr); + bytes memory data = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", + address(deployment.mcrAdmin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(mcrProxy), + address(newMCRImplementation), + "" + ), + bytes32(0), + bytes32(0), + config.minDelay + ); + _proposeUpgrade(data, "mcr.json"); + } + +} diff --git a/protocol-units/settlement/mcr/contracts/script/MOVETokenDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/MOVETokenDeployer.s.sol new file mode 100644 index 000000000..14cd23e9e --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/MOVETokenDeployer.s.sol @@ -0,0 +1,95 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {MOVEToken} from "../src/token/MOVEToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import { Helper, ProxyAdmin } from "./helpers/Helper.sol"; +import {ICREATE3Factory} from "./helpers/Create3/ICREATE3Factory.sol"; + +// Script intended to be used for deploying the MOVE token from an EOA +// Utilizies existing safes and sets them as proposers and executors. +// The MOVEToken contract takes in the Movement Foundation address and sets it as its own admin for future upgrades. +// The whole supply is minted to the Movement Foundation Safe. +// The script also verifies that the token has the correct balances, decimals and permissions. +contract MOVETokenDeployer is Helper { + // COMMANDS + // mainnet + // forge script MOVETokenDeployer --fork-url https://eth.llamarpc.com --verify --etherscan-api-key ETHERSCAN_API_KEY + // testnet + // forge script MOVETokenDeployer --fork-url https://eth-sepolia.api.onfinality.io/public + // Safes should be already deployed + bytes32 public salt = 0x0; + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + function run() external virtual { + + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + deployment.moveAdmin == ZERO && deployment.move == ZERO ? + _deployMove() : deployment.moveAdmin != ZERO && deployment.move != ZERO ? + // if move is already deployed, upgrade it + _upgradeMove() : revert("MOVE: both admin and proxy should be registered"); + + require(MOVEToken(deployment.move).balanceOf(address(deployment.movementAnchorage)) == 999999998000000000, "Movement Anchorage Safe balance is wrong"); + require(MOVEToken(deployment.move).decimals() == 8, "Decimals are expected to be 8"); + require(MOVEToken(deployment.move).totalSupply() == 1000000000000000000,"Total supply is wrong"); + require(MOVEToken(deployment.move).hasRole(DEFAULT_ADMIN_ROLE, address(deployment.movementFoundationSafe)),"Movement Foundation expected to have token admin role"); + require(!MOVEToken(deployment.move).hasRole(DEFAULT_ADMIN_ROLE, address(deployment.movementLabsSafe)),"Movement Labs not expected to have token admin role"); + require(!MOVEToken(deployment.move).hasRole(DEFAULT_ADMIN_ROLE, address(timelock)),"Timelock not expected to have token admin role"); + vm.stopBroadcast(); + + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + // •☽────✧˖°˖DANGER ZONE˖°˖✧────☾• +// Modifications to the following functions have to be throughly tested + + function _deployMove() internal { + console.log("MOVE: deploying"); + MOVEToken moveImplementation = new MOVEToken(); + // genetares bytecode for CREATE3 deployment + bytes memory bytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode(address(moveImplementation), address(timelock), abi.encodeWithSignature(moveSignature, deployment.movementFoundationSafe, deployment.movementAnchorage)) + ); + vm.recordLogs(); + // deploys the MOVE token proxy using CREATE3 + moveProxy = TransparentUpgradeableProxy(payable(ICREATE3Factory(create3).deploy(salt, bytecode))); + console.log("MOVEToken deployment records:"); + console.log("proxy", address(moveProxy)); + deployment.move = address(moveProxy); + deployment.moveAdmin = _storeAdminDeployment(); + } + + function _upgradeMove() internal { + console.log("MOVE: upgrading"); + MOVEToken newMoveImplementation = new MOVEToken(); + _checkBytecodeDifference(address(newMoveImplementation), deployment.move); + // Prepare the data for the upgrade + bytes memory data = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", + address(deployment.moveAdmin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(deployment.move), + address(newMoveImplementation), + "" + ), + bytes32(0), + bytes32(0), + config.minDelay + ); + + _proposeUpgrade(data, "movetoken.json"); + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/MovementStakingDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/MovementStakingDeployer.s.sol new file mode 100644 index 000000000..b487be236 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/MovementStakingDeployer.s.sol @@ -0,0 +1,76 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {MovementStaking} from "../src/staking/MovementStaking.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import { Helper } from "./helpers/Helper.sol"; + +contract MovementStakingDeployer is Helper { + + function run() external virtual { + + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + deployment.stakingAdmin == ZERO && deployment.staking == ZERO && deployment.move != ZERO ? + _deployStaking() : deployment.stakingAdmin != ZERO && deployment.staking != ZERO ? + _upgradeStaking() : revert("STAKING: both admin and proxy should be registered"); + + vm.stopBroadcast(); + + // Only write to file if chainid is not running a foundry local chain + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + // •☽────✧˖°˖DANGER ZONE˖°˖✧────☾• +// Modifications to the following functions have to be throughly tested + + function _deployStaking() internal { + console.log("STAKING: deploying"); + MovementStaking stakingImplementation = new MovementStaking(); + vm.recordLogs(); + stakingProxy = new TransparentUpgradeableProxy( + address(stakingImplementation), + address(timelock), + abi.encodeWithSignature(stakingSignature, address(moveProxy)) + ); + console.log("STAKING deployment records:"); + console.log("proxy", address(stakingProxy)); + deployment.staking = address(stakingProxy); + deployment.stakingAdmin = _storeAdminDeployment(); + } + + function _upgradeStaking() internal { + console.log("STAKING: upgrading"); + MovementStaking newStakingImplementation = new MovementStaking(); + _checkBytecodeDifference(address(newStakingImplementation), deployment.staking); + // Prepare the data for the upgrade + bytes memory data = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", + address(deployment.stakingAdmin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(stakingProxy), + address(newStakingImplementation), + "" + ), + bytes32(0), + bytes32(0), + config.minDelay + ); + + _proposeUpgrade(data, "staking.json"); +} + + +} diff --git a/protocol-units/settlement/mcr/contracts/script/MultisigMOVETokenDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/MultisigMOVETokenDeployer.s.sol new file mode 100644 index 000000000..2261e22ff --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/MultisigMOVETokenDeployer.s.sol @@ -0,0 +1,158 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {MOVEToken} from "../src/token/MOVEToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Helper, Safe} from "./helpers/Helper.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {ICREATE3Factory} from "./helpers/Create3/ICREATE3Factory.sol"; +import {Enum} from "@safe-smart-account/contracts/common/Enum.sol"; +import {stdJson} from "forge-std/StdJson.sol"; + +// Script intended to be used for deploying the MOVE token from an EOA +// Utilizies existing safes and sets them as proposers and executors. +// The MOVEToken contract takes in the Movement Foundation address and sets it as its own admin for future upgrades. +// The whole supply is minted to the Movement Foundation Safe. +// The script also verifies that the token has the correct balances, decimals and permissions. +contract MultisigMOVETokenDeployer is Helper { + using stdJson for string; + // COMMANDS + // mainnet + // forge script MultisigMOVETokenDeployer --fork-url https://eth.llamarpc.com --verify --etherscan-api-key ETHERSCAN_API_KEY + // testnet + // forge script MultisigMOVETokenDeployer --fork-url https://eth-sepolia.api.onfinality.io/public + // Safes should be already deployed + + bytes32 public salt = 0x6c0000000000000000000000018eddf77afc0a5c6d05a564a44fe37b068922c3; + bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00; + + function run() external virtual { + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + // This deployer solely deploys a timelock and an implementation, it leaves to multisig to execute the deployment + // of the actual token. + _proposeMultisigMove(); + + vm.stopBroadcast(); + + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + // •☽────✧˖°˖DANGER ZONE˖°˖✧────☾• + // Modifications to the following functions have to be throughly tested + + function _proposeMultisigMove() internal { + console.log("MOVE: deploying"); + MOVEToken moveImplementation = new MOVEToken(); + // genetares bytecode for CREATE3 deployment + bytes memory create3Bytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode( + address(moveImplementation), + address(timelock), + abi.encodeWithSignature(moveSignature, deployment.movementFoundationSafe, deployment.movementAnchorage) + ) + ); + + deployment.move = create3.getDeployed(deployment.movementDeployerSafe, salt); + console.log("MOVE: deployment address", deployment.move); + + // check if the deployment address starts with 0x3073 so we can be sure CREATE3 deployed successfully + // this is a safety check to prevent deploying to an incorrect address + // starting and ending with 3073 is a deterministic address that can be reproduced on other networks and brands the token address + // users have an extra layer of security by easily identifying the address + require(_startsWith3073(deployment.move), "MOVE: deployment address does not start with 0x3073"); + + // create bytecode the MOVE token proxy using CREATE3 + bytes memory bytecode = abi.encodeWithSignature("deploy(bytes32,bytes)", salt, create3Bytecode); + + // NOTE: digest can be used if immediately signing and executing the transaction + // bytes32 digest = Safe(payable(deployment.movementFoundationSafe)).getTransactionHash( + // address(create3), 0, bytecode, Enum.Operation.Call, 0, 0, 0, ZERO, payable(ZERO), 0 + // ); + + string memory json = "safeCall"; + // Serialize the relevant fields into JSON format + json.serialize("to", address(create3)); + string memory zero = "0"; + json.serialize("value", zero); + json.serialize("data", bytecode); + string memory operation = "OperationType.Call"; + json.serialize("chainId", chainId); + json.serialize("safeAddress", deployment.movementDeployerSafe); + string memory serializedData = json.serialize("operation", operation); + // Log the serialized JSON for debugging + console.log("json |start|", serializedData, "|end|"); + // Write the serialized data to a file + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + vm.writeFile(string.concat(root, upgradePath, "deploymove.json"), serializedData); + } + } + + function _deployMultisigMove() internal { + console.log("MOVE: deploying"); + MOVEToken moveImplementation = new MOVEToken(); + // genetares bytecode for CREATE3 deployment + bytes memory create3Bytecode = abi.encodePacked( + type(TransparentUpgradeableProxy).creationCode, + abi.encode( + address(moveImplementation), + address(timelock), + abi.encodeWithSignature(moveSignature, deployment.movementFoundationSafe, deployment.movementAnchorage) + ) + ); + vm.recordLogs(); + // craete bytecode the MOVE token proxy using CREATE3 + bytes memory bytecode = abi.encodeWithSignature("deploy(bytes32,bytes)", salt, create3Bytecode); + bytes32 digest = Safe(payable(deployment.movementDeployerSafe)).getTransactionHash( + address(create3), 0, bytecode, Enum.Operation.Call, 0, 0, 0, ZERO, payable(ZERO), 0 + ); + + // three signers for the deployment (this is mocked and only works in foundry chain) + uint256[] memory signers = new uint256[](3); + signers[0] = vm.envUint("PRIVATE_KEY"); + signers[1] = 1; + signers[2] = 2; + + bytes memory signatures = _generateSignatures(signers, digest); + + Safe(payable(deployment.movementFoundationSafe)).execTransaction( + address(create3), 0, bytecode, Enum.Operation.Call, 0, 0, 0, ZERO, payable(ZERO), signatures + ); + // moveProxy = + console.log("MOVEToken deployment records:"); + Vm.Log[] memory logs = vm.getRecordedLogs(); + deployment.move = logs[0].emitter; + deployment.moveAdmin = logs[logs.length - 3].emitter; + console.log("proxy", deployment.move); + console.log("admin", deployment.moveAdmin); + } + + // MULTISIG WILL NEVER BE USED WITHIN THE CONTRACT PIPELINE + function _upgradeMultisigMove() internal { + console.log("MOVE: upgrading"); + MOVEToken newMoveImplementation = new MOVEToken(); + timelock.schedule( + deployment.moveAdmin, + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + deployment.move, + address(newMoveImplementation), + abi.encodeWithSignature("initialize(address)", deployment.movementFoundationSafe) + ), + bytes32(0), + bytes32(0), + config.minDelay + ); + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/StlMoveDeployer.s.sol b/protocol-units/settlement/mcr/contracts/script/StlMoveDeployer.s.sol new file mode 100644 index 000000000..2c0379510 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/StlMoveDeployer.s.sol @@ -0,0 +1,74 @@ +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import {stlMoveToken} from "../src/token/stlMoveToken.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import { Helper } from "./helpers/Helper.sol"; + +contract StlMoveDeployer is Helper { + + function run() external virtual { + + // load config and deployments data + _loadExternalData(); + + uint256 signer = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(signer); + + // Deploy CREATE3Factory, Safes and Timelock if not deployed + _deployDependencies(); + + deployment.stlMoveAdmin == ZERO && deployment.stlMove == ZERO && deployment.move != ZERO ? + _deployStlMove() : deployment.stlMoveAdmin != ZERO && deployment.stlMove != ZERO ? + _upgradeStlMove() : revert("STL: both admin and proxy should be registered"); + + vm.stopBroadcast(); + + // Only write to file if chainid is not running a foundry local chain + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + _writeDeployments(); + } + } + + // •☽────✧˖°˖DANGER ZONE˖°˖✧────☾• +// Modifications to the following functions have to be throughly tested + + function _deployStlMove() internal { + console.log("STL: deploying"); + stlMoveToken stlMoveImplementation = new stlMoveToken(); + vm.recordLogs(); + stlMoveProxy = new TransparentUpgradeableProxy( + address(stlMoveImplementation), + address(timelock), + abi.encodeWithSignature(stlMoveSignature, "STL Move Token", "STL", address(moveProxy)) + ); + console.log("STL deployment records:"); + console.log("proxy", address(stlMoveProxy)); + deployment.stlMove = address(stlMoveProxy); + deployment.stlMoveAdmin = _storeAdminDeployment(); + } + + function _upgradeStlMove() internal { + console.log("STL: upgrading"); + stlMoveToken newStlMoveImplementation = new stlMoveToken(); + _checkBytecodeDifference(address(newStlMoveImplementation), deployment.stlMove); + // Prepare the data for the upgrade + bytes memory data = abi.encodeWithSignature( + "schedule(address,uint256,bytes,bytes32,bytes32,uint256)", + address(deployment.stlMoveAdmin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(stlMoveProxy), + address(newStlMoveImplementation), + "" + ), + bytes32(0), + bytes32(0), + config.minDelay + ); + + _proposeUpgrade(data, "stlmove.json"); + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/helpers/Helper.sol b/protocol-units/settlement/mcr/contracts/script/helpers/Helper.sol new file mode 100644 index 000000000..32ad2cfea --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/script/helpers/Helper.sol @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {stdJson} from "forge-std/StdJson.sol"; +import { + TransparentUpgradeableProxy, + ERC1967Utils +} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {SafeProxyFactory} from "@safe-smart-account/contracts/proxies/SafeProxyFactory.sol"; +import {CompatibilityFallbackHandler} from "@safe-smart-account/contracts/handler/CompatibilityFallbackHandler.sol"; +import {SafeProxy} from "@safe-smart-account/contracts/proxies/SafeProxy.sol"; +import {Safe} from "@safe-smart-account/contracts/Safe.sol"; +import {Vm} from "forge-std/Vm.sol"; +import {CREATE3Factory} from "./Create3/CREATE3Factory.sol"; + +contract Helper is Script { + using stdJson for string; + + TransparentUpgradeableProxy public moveProxy; + TransparentUpgradeableProxy public stlMoveProxy; + TransparentUpgradeableProxy public stakingProxy; + TransparentUpgradeableProxy public mcrProxy; + TimelockController public timelock; + // CREATE3 exists across all major chains, we only enforce it on the same address if not deployed yet + CREATE3Factory public create3 = CREATE3Factory(0x2Dfcc7415D89af828cbef005F0d072D8b3F23183); + string public mcrSignature = "initialize(address,uint256,uint256,uint256,address[])"; + string public stakingSignature = "initialize(address)"; + string public stlMoveSignature = "initialize(string,string,address)"; + string public moveSignature = "initialize(address,address)"; + string public safeSetupSignature = "setup(address[],uint256,address,bytes,address,address,uint256,address)"; + string public root = vm.projectRoot(); + string public deploymentsPath = "/script/helpers/deployments.json"; + string public upgradePath = "/script/helpers/upgrade/"; + string public configPath = "/script/helpers/config.json"; + address public ZERO = 0x0000000000000000000000000000000000000000; + string public chainId = _uint2str(block.chainid); + uint256 public foundryChainId = 31337; + string public storageJson; + bool public allowsSameContract; + + ConfigData public config; + + struct ConfigData { + uint256 minDelay; + address[] signersDeployer; + address[] signersFoundation; + address[] signersLabs; + uint256 thresholdDeployer; + uint256 thresholdFoundation; + uint256 thresholdLabs; + } + + Deployment public deployment; + + struct Deployment { + address mcr; + address mcrAdmin; + address move; + address moveAdmin; + address movementAnchorage; + address movementDeployerSafe; + address movementFoundationSafe; + address movementLabsSafe; + address staking; + address stakingAdmin; + address stlMove; + address stlMoveAdmin; + address timelock; + } + + function _loadConfig() internal { + string memory path = string.concat(root, configPath); + string memory json = vm.readFile(path); + bytes memory rawConfigData = json.parseRaw(string(abi.encodePacked("."))); + config = abi.decode(rawConfigData, (ConfigData)); + + if (config.signersLabs[0] == ZERO) { + config.signersLabs[0] = vm.addr(vm.envUint("PRIVATE_KEY")); + // populate multisigs with signers + for (uint256 i = 1; i < config.signersLabs.length; i++) { + if (config.signersLabs[i] == ZERO) { + config.signersLabs[i] = vm.addr(i); + } + } + } + if (config.signersFoundation[0] == ZERO) { + config.signersFoundation[0] = vm.addr(vm.envUint("PRIVATE_KEY")); + // populate multisigs with signers + for (uint256 i = 1; i < config.signersFoundation.length; i++) { + if (config.signersFoundation[i] == ZERO) { + config.signersFoundation[i] = vm.addr(i); + } + } + } + } + + function _loadDeployments() internal { + // load deployments + // Inspo https://github.com/traderjoe-xyz/joe-v2/blob/main/script/deploy-core.s.sol + string memory path = string.concat(root, deploymentsPath); + string memory json = vm.readFile(path); + bytes memory rawDeploymentData = json.parseRaw(string(abi.encodePacked(".", chainId))); + deployment = abi.decode(rawDeploymentData, (Deployment)); + storageJson = json; + } + + function _loadExternalData() internal { + _loadConfig(); + _loadDeployments(); + } + + function _deploySafes() internal { + console.log("Deploying Safes"); + if (deployment.movementLabsSafe == ZERO && block.chainid != foundryChainId) { + // use canonical v1.4.1 safe factory address 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67 if: + // - chainid is not foundry + // - safe is not deployed + SafeProxyFactory safeFactory = SafeProxyFactory(0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67); + deployment.movementDeployerSafe = _deploySafe( + safeFactory, + 0x41675C099F32341bf84BFc5382aF534df5C7461a, + 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99, + config.signersDeployer, + config.thresholdDeployer + ); + deployment.movementLabsSafe = _deploySafe( + safeFactory, + 0x41675C099F32341bf84BFc5382aF534df5C7461a, + 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99, + config.signersLabs, + config.thresholdLabs + ); + deployment.movementFoundationSafe = _deploySafe( + safeFactory, + 0x41675C099F32341bf84BFc5382aF534df5C7461a, + 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99, + config.signersFoundation, + config.thresholdFoundation + ); + } else { + if (block.chainid == foundryChainId) { + SafeProxyFactory safeFactory = new SafeProxyFactory(); + Safe safeSingleton = new Safe(); + CompatibilityFallbackHandler fallbackHandler = new CompatibilityFallbackHandler(); + deployment.movementDeployerSafe = _deploySafe( + safeFactory, + address(safeSingleton), + address(fallbackHandler), + config.signersDeployer, + config.thresholdDeployer + ); + deployment.movementLabsSafe = _deploySafe( + safeFactory, + address(safeSingleton), + address(fallbackHandler), + config.signersLabs, + config.thresholdLabs + ); + deployment.movementFoundationSafe = _deploySafe( + safeFactory, + address(safeSingleton), + address(fallbackHandler), + config.signersFoundation, + config.thresholdFoundation + ); + // repeats foundation signers + deployment.movementAnchorage = _deploySafe( + safeFactory, + address(safeSingleton), + address(fallbackHandler), + config.signersFoundation, + config.thresholdLabs + ); + } + } + console.log("Safe addresses:"); + console.log("Deployer:", address(deployment.movementDeployerSafe)); + console.log("Labs:", address(deployment.movementLabsSafe)); + console.log("Foundation:", address(deployment.movementFoundationSafe)); + } + + function _deploySafe( + SafeProxyFactory safeFactory, + address safeSingleton, + address fallbackHandler, + address[] memory signers, + uint256 threshold + ) internal returns (address safe) { + safe = payable( + address( + safeFactory.createProxyWithNonce( + safeSingleton, + abi.encodeWithSignature( + safeSetupSignature, signers, threshold, ZERO, "0x", fallbackHandler, ZERO, 0, payable(ZERO) + ), + 0 + ) + ) + ); + } + + function _deployTimelock() internal { + if (deployment.timelock == ZERO) { + timelock = new TimelockController(config.minDelay, _arrayfy(deployment.movementLabsSafe), _arrayfy(deployment.movementFoundationSafe), ZERO); + deployment.timelock = address(timelock); + } + } + + function _arrayfy(address addr) internal pure returns (address[] memory arr) { + arr = new address[](1); + arr[0] = addr; + } + + function _deployCreate3() internal { + if (address(create3).code.length == 0) { + console.log("CREATE3: deploying"); + create3 = new CREATE3Factory(); + } + } + + function _deployDependencies() internal { + _deployCreate3(); + _deploySafes(); + _deployTimelock(); + } + + function _storeAdminDeployment() internal returns (address admin) { + Vm.Log[] memory logs = vm.getRecordedLogs(); + admin = logs[logs.length - 2].emitter; + console.log("admin", admin); + } + + function _writeDeployments() internal { + string memory path = string.concat(root, deploymentsPath); + string memory json = storageJson; + string memory base = "new"; + string memory newChainData = _serializer(json, deployment); + // take values from storageJson that were not updated (e.g. 3771) and serialize them + // since transaction reverts if writeDeployments does not contain all chain data, + // we need to serialize chain data for all valid chains besides the current one + uint256[] memory validChains = new uint256[](4); + validChains[0] = 1; // ethereum + validChains[1] = 11155111; // sepolia + validChains[2] = 17000; // holesky + validChains[3] = 31337; // foundry + for (uint256 i = 0; i < validChains.length; i++) { + if (validChains[i] != block.chainid) { + _serializeChainData(base, storageJson, validChains[i]); + } + } + // new chain data + string memory data = base.serialize(chainId, newChainData); + vm.writeFile(path, data); + } + + function _serializeChainData(string memory base, string storage sJson, uint256 chain) internal { + bytes memory rawDeploymentData = sJson.parseRaw(string(abi.encodePacked(".", _uint2str(chain)))); + Deployment memory deploymentData = abi.decode(rawDeploymentData, (Deployment)); + string memory json = _uint2str(chain); + string memory chainData = _serializer(json, deploymentData); + base.serialize(_uint2str(chain), chainData); + } + + function _serializer(string memory json, Deployment memory memoryDeployment) internal returns (string memory) { + json.serialize("mcr", memoryDeployment.mcr); + json.serialize("mcrAdmin", memoryDeployment.mcrAdmin); + json.serialize("move", memoryDeployment.move); + json.serialize("moveAdmin", memoryDeployment.moveAdmin); + json.serialize("movementAnchorage", memoryDeployment.movementAnchorage); + json.serialize("movementDeployerSafe", memoryDeployment.movementDeployerSafe); + json.serialize("movementFoundationSafe", memoryDeployment.movementFoundationSafe); + json.serialize("movementLabsSafe", memoryDeployment.movementLabsSafe); + json.serialize("staking", memoryDeployment.staking); + json.serialize("stakingAdmin", memoryDeployment.stakingAdmin); + json.serialize("stlMove", memoryDeployment.stlMove); + json.serialize("stlMoveAdmin", memoryDeployment.stlMoveAdmin); + return json.serialize("timelock", memoryDeployment.timelock); + } + + function _proposeUpgrade(bytes memory data, string memory fileName) internal { + string memory json = "safeCall"; + // Serialize the relevant fields into JSON format + json.serialize("to", address(timelock)); + string memory zero = "0"; + json.serialize("value", zero); + json.serialize("data", data); + string memory operation = "OperationType.Call"; + json.serialize("chainId", chainId); + json.serialize("safeAddress", deployment.movementLabsSafe); + string memory serializedData = json.serialize("operation", operation); + // Log the serialized JSON for debugging + console.log("json |start|", serializedData, "|end|"); + // Write the serialized data to a file + if (vm.isContext(VmSafe.ForgeContext.ScriptBroadcast)) { + vm.writeFile(string.concat(root, upgradePath, fileName), serializedData); + } + } + + // string to address + function s2a(bytes memory str) public returns (address addr) { + bytes32 data = keccak256(str); + assembly { + addr := data + } + } + + function _generateSignatures(uint256[] memory privKeys, bytes32 digest) + internal + returns (bytes memory signatures) + { + require(vm.addr(privKeys[0]) == vm.addr(vm.envUint("PRIVATE_KEY")), "First signer must be the sender"); + _sortByAddress(privKeys); + for (uint256 i = 0; i < privKeys.length; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privKeys[i], digest); + signatures = abi.encodePacked(signatures, r, s, v); + } + } + + function _sortByAddress(uint256[] memory privKeys) internal { + for (uint256 i = 0; i < privKeys.length - 1; i++) { + for (uint256 j = 0; j < privKeys.length - i - 1; j++) { + if (vm.addr(privKeys[j]) > vm.addr(privKeys[j + 1])) { + (privKeys[j], privKeys[j + 1]) = (privKeys[j + 1], privKeys[j]); + } + } + } + } + + function _uint2str(uint256 _i) internal pure returns (string memory _uintAsString) { + if (_i == 0) { + return "0"; + } + uint256 j = _i; + uint256 len; + while (j != 0) { + len++; + j /= 10; + } + bytes memory bstr = new bytes(len); + uint256 k = len; + while (_i != 0) { + k = k - 1; + uint8 temp = (48 + uint8(_i - _i / 10 * 10)); + bytes1 b1 = bytes1(temp); + bstr[k] = b1; + _i /= 10; + } + return string(bstr); + } + + function _startsWith3073(address addr) internal pure returns (bool) { + bytes20 addrBytes = bytes20(addr); + return (uint16(uint8(addrBytes[0])) << 8 | uint8(addrBytes[1])) == 0x3073; + } + + function _getBytecode(address _addr) internal view returns (bytes memory code) { + assembly { + let size := extcodesize(_addr) + code := mload(0x40) + mstore(0x40, add(code, add(size, 0x20))) + mstore(code, size) + extcodecopy(_addr, add(code, 0x20), 0, size) + } + } + + function _getImplementation(address proxy) internal view returns (address implementation) { + bytes32 IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + implementation = address(uint160(uint256(vm.load(proxy, IMPLEMENTATION_SLOT)))); + } + + function _checkBytecodeDifference(address newImplementation, address proxy) internal { + if (allowsSameContract) { + return; + } + address currentImplementation = _getImplementation(proxy); + bytes memory newCode = _getBytecode(newImplementation); + bytes memory currentCode = _getBytecode(currentImplementation); + require(keccak256(newCode) != keccak256(currentCode), "Helper: New implementation is the same as the current one"); + } + + function _allowSameContract() internal { + allowsSameContract = true; + } +} diff --git a/protocol-units/settlement/mcr/contracts/script/helpers/config.json b/protocol-units/settlement/mcr/contracts/script/helpers/config.json index 7e8a2d73e..ad90a55a6 100644 --- a/protocol-units/settlement/mcr/contracts/script/helpers/config.json +++ b/protocol-units/settlement/mcr/contracts/script/helpers/config.json @@ -1,5 +1,5 @@ { - "minDelay": "172800", + "minDelay": 172800, "signersDeployer": [ "0xB2105464215716e1445367BEA5668F581eF7d063", "0x3eB69Ef2DbEDD5d58AA5E074131Cd22D5e87Ff53" diff --git a/protocol-units/settlement/mcr/contracts/script/helpers/deployments.json b/protocol-units/settlement/mcr/contracts/script/helpers/deployments.json index 78a076cd3..155447bf5 100644 --- a/protocol-units/settlement/mcr/contracts/script/helpers/deployments.json +++ b/protocol-units/settlement/mcr/contracts/script/helpers/deployments.json @@ -6,13 +6,13 @@ "moveAdmin": "0x8365AA031806A1ac2b31a5d3b8323020FC85DfEc", "movementAnchorage": "0xe3e86E126fcCd071Af39a0899734Ca5C8E5F4F25", "movementDeployerSafe": "0x7aE744e3b2816F660054EAbd1a1C4935DA34Ae28", - "movementFoundationSafe": "0x074C155f09cE5fC3B65b4a9Bbb01739459C7AD63", + "movementFoundationSafe": "0xB304C899EcB46DD91F31Ef0d177fF9dAf8C17edf", "movementLabsSafe": "0xd7E22951DE7aF453aAc5400d6E072E3b63BeB7E2", "staking": "0x0000000000000000000000000000000000000000", "stakingAdmin": "0x0000000000000000000000000000000000000000", "stlMove": "0x0000000000000000000000000000000000000000", "stlMoveAdmin": "0x0000000000000000000000000000000000000000", - "timelock": "0xA649f6335828f070dDDd7A8c4F5bef2b6FF7Bd51" + "timelock": "0x25a5A3FA61cba5Fd5fb1D75D0AcfEB81370778Eb" }, "11155111": { "mcr": "0x0000000000000000000000000000000000000000", diff --git a/protocol-units/settlement/mcr/contracts/src/token/MOVETokenDev.sol b/protocol-units/settlement/mcr/contracts/src/token/MOVETokenDev.sol new file mode 100644 index 000000000..f5c204132 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/src/token/MOVETokenDev.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./base/MintableToken.sol"; + +contract MOVETokenDev is MintableToken { + + /** + * @dev Initialize the contract + */ + function initialize(address manager) public initializer { + __MintableToken_init("Movement", "MOVE"); + _mint(manager, 10000000000 * 10 ** decimals()); + _grantRole(MINTER_ADMIN_ROLE, manager); + _grantRole(MINTER_ROLE, manager); + } + + function grantRoles(address account) public onlyRole(DEFAULT_ADMIN_ROLE) { + _grantRole(MINTER_ADMIN_ROLE, account); + _grantRole(MINTER_ROLE, account); + + } + + function decimals() public pure override returns (uint8) { + return 8; + } +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/src/token/stlMoveToken.sol b/protocol-units/settlement/mcr/contracts/src/token/stlMoveToken.sol new file mode 100644 index 000000000..0a819c5e9 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/src/token/stlMoveToken.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {LockedToken} from "./locked/LockedToken.sol"; +import {CustodianToken} from "./custodian/CustodianToken.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol"; +import {IMintableToken} from "./base/MintableToken.sol"; +import {ERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; + +contract stlMoveToken is LockedToken, CustodianToken { + using SafeERC20 for IERC20; + + /** + * @dev Initialize the contract + * @param _underlyingToken The underlying token to wrap + */ + function initialize(IMintableToken _underlyingToken) public { + initialize("Stakable Locked Move Token", "stlMOVE", _underlyingToken); + } + + function initialize(string memory name, string memory symbol, IMintableToken _underlyingToken) + public + override(CustodianToken, LockedToken) + initializer + { + __ERC20_init_unchained(name, symbol); + __BaseToken_init_unchained(); + __MintableToken_init_unchained(); + __WrappedToken_init_unchained(_underlyingToken); + __LockedToken_init_unchained(); + __CustodianToken_init_unchained(); + } + + function transfer(address to, uint256 amount) + public + override(CustodianToken, ERC20Upgradeable, IERC20) + returns (bool) + { + return CustodianToken.transfer(to, amount); + } + + function transferFrom(address from, address to, uint256 amount) + public + override(CustodianToken, ERC20Upgradeable, IERC20) + returns (bool) + { + return CustodianToken.transferFrom(from, to, amount); + } + + function approve(address spender, uint256 amount) + public + override(CustodianToken, ERC20Upgradeable, IERC20) + returns (bool) + { + return CustodianToken.approve(spender, amount); + } +} + +// Flow for staking +// StakingContract: signer call stake +// StakingContract: signer approves StakingContract to spend their stlkMOVE tokens. +// StakingContract: calls transferFrom on stlkMOVE to move both stlkMOVE and MOVE tokens to the staking contract +// StakingContract: staking contract confirms it received the tokens and records balance for the signer with the custodian + +// Flow for unstaking +// StakingContract: signer calls unstake with the custodian +// StakingContract: staking contract transfers stlkMOVE and MOVE tokens back to the custodian via calling transfer on the stlkMOVE contract +// StakingContract: staking contract confirms it transferred the tokens back to the custodian and updates the signer's balance to 0 diff --git a/protocol-units/settlement/mcr/contracts/test/Deployer.t.sol b/protocol-units/settlement/mcr/contracts/test/Deployer.t.sol new file mode 100644 index 000000000..f66c39aa1 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/Deployer.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../src/token/MOVEToken.sol"; + +contract DeployerTest is Test { + + function setUp() public { + // Set the sender address + } +} diff --git a/protocol-units/settlement/mcr/contracts/test/settlement/MCR.sol b/protocol-units/settlement/mcr/contracts/test/settlement/MCR.sol new file mode 100644 index 000000000..7a9742c6e --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/settlement/MCR.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../src/staking/MovementStaking.sol"; +import "../../src/token/MOVETokenDev.sol"; +import "../../src/settlement/MCR.sol"; +import "../../src/settlement/MCRStorage.sol"; +import "../../src/settlement/interfaces/IMCR.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; + +contract MCRTest is Test, IMCR { + MOVETokenDev public moveToken; + MovementStaking public staking; + MCR public mcr; + ProxyAdmin public admin; + string public moveSignature = "initialize(string,string)"; + string public stakingSignature = "initialize(address)"; + string public mcrSignature = "initialize(address,uint256,uint256,uint256,address[])"; + + function setUp() public { + MOVETokenDev moveTokenImplementation = new MOVETokenDev(); + MovementStaking stakingImplementation = new MovementStaking(); + MCR mcrImplementation = new MCR(); + + // Contract MCRTest is the admin + admin = new ProxyAdmin(address(this)); + + // Deploy proxies + TransparentUpgradeableProxy moveProxy = new TransparentUpgradeableProxy( + address(moveTokenImplementation), + address(admin), + abi.encodeWithSignature(moveSignature, "Move Token", "MOVE") + ); + TransparentUpgradeableProxy stakingProxy = new TransparentUpgradeableProxy( + address(stakingImplementation), + address(admin), + abi.encodeWithSignature(stakingSignature, IMintableToken(address(moveProxy))) + ); + address[] memory custodians = new address[](1); + custodians[0] = address(moveProxy); + TransparentUpgradeableProxy mcrProxy = new TransparentUpgradeableProxy( + address(mcrImplementation), + address(admin), + abi.encodeWithSignature(mcrSignature, stakingProxy, 0, 5, 10 seconds, custodians) + ); + moveToken = MOVETokenDev(address(moveProxy)); + staking = MovementStaking(address(stakingProxy)); + mcr = MCR(address(mcrProxy)); + } + + function testCannotInitializeTwice() public { + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + mcr.initialize(staking, 0, 5, 10 seconds, custodians); + } + + // function testSimpleStaking() public { + // // three well-funded signers + // address payable alice = payable(vm.addr(1)); + // staking.whitelistAddress(alice); + // moveToken.mint(alice, 100); + // address payable bob = payable(vm.addr(2)); + // staking.whitelistAddress(bob); + // moveToken.mint(bob, 100); + // address payable carol = payable(vm.addr(3)); + // moveToken.mint(carol, 100); + // staking.whitelistAddress(carol); + + // // have them participate in the genesis ceremony + // vm.prank(alice); + // moveToken.approve(address(staking), 100); + // vm.prank(alice); + // staking.stake(address(mcr), moveToken, 34); + // vm.prank(bob); + // moveToken.approve(address(staking), 100); + // vm.prank(bob); + // staking.stake(address(mcr), moveToken, 33); + // vm.prank(carol); + // moveToken.approve(address(staking), 100); + // vm.prank(carol); + // staking.stake(address(mcr), moveToken, 33); + + // // end the genesis ceremony + // mcr.acceptGenesisCeremony(); + + // // make a block commitment + // MCRStorage.BlockCommitment memory bc1 = MCRStorage.BlockCommitment({ + // height: 1, + // commitment: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))), + // blockId: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))) + // }); + // vm.prank(alice); + // mcr.submitBlockCommitment(bc1); + // vm.prank(bob); + // mcr.submitBlockCommitment(bc1); + + // // now we move to block 2 and make some commitment just to trigger the epochRollover + // (uint256 height, bytes32 commitment, bytes32 blockId) = mcr.acceptedBlocks(1); + // assert(commitment == bc1.commitment); + // assert(blockId == bc1.blockId); + // assert(height == 1); + // } + + // function testDishonestValidator() public { + // // three well-funded signers + // address payable alice = payable(vm.addr(1)); + // staking.whitelistAddress(alice); + // moveToken.mint(alice, 100); + // address payable bob = payable(vm.addr(2)); + // moveToken.mint(bob, 100); + // staking.whitelistAddress(bob); + // address payable carol = payable(vm.addr(3)); + // moveToken.mint(carol, 100); + // staking.whitelistAddress(carol); + + // // have them participate in the genesis ceremony + // vm.prank(alice); + // moveToken.approve(address(staking), 100); + // vm.prank(alice); + // staking.stake(address(mcr), moveToken, 34); + // vm.prank(bob); + // moveToken.approve(address(staking), 100); + // vm.prank(bob); + // staking.stake(address(mcr), moveToken, 33); + // vm.prank(carol); + // moveToken.approve(address(staking), 100); + // vm.prank(carol); + // staking.stake(address(mcr), moveToken, 33); + + // // end the genesis ceremony + // mcr.acceptGenesisCeremony(); + + // // carol will be dishonest + // MCRStorage.BlockCommitment memory dishonestCommitment = MCRStorage.BlockCommitment({ + // height: 1, + // commitment: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))), + // blockId: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))) + // }); + // vm.prank(carol); + // mcr.submitBlockCommitment(dishonestCommitment); + + // // carol will try to sign again + // vm.prank(carol); + // vm.expectRevert(AttesterAlreadyCommitted.selector); + // mcr.submitBlockCommitment(dishonestCommitment); + + // // make a block commitment + // MCRStorage.BlockCommitment memory bc1 = MCRStorage.BlockCommitment({ + // height: 1, + // commitment: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))), + // blockId: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))) + // }); + // vm.prank(alice); + // mcr.submitBlockCommitment(bc1); + // vm.prank(bob); + // mcr.submitBlockCommitment(bc1); + + // (uint256 height, bytes32 commitment, bytes32 blockId) = mcr.acceptedBlocks(1); + // // now we move to block 2 and make some commitment just to trigger the epochRollover + // assert(commitment == bc1.commitment); + // assert(blockId == bc1.blockId); + // assert(height == 1); + // } + + // function testRollsOverHandlingDishonesty() public { + // vm.warp(300 seconds); + + // // three well-funded signers + // address payable alice = payable(vm.addr(1)); + // staking.whitelistAddress(alice); + // moveToken.mint(alice, 100); + // address payable bob = payable(vm.addr(2)); + // staking.whitelistAddress(bob); + // moveToken.mint(bob, 100); + // address payable carol = payable(vm.addr(3)); + // staking.whitelistAddress(carol); + // moveToken.mint(carol, 100); + + // // have them participate in the genesis ceremony + // vm.prank(alice); + // moveToken.approve(address(staking), 100); + // vm.prank(alice); + // staking.stake(address(mcr), moveToken, 34); + // vm.prank(bob); + // moveToken.approve(address(staking), 100); + // vm.prank(bob); + // staking.stake(address(mcr), moveToken, 33); + // vm.prank(carol); + // moveToken.approve(address(staking), 100); + // vm.prank(carol); + // staking.stake(address(mcr), moveToken, 33); + + // // end the genesis ceremony + // mcr.acceptGenesisCeremony(); + + // // carol will be dishonest + // MCRStorage.BlockCommitment memory dishonestCommitment = MCRStorage.BlockCommitment({ + // height: 1, + // commitment: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))), + // blockId: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))) + // }); + // vm.prank(carol); + // mcr.submitBlockCommitment(dishonestCommitment); + + // // carol will try to sign again + // vm.prank(carol); + // vm.expectRevert(AttesterAlreadyCommitted.selector); + // mcr.submitBlockCommitment(dishonestCommitment); + + // // make a block commitment + // MCRStorage.BlockCommitment memory bc1 = MCRStorage.BlockCommitment({ + // height: 1, + // commitment: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))), + // blockId: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))) + // }); + // vm.prank(alice); + // mcr.submitBlockCommitment(bc1); + // vm.prank(bob); + // mcr.submitBlockCommitment(bc1); + + // // now we move to block 2 and make some commitment just to trigger the epochRollover + // vm.warp(310 seconds); + + // // make a block commitment + // MCRStorage.BlockCommitment memory bc2 = MCRStorage.BlockCommitment({ + // height: 2, + // commitment: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))), + // blockId: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))) + // }); + // vm.prank(alice); + // mcr.submitBlockCommitment(bc2); + + // // check that roll over happened + // assertEq(mcr.getCurrentEpoch(), mcr.getEpochByBlockTime()); + // assertEq(mcr.getCurrentEpochStake(address(moveToken), alice), 34); + // assertEq(mcr.getCurrentEpochStake(address(moveToken), bob), 33); + // assertEq(mcr.getCurrentEpochStake(address(moveToken), carol), 33); + // (uint256 height, bytes32 commitment, bytes32 blockId) = mcr.acceptedBlocks(1); + // assert(commitment == bc1.commitment); + // assert(blockId == bc1.blockId); + // assert(height == 1); + // } + + address[] honestSigners = new address[](0); + address[] dishonestSigners = new address[](0); + + // function testChangingValidatorSet() public { + // vm.pauseGasMetering(); + + // uint256 blockTime = 300; + + // vm.warp(blockTime); + + // // three well-funded signers + // address payable alice = payable(vm.addr(1)); + // staking.whitelistAddress(alice); + // moveToken.mint(alice, 100); + + // address payable bob = payable(vm.addr(2)); + // staking.whitelistAddress(bob); + // moveToken.mint(bob, 100); + + // address payable carol = payable(vm.addr(3)); + // staking.whitelistAddress(carol); + // moveToken.mint(carol, 100); + + // // have them participate in the genesis ceremony + // vm.prank(alice); + // moveToken.approve(address(staking), 100); + // vm.prank(alice); + // staking.stake(address(mcr), moveToken, 34); + // vm.prank(bob); + // moveToken.approve(address(staking), 100); + // vm.prank(bob); + // staking.stake(address(mcr), moveToken, 33); + // vm.prank(carol); + // moveToken.approve(address(staking), 100); + // vm.prank(carol); + // staking.stake(address(mcr), moveToken, 33); + + // // honest signers + // honestSigners.push(alice); + // honestSigners.push(bob); + + // // dishonest signers + // dishonestSigners.push(carol); + + // uint256 reorgs = 50; + // for (uint256 i = 0; i < reorgs; i++) { + // uint256 commitmentHeights = 10; + // for (uint256 j = 0; j < commitmentHeights; j++) { + // uint256 blockHeight = i * 10 + j + 1; + // blockTime += 1; + // vm.warp(blockTime); + + // // commit dishonestly + // MCRStorage.BlockCommitment memory dishonestCommitment = MCRStorage.BlockCommitment({ + // height: blockHeight, + // commitment: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))), + // blockId: keccak256(abi.encodePacked(uint256(3), uint256(2), uint256(1))) + // }); + // for (uint256 k = 0; k < dishonestSigners.length / 2; k++) { + // vm.prank(dishonestSigners[k]); + // mcr.submitBlockCommitment(dishonestCommitment); + // } + + // // commit honestly + // MCRStorage.BlockCommitment memory honestCommitment = MCRStorage.BlockCommitment({ + // height: blockHeight, + // commitment: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))), + // blockId: keccak256(abi.encodePacked(uint256(1), uint256(2), uint256(3))) + // }); + // for (uint256 k = 0; k < honestSigners.length; k++) { + // vm.prank(honestSigners[k]); + // mcr.submitBlockCommitment(honestCommitment); + // } + + // // commit dishonestly some more + // for (uint256 k = dishonestSigners.length / 2; k < dishonestSigners.length; k++) { + // vm.prank(dishonestSigners[k]); + // mcr.submitBlockCommitment(dishonestCommitment); + // } + + // (uint256 height, bytes32 commitment, bytes32 blockId) = mcr.acceptedBlocks(blockHeight); + // assert(commitment == honestCommitment.commitment); + // assert(blockId == honestCommitment.blockId); + // assert(height == blockHeight); + // } + + // // add a new signer + // address payable newSigner = payable(vm.addr(4 + i)); + // staking.whitelistAddress(newSigner); + // moveToken.mint(newSigner, 100); + // vm.prank(newSigner); + // moveToken.approve(address(staking), 33); + // vm.prank(newSigner); + // staking.stake(address(mcr), moveToken, 33); + + // if (i % 3 == 2) { + // dishonestSigners.push(newSigner); + // } else { + // honestSigners.push(newSigner); + // } + + // if (i % 5 == 4) { + // // remove a dishonest signer + // address dishonestSigner = dishonestSigners[0]; + // vm.prank(dishonestSigner); + // staking.unstake(address(mcr), address(moveToken), 33); + // dishonestSigners[0] = dishonestSigners[dishonestSigners.length - 1]; + // dishonestSigners.pop(); + // } + + // if (i % 8 == 7) { + // // remove an honest signer + // address honestSigner = honestSigners[0]; + // vm.prank(honestSigner); + // staking.unstake(address(mcr), address(moveToken), 33); + // honestSigners[0] = honestSigners[honestSigners.length - 1]; + // honestSigners.pop(); + // } + + // blockTime += 5; + // vm.warp(blockTime); + // } + // } +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/staking/MovementStaking.t.sol b/protocol-units/settlement/mcr/contracts/test/staking/MovementStaking.t.sol new file mode 100644 index 000000000..2cf39e9da --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/staking/MovementStaking.t.sol @@ -0,0 +1,356 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../src/staking/MovementStaking.sol"; +import "../../src/token/MOVETokenDev.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract MovementStakingTest is Test { + bytes32 public constant WHITELIST_ROLE = keccak256("WHITELIST_ROLE"); + address public multisig = address(this); + MOVETokenDev public moveToken; + MovementStaking public staking; + + function setUp() public { + MOVETokenDev moveTokenImpl = new MOVETokenDev(); + TransparentUpgradeableProxy moveProxy = new TransparentUpgradeableProxy( + address(moveTokenImpl), + address(this), + abi.encodeWithSignature("initialize(address)", multisig) + ); + + MovementStaking stakingImpl = new MovementStaking(); + TransparentUpgradeableProxy stakingProxy = new TransparentUpgradeableProxy( + address(stakingImpl), + address(this), + abi.encodeWithSignature("initialize(address)", address(moveProxy)) + ); + moveToken = MOVETokenDev(address(moveProxy)); + staking = MovementStaking(address(stakingProxy)); + } + + function testCannotInitializeTwice() public { + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + staking.initialize(moveToken); + } + + function testRegister() public { + + // Register a new domain + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + assertEq(staking.getCurrentEpoch(domain), 0); + } + + function testWhitelist() public { + + // Our whitelister + address whitelister = vm.addr(1); + // Whitelist them + staking.whitelistAddress(whitelister); + assertEq(staking.hasRole(WHITELIST_ROLE, whitelister), true); + // Remove them from the whitelist + staking.removeAddressFromWhitelist(whitelister); + assertEq(staking.hasRole(WHITELIST_ROLE, whitelister), false); + // As a whitelister let's see if I can whitelist myself + vm.prank(whitelister); + vm.expectRevert(); + staking.whitelistAddress(whitelister); + } + + function testSimpleStaker() public { + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // stake at the domain + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 100); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + assertEq(moveToken.balanceOf(staker), 0); + assertEq(staking.getStakeAtEpoch(domain, 0, address(moveToken), staker), 100); + } + + function testSimpleGenesisCeremony() public { + + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // genesis ceremony + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 100); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + vm.prank(domain); + staking.acceptGenesisCeremony(); + assertNotEq(staking.currentEpochByDomain(domain), 0); + assertEq(staking.getCurrentEpochStake(domain, address(moveToken), staker), 100); + + vm.expectRevert(IMovementStaking.GenesisAlreadyAccepted.selector); + vm.prank(domain); + staking.acceptGenesisCeremony(); + } + + function testSimpleRolloverEpoch() public { + + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // genesis ceremony + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 100); + staking.whitelistAddress(staker); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + vm.prank(domain); + staking.acceptGenesisCeremony(); + + // rollover epoch + for (uint256 i = 0; i < 10; i++) { + vm.warp((i + 1) * 1 seconds); + uint256 epochBefore = staking.getCurrentEpoch(domain); + vm.prank(domain); + staking.rollOverEpoch(); + uint256 epochAfter = staking.getCurrentEpoch(domain); + assertEq(epochAfter, epochBefore + 1); + assertEq(staking.getCurrentEpochStake(domain, address(moveToken), staker), 100); + } + } + + function testUnstakeRolloverEpoch() public { + + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // genesis ceremony + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 100); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + vm.prank(domain); + staking.acceptGenesisCeremony(); + + for (uint256 i = 0; i < 10; i++) { + vm.warp((i + 1) * 1 seconds); + uint256 epochBefore = staking.getCurrentEpoch(domain); + + // unstake + vm.prank(staker); + staking.unstake(domain, address(moveToken), 10); + assertEq(staking.getCurrentEpochStake(domain, address(moveToken), staker), 100 - (i * 10)); + assertEq(moveToken.balanceOf(staker), i * 10); + + // roll over + vm.prank(domain); + staking.rollOverEpoch(); + uint256 epochAfter = staking.getCurrentEpoch(domain); + assertEq(epochAfter, epochBefore + 1); + } + } + + function testUnstakeAndStakeRolloverEpoch() public { + + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // genesis ceremony + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 150); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + vm.prank(domain); + staking.acceptGenesisCeremony(); + + for (uint256 i = 0; i < 10; i++) { + vm.warp((i + 1) * 1 seconds); + uint256 epochBefore = staking.getCurrentEpoch(domain); + + // unstake + vm.prank(staker); + staking.unstake(domain, address(moveToken), 10); + + // stake + vm.prank(staker); + moveToken.approve(address(staking), 5); + vm.prank(staker); + staking.stake(domain, moveToken, 5); + + // check stake + assertEq(staking.getCurrentEpochStake(domain, address(moveToken), staker), (100 - (i * 10)) + (i * 5)); + assertEq(moveToken.balanceOf(staker), (50 - (i + 1) * 5) + (i * 10)); + + // roll over + vm.prank(domain); + staking.rollOverEpoch(); + uint256 epochAfter = staking.getCurrentEpoch(domain); + assertEq(epochAfter, epochBefore + 1); + } + } + + function testUnstakeStakeAndSlashRolloverEpoch() public { + + + // Register a new staker + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // genesis ceremony + address payable staker = payable(vm.addr(2)); + staking.whitelistAddress(staker); + moveToken.mint(staker, 150); + vm.prank(staker); + moveToken.approve(address(staking), 100); + vm.prank(staker); + staking.stake(domain, moveToken, 100); + vm.prank(domain); + staking.acceptGenesisCeremony(); + + for (uint256 i = 0; i < 5; i++) { + vm.warp((i + 1) * 1 seconds); + uint256 epochBefore = staking.getCurrentEpoch(domain); + + // unstake + vm.prank(staker); + staking.unstake(domain, address(moveToken), 10); + + // stake + vm.prank(staker); + moveToken.approve(address(staking), 5); + vm.prank(staker); + staking.stake(domain, moveToken, 5); + + // check stake + assertEq( + staking.getCurrentEpochStake(domain, address(moveToken), staker), (100 - (i * 10)) + (i * 5) - (i * 1) + ); + assertEq(moveToken.balanceOf(staker), (50 - (i + 1) * 5) + (i * 10)); + + // slash + vm.prank(domain); + address[] memory custodians1 = new address[](1); + custodians1[0] = address(moveToken); + address[] memory attesters1 = new address[](1); + attesters1[0] = staker; + uint256[] memory amounts1 = new uint256[](1); + amounts1[0] = 1; + uint256[] memory refundAmounts1 = new uint256[](1); + refundAmounts1[0] = 0; + staking.slash(custodians1, attesters1, amounts1, refundAmounts1); + + // slash immediately takes effect + assertEq( + staking.getCurrentEpochStake(domain, address(moveToken), staker), + (100 - (i * 10)) + (i * 5) - ((i + 1) * 1) + ); + + // roll over + vm.prank(domain); + staking.rollOverEpoch(); + uint256 epochAfter = staking.getCurrentEpoch(domain); + assertEq(epochAfter, epochBefore + 1); + } + } + + function testHalbornReward() public { + + + // Register a domain + address payable domain = payable(vm.addr(1)); + address[] memory custodians = new address[](1); + custodians[0] = address(moveToken); + vm.prank(domain); + staking.registerDomain(1 seconds, custodians); + + // Alice stakes 1000 tokens + address payable alice = payable(vm.addr(2)); + staking.whitelistAddress(alice); + moveToken.mint(alice, 1000); + vm.prank(alice); + moveToken.approve(address(staking), 1000); + vm.prank(alice); + staking.stake(domain, moveToken, 1000); + + // Bob stakes 100 tokens + address payable bob = payable(vm.addr(3)); + staking.whitelistAddress(bob); + moveToken.mint(bob, 100); + vm.prank(bob); + moveToken.approve(address(staking), 100); + vm.prank(bob); + staking.stake(domain, moveToken, 100); + + // Assertions on stakes and balances + assertEq(moveToken.balanceOf(alice), 0); + assertEq(moveToken.balanceOf(bob), 0); + assertEq(moveToken.balanceOf(address(staking)), 1100); + assertEq(staking.getTotalStakeForEpoch(domain, 0, address(moveToken)), 1100); + assertEq(staking.getStakeAtEpoch(domain, 0, address(moveToken), alice), 1000); + assertEq(staking.getStakeAtEpoch(domain, 0, address(moveToken), bob), 100); + + // Charlie calls reward with himself only to steal tokens + address charlie = vm.addr(4); + address[] memory attesters = new address[](1); + attesters[0] = charlie; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 1000; + vm.prank(charlie); + vm.expectRevert( + abi.encodeWithSignature( + "ERC20InsufficientAllowance(address,uint256,uint256)", + address(staking), // should be called by the staking contract + 0, + 1000 + ) + ); + staking.reward(attesters, amounts, custodians); + } +} diff --git a/protocol-units/settlement/mcr/contracts/test/staking/base/BaseStaking.t.sol b/protocol-units/settlement/mcr/contracts/test/staking/base/BaseStaking.t.sol new file mode 100644 index 000000000..956297050 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/staking/base/BaseStaking.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/staking/base/BaseStaking.sol"; + +contract BaseStakingTest is Test { + + function testInitialize() public { + + BaseStaking staking = new BaseStaking(); + staking.initialize(); + + } + + function testCannotInitializeTwice() public { + + BaseStaking staking = new BaseStaking(); + staking.initialize(); + + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + staking.initialize(); + + } +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/token/Faucet.t.sol b/protocol-units/settlement/mcr/contracts/test/token/Faucet.t.sol new file mode 100644 index 000000000..86c1d0f55 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/Faucet.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {MOVEFaucet, IERC20} from '../../src/token/faucet/MOVEFaucet.sol'; +import {MOVETokenDev} from '../../src/token/MOVETokenDev.sol'; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract MOVEFaucetTest is Test { + MOVEFaucet public faucet; + MOVETokenDev public token; + + fallback() external payable {} + + function setUp() public { + MOVETokenDev tokenImpl = new MOVETokenDev(); + TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy(address(tokenImpl), address(this), abi.encodeWithSignature("initialize(address)", address(this))); + token = MOVETokenDev(address(tokenProxy)); + faucet = new MOVEFaucet(IERC20(address(token))); + } + + // function testFaucet() public { + // vm.warp(1 days); + + // token.balanceOf(address(this)); + + // token.transfer(address(faucet), 20 * 10 ** token.decimals()); + + // vm.deal(address(0x1337), 2* 10**17); + + // vm.startPrank(address(0x1337)); + // vm.expectRevert("MOVEFaucet: eth invalid amount"); + // faucet.faucet{value: 10**16}(); + + // faucet.faucet{value: 10**17}(); + // assertEq(token.balanceOf(address(0x1337)), 10 * 10 ** token.decimals()); + + // vm.expectRevert("MOVEFaucet: balance must be less than 1 MOVE"); + // faucet.faucet{value: 10**17}(); + + // token.transfer(address(0xdead), token.balanceOf(address(0x1337))); + + // vm.expectRevert("MOVEFaucet: rate limit exceeded"); + // faucet.faucet{value: 10**17}(); + + // vm.warp(block.timestamp + 1 days); + // faucet.faucet{value: 10**17}(); + // vm.stopPrank(); + // vm.prank(address(this)); + // uint256 balance = address(this).balance; + // faucet.withdraw(); + // assertEq(address(faucet).balance, 0); + // assertEq(address(this).balance, balance + 2*10**17); + // } + + +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/token/MOVEToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/MOVEToken.t.sol new file mode 100644 index 000000000..674c4f5a4 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/MOVEToken.t.sol @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {MOVEToken} from "../../src/token/MOVEToken.sol"; +import {MOVETokenDev} from "../../src/token/MOVETokenDev.sol"; +import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol"; +import {CompatibilityFallbackHandler} from "@safe-smart-account/contracts/handler/CompatibilityFallbackHandler.sol"; +import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol"; +import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol"; + +function string2Address(bytes memory str) returns (address addr) { + bytes32 data = keccak256(str); + assembly { + mstore(0, data) + addr := mload(0) + } +} + +contract MOVETokenTest is Test { + MOVEToken public token; + TransparentUpgradeableProxy public tokenProxy; + ProxyAdmin public admin; + MOVEToken public moveTokenImplementation; + MOVETokenDev public moveTokenImplementation2; + TimelockController public timelock; + string public moveSignature = "initialize(address,address)"; + address public multisig = address(0x00db70A9e12537495C359581b7b3Bc3a69379A00); + address public anchorage = address(0xabc); + + function setUp() public { + moveTokenImplementation = new MOVEToken(); + moveTokenImplementation2 = new MOVETokenDev(); + + uint256 minDelay = 1 days; + address[] memory proposers = new address[](5); + address[] memory executors = new address[](1); + + proposers[0] = string2Address("Andy"); + proposers[1] = string2Address("Bob"); + proposers[2] = string2Address("Charlie"); + proposers[3] = string2Address("David"); + proposers[4] = string2Address("Eve"); + executors[0] = multisig; + + timelock = new TimelockController(minDelay, proposers, executors, address(0x0)); + + vm.recordLogs(); + // Deploy proxy + tokenProxy = new TransparentUpgradeableProxy( + address(moveTokenImplementation), + address(timelock), + abi.encodeWithSignature(moveSignature, multisig, anchorage) + ); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + admin = ProxyAdmin(entries[entries.length - 2].emitter); + + token = MOVEToken(address(tokenProxy)); + } + + function testCannotInitializeTwice() public { + // Initialize the contract + vm.expectRevert(0xf92ee8a9); + token.initialize(multisig, anchorage); + } + + function testDecimals() public { + assertEq(token.decimals(), 8); + } + + function testTotalSupply() public { + assertEq(token.totalSupply(), 10000000000 * 10 ** 8); + } + + function testMultisigBalance() public { + assertEq(token.balanceOf(anchorage), 10000000000 * 10 ** 8); + } + + function testAdminRoleFuzz(address other) public { + assertEq(token.hasRole(0x00, other), false); + assertEq(token.hasRole(0x00, multisig), true); + assertEq(token.hasRole(0x00, anchorage), false); + + vm.expectRevert( + abi.encodeWithSelector(IAccessControl.AccessControlUnauthorizedAccount.selector, address(this), 0x00) + ); + token.grantRole(0x00, other); + } + + function testUpgradeFromTimelock() public { + assertEq(admin.owner(), address(timelock)); + + vm.prank(string2Address("Andy")); + timelock.schedule( + address(admin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(tokenProxy), + address(moveTokenImplementation2), + "" + ), + bytes32(0), + bytes32(0), + block.timestamp + 1 days + ); + + vm.warp(block.timestamp + 1 days + 1); + + vm.prank(multisig); + timelock.execute( + address(admin), + 0, + abi.encodeWithSignature( + "upgradeAndCall(address,address,bytes)", + address(tokenProxy), + address(moveTokenImplementation2), + "" + ), + bytes32(0), + bytes32(0) + ); + + // Check the token details + assertEq(token.decimals(), 8); + assertEq(token.totalSupply(), 10000000000 * 10 ** 8); + assertEq(token.balanceOf(anchorage), 10000000000 * 10 ** 8); + } + + function testTransferToNewTimelock() public { + assertEq(admin.owner(), address(timelock)); + + uint256 minDelay = 1 days; + address[] memory proposers = new address[](5); + address[] memory executors = new address[](1); + + // Andy has been compromised, Albert will be the new proposer + // we need to transfer the proxyAdmin ownership to a new timelock + proposers[0] = string2Address("Albert"); + proposers[1] = string2Address("Bob"); + proposers[2] = string2Address("Charlie"); + proposers[3] = string2Address("David"); + proposers[4] = string2Address("Eve"); + + executors[0] = multisig; + + TimelockController newTimelock = new TimelockController(minDelay, proposers, executors, address(0x0)); + vm.prank(string2Address("Bob")); + timelock.schedule( + address(admin), + 0, + abi.encodeWithSignature("transferOwnership(address)", address(newTimelock)), + bytes32(0), + bytes32(0), + block.timestamp + 1 days + ); + + vm.warp(block.timestamp + 1 days + 1); + vm.prank(multisig); + timelock.execute( + address(admin), + 0, + abi.encodeWithSignature("transferOwnership(address)", address(newTimelock)), + bytes32(0), + bytes32(0) + ); + + assertEq(admin.owner(), address(newTimelock)); + } + + function testGrants() public { + testUpgradeFromTimelock(); + + vm.prank(multisig); + MOVETokenDev(address(token)).grantRoles(multisig); + + // Check the token details + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), multisig), true); + } + + function testMint() public { + testUpgradeFromTimelock(); + + vm.prank(multisig); + MOVETokenDev(address(token)).grantRoles(multisig); + uint256 intialBalance = MOVETokenDev(address(token)).balanceOf(address(0x1337)); + // Mint tokens + vm.prank(multisig); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + + // Check the token details + assertEq(MOVETokenDev(address(token)).balanceOf(address(0x1337)), intialBalance + 100); + } + + function testRevokeMinterRole() public { + testUpgradeFromTimelock(); + + vm.prank(multisig); + MOVETokenDev(address(token)).grantRoles(multisig); + + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), multisig), true); + + vm.startPrank(multisig); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + // Revoke minter role + MOVETokenDev(address(token)).revokeMinterRole(multisig); + + // Check the token details + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), multisig), false); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + multisig, + MOVETokenDev(address(token)).MINTER_ROLE() + ) + ); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + vm.stopPrank(); + } + + function testGrantRevokeMinterAdminRole() public { + testUpgradeFromTimelock(); + vm.prank(multisig); + MOVETokenDev(address(token)).grantRoles(multisig); + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), multisig), true); + vm.startPrank(multisig); + + MOVETokenDev(address(token)).mint(address(0x1337), 100); + // Revoke minter role + MOVETokenDev(address(token)).revokeMinterRole(multisig); + + // Check the token details + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), multisig), false); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + multisig, + MOVETokenDev(address(token)).MINTER_ROLE() + ) + ); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + + assertEq( + MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), address(0x1337)), false + ); + // Grant minter role + MOVETokenDev(address(token)).grantMinterRole(address(0x1337)); + vm.stopPrank(); + vm.prank(address(0x1337)); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + + // Check the token details + assertEq( + MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), address(0x1337)), true + ); + + // Revoke minter role + vm.prank(multisig); + MOVETokenDev(address(token)).revokeMinterRole(address(0x1337)); + + assertEq( + MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ROLE(), address(0x1337)), false + ); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + address(0x1337), + MOVETokenDev(address(token)).MINTER_ROLE() + ) + ); + vm.prank(address(0x1337)); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + + assertEq(MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ADMIN_ROLE(), multisig), true); + // Revoke minter admin role + vm.startPrank(multisig); + MOVETokenDev(address(token)).revokeMinterAdminRole(multisig); + + assertEq( + MOVETokenDev(address(token)).hasRole(MOVETokenDev(address(token)).MINTER_ADMIN_ROLE(), multisig), false + ); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + multisig, + MOVETokenDev(address(token)).MINTER_ADMIN_ROLE() + ) + ); + MOVETokenDev(address(token)).grantMinterRole(multisig); + + vm.expectRevert( + abi.encodeWithSelector( + IAccessControl.AccessControlUnauthorizedAccount.selector, + multisig, + MOVETokenDev(address(token)).MINTER_ROLE() + ) + ); + MOVETokenDev(address(token)).mint(address(0x1337), 100); + vm.stopPrank(); + } +} diff --git a/protocol-units/settlement/mcr/contracts/test/token/base/BaseToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/base/BaseToken.t.sol new file mode 100644 index 000000000..493214f0a --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/base/BaseToken.t.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/token/base/BaseToken.sol"; + +contract BaseTokenTest is Test { + function testInitialize() public { + BaseToken token = new BaseToken(); + + // Call the initialize function + token.initialize("Base Token", "BASE"); + + // Check the token details + assertEq(token.name(), "Base Token"); + assertEq(token.symbol(), "BASE"); + } + + function testCannotInitializeTwice() public { + BaseToken token = new BaseToken(); + + // Initialize the contract + token.initialize("Base Token", "BASE"); + + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + token.initialize("Base Token", "BASE"); + } +} diff --git a/protocol-units/settlement/mcr/contracts/test/token/base/MintableToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/base/MintableToken.t.sol new file mode 100644 index 000000000..353fdb542 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/base/MintableToken.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/token/base/MintableToken.sol"; + +contract MintableTokenTest is Test { + + function testInitialize() public { + + MintableToken token = new MintableToken(); + + // Call the initialize function + token.initialize("Base Token", "BASE"); + + // Check the token details + assertEq(token.name(), "Base Token"); + assertEq(token.symbol(), "BASE"); + } + + function testCannotInitializeTwice() public { + + MintableToken token = new MintableToken(); + + // Initialize the contract + token.initialize("Base Token", "BASE"); + + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + token.initialize("Base Token", "BASE"); + } +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/token/base/WrappedToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/base/WrappedToken.t.sol new file mode 100644 index 000000000..e0b087221 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/base/WrappedToken.t.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/token/base/MintableToken.sol"; +import "../../../src/token/base/WrappedToken.sol"; +// import base access control instead of upgradeable access control + + +contract WrappedTokenTest is Test { + + function testInitialize() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + WrappedToken token = new WrappedToken(); + token.initialize("Base Token", "BASE", underlyingToken); + + // Check the token details + assertEq(token.name(), "Base Token"); + assertEq(token.symbol(), "BASE"); + + } + + function testCannotInitializeTwice() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + WrappedToken token = new WrappedToken(); + token.initialize("Base Token", "BASE", underlyingToken); + + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + token.initialize("Base Token", "BASE", underlyingToken); + + } + + function testGrants() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + WrappedToken token = new WrappedToken(); + token.initialize("Base Token", "BASE", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert(underlyingToken.hasRole(underlyingToken.MINTER_ROLE(), address(token))); + + // valid minting succeeds + vm.prank(address(token)); + underlyingToken.mint(address(this), 100); + assert(underlyingToken.balanceOf(address(this)) == 100); + + // invalid minting fails + address payable signer = payable(vm.addr(1)); + vm.prank(signer); + vm.expectRevert(); // todo: catch type + underlyingToken.mint(signer, 100); + + } + +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/token/custodian/CustodianToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/custodian/CustodianToken.t.sol new file mode 100644 index 000000000..0d86c554e --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/custodian/CustodianToken.t.sol @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/token/base/MintableToken.sol"; +import "../../../src/token/custodian/CustodianToken.sol"; +// import base access control instead of upgradeable access control + +contract CustodianTokenTest is Test { + function testInitialize() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + // Check the token details + assertEq(token.name(), "Custodian Token"); + assertEq(token.symbol(), "CUSTODIAN"); + } + + function testCannotInitializeTwice() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + // Attempt to initialize again should fail + vm.expectRevert(0xf92ee8a9); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + } + + function testGrants() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // valid minting succeeds + vm.prank(address(token)); + underlyingToken.mint(address(this), 100); + assert(underlyingToken.balanceOf(address(this)) == 100); + + // invalid minting fails + address payable signer = payable(vm.addr(1)); + vm.prank(signer); + vm.expectRevert(); // todo: catch type + underlyingToken.mint(signer, 100); + } + + function testCustodianMint() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // valid minting succeeds + token.mint(address(this), 100); + assert(token.balanceOf(address(this)) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + + // valid minting is incremental + address payable signer = payable(vm.addr(1)); + token.mint(signer, 100); + assert(token.balanceOf(signer) == 100); + assert(underlyingToken.balanceOf(address(token)) == 200); + + // signers with the minter role can call through the custodian + token.grantMinterRole(signer); + vm.prank(signer); + token.mint(signer, 100); + assert(token.balanceOf(signer) == 200); + assert(underlyingToken.balanceOf(address(token)) == 300); + + // signers without the minter role cannot call through the custodian + token.revokeMinterRole(signer); + vm.prank(signer); + vm.expectRevert(); // todo: catch type + token.mint(signer, 100); + } + + function testCustodianTransferToValidSink() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // signers + address payable validSink = payable(vm.addr(2)); + token.grantTransferSinkRole(validSink); + address payable alice = payable(vm.addr(5)); + + // transfer to valid sink succeeds + token.mint(alice, 100); + vm.prank(alice); + token.transfer(validSink, 100); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(validSink) == 100); + } + + function testCustodianTransferToInvalidSink() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // signers + address payable invalidSink = payable(vm.addr(2)); + address payable alice = payable(vm.addr(5)); + + // transfer to invalid sink fails + token.mint(alice, 100); + vm.prank(alice); + vm.expectRevert(); // todo: catch type + token.transfer(invalidSink, 100); + } + + function testCustodianBuyValidSource() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // signers + address payable validSource = payable(vm.addr(2)); + token.grantBuyerRole(validSource); + address payable alice = payable(vm.addr(5)); + + // fund the valid source in the underlying token + underlyingToken.mint(validSource, 100); + + // approve the custodian to spend the underlying token + vm.prank(validSource); + underlyingToken.approve(address(token), 100); + + // buy from valid source succeeds + vm.prank(validSource); + token.buyCustodialToken(alice, 100); + assert(token.balanceOf(alice) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + assert(underlyingToken.balanceOf(validSource) == 0); + } + + function testCustodianBuyInvalidSource() public { + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + CustodianToken token = new CustodianToken(); + token.initialize("Custodian Token", "CUSTODIAN", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert( + underlyingToken.hasRole( + underlyingToken.MINTER_ROLE(), + address(token) + ) + ); + + // signers + address payable invalidSource = payable(vm.addr(2)); + address payable alice = payable(vm.addr(5)); + + // fund the valid source in the underlying token + underlyingToken.mint(invalidSource, 100); + + // approve the custodian to spend the underlying token + vm.prank(invalidSource); + underlyingToken.approve(address(token), 100); + + // buy from valid source succeeds + vm.prank(invalidSource); + vm.expectRevert(); // todo: catch type + token.buyCustodialToken(alice, 100); + } +} diff --git a/protocol-units/settlement/mcr/contracts/test/token/locked/LockedToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/locked/LockedToken.t.sol new file mode 100644 index 000000000..40aeed307 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/locked/LockedToken.t.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../src/token/base/MintableToken.sol"; +import "../../../src/token/locked/LockedToken.sol"; +// import base access control instead of upgradeable access control + +contract LockedTokenTest is Test { + + function testInitialize() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + // Check the token details + assertEq(token.name(), "Locked Token"); + assertEq(token.symbol(), "LOCKED"); + + } + + function testBasicLock() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + assert(underlyingToken.hasRole(underlyingToken.MINTER_ROLE(), address(token))); + + // signers + address payable alice = payable(vm.addr(1)); + + // mint locked tokens + address[] memory addresses = new address[](1); + addresses[0] = alice; + uint256[] memory amounts = new uint256[](1); + amounts[0] = 100; + uint256[] memory locks = new uint256[](1); + locks[0] = block.timestamp + 100; + token.mintAndLock( + addresses, + amounts, + amounts, // in this test case, we are not adding separate lock amounts + locks + ); + assert(token.balanceOf(alice) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + assert(underlyingToken.balanceOf(alice) == 0); + + vm.warp(block.timestamp + 1); + // cannot release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + assert(underlyingToken.balanceOf(alice) == 0); + + // tick forward + vm.warp(block.timestamp + 101); + + // release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(address(token)) == 0); + assert(underlyingToken.balanceOf(alice) == 100); + + } + + function testLockWithEarnings() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + + // signers + address payable alice = payable(vm.addr(1)); + + // mint locked tokens + address[] memory addresses = new address[](1); + addresses[0] = alice; + uint256[] memory mintAmounts = new uint256[](1); + mintAmounts[0] = 100; + uint256[] memory lockAmounts = new uint256[](1); + lockAmounts[0] = 150; + uint256[] memory locks = new uint256[](1); + locks[0] = block.timestamp + 100; + token.mintAndLock( + addresses, + mintAmounts, + lockAmounts, + locks + ); + assert(token.balanceOf(alice) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + assert(underlyingToken.balanceOf(alice) == 0); + + // cannot release locked tokens + vm.warp(block.timestamp + 1); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 100); + assert(underlyingToken.balanceOf(address(token)) == 100); + assert(underlyingToken.balanceOf(alice) == 0); + + // alice earns on locked tokens + token.mint(alice, 50); + assert(token.balanceOf(alice) == 150); + + // tick forward + vm.warp(block.timestamp + 101); + + // release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(address(token)) == 0); + assert(underlyingToken.balanceOf(alice) == 150); + } + + function testLockMultiple() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + + // signers + address payable alice = payable(vm.addr(1)); + + // mint locked tokens + address[] memory addresses = new address[](3); + addresses[0] = alice; + addresses[1] = alice; + addresses[2] = alice; + uint256[] memory mintAmounts = new uint256[](3); + mintAmounts[0] = 100; + mintAmounts[1] = 50; + mintAmounts[2] = 25; + uint256[] memory lockAmounts = new uint256[](3); + lockAmounts[0] = 100; + lockAmounts[1] = 50; + lockAmounts[2] = 25; + uint256[] memory locks = new uint256[](3); + locks[0] = block.timestamp + 100; + locks[1] = block.timestamp + 200; + locks[2] = block.timestamp + 300; + token.mintAndLock( + addresses, + mintAmounts, + lockAmounts, + locks + ); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // cannot release locked tokens + vm.warp(block.timestamp + 1); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // tick forward + vm.warp(block.timestamp + 301); + + // release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(address(token)) == 0); + assert(underlyingToken.balanceOf(alice) == 175); + } + + function testLockMultiplePrematureClaim() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + + // signers + address payable alice = payable(vm.addr(1)); + + // mint locked tokens + address[] memory addresses = new address[](3); + addresses[0] = alice; + addresses[1] = alice; + addresses[2] = alice; + uint256[] memory mintAmounts = new uint256[](3); + mintAmounts[0] = 100; + mintAmounts[1] = 50; + mintAmounts[2] = 25; + uint256[] memory lockAmounts = new uint256[](3); + lockAmounts[0] = 100; + lockAmounts[1] = 50; + lockAmounts[2] = 25; + uint256[] memory locks = new uint256[](3); + locks[0] = block.timestamp + 100; + locks[1] = block.timestamp + 200; + locks[2] = block.timestamp + 400; + token.mintAndLock( + addresses, + mintAmounts, + lockAmounts, + locks + ); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // cannot release locked tokens + vm.warp(block.timestamp + 1); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // tick forward + vm.warp(block.timestamp + 301); + + // release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 25); + assert(underlyingToken.balanceOf(address(token)) == 25); + assert(underlyingToken.balanceOf(alice) == 150); + // two releases occurred, alice lock index 0 should still be present + (uint256 lock1,) = token.locks(alice, 0); + assert(lock1 == 25); + + // tick forward + vm.warp(block.timestamp + 101); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(address(token)) == 0); + assert(underlyingToken.balanceOf(alice) == 175); + // call should revert with no locks existent + vm.expectRevert(); + (uint256 lock2,) = token.locks(alice, 0); + } + + + function testTransferLockedAsset() public { + + MintableToken underlyingToken = new MintableToken(); + underlyingToken.initialize("Underlying Token", "UNDERLYING"); + + LockedToken token = new LockedToken(); + token.initialize("Locked Token", "LOCKED", underlyingToken); + + underlyingToken.grantMinterRole(address(token)); + + // signers + address payable alice = payable(vm.addr(1)); + + // mint locked tokens + address[] memory addresses = new address[](3); + addresses[0] = alice; + addresses[1] = alice; + addresses[2] = alice; + uint256[] memory mintAmounts = new uint256[](3); + mintAmounts[0] = 100; + mintAmounts[1] = 50; + mintAmounts[2] = 25; + uint256[] memory lockAmounts = new uint256[](3); + lockAmounts[0] = 100; + lockAmounts[1] = 50; + lockAmounts[2] = 25; + uint256[] memory locks = new uint256[](3); + locks[0] = block.timestamp + 100; + locks[1] = block.timestamp + 200; + locks[2] = block.timestamp + 400; + token.mintAndLock( + addresses, + mintAmounts, + lockAmounts, + locks + ); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // cannot release locked tokens + vm.warp(block.timestamp + 1); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 175); + assert(underlyingToken.balanceOf(address(token)) == 175); + assert(underlyingToken.balanceOf(alice) == 0); + + // tick forward + vm.warp(block.timestamp + 301); + + // release locked tokens + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 25); + assert(underlyingToken.balanceOf(address(token)) == 25); + assert(underlyingToken.balanceOf(alice) == 150); + // two releases occurred, alice lock index 0 should still be present + (uint256 lock1,) = token.locks(alice, 0); + assert(lock1 == 25); + + vm.prank(alice); + token.transfer(address(0x1337), 20); + // tick forward + vm.warp(block.timestamp + 101); + vm.prank(alice); + token.release(); + assert(token.balanceOf(alice) == 0); + assert(underlyingToken.balanceOf(address(token)) == 20); + assert(underlyingToken.balanceOf(alice) == 155); + // call should revert with no locks existent + (uint256 lock2,) = token.locks(alice, 0); + assert(lock2 == 20); + } + + +} \ No newline at end of file diff --git a/protocol-units/settlement/mcr/contracts/test/token/stlMoveToken.t.sol b/protocol-units/settlement/mcr/contracts/test/token/stlMoveToken.t.sol new file mode 100644 index 000000000..2d7ea50d4 --- /dev/null +++ b/protocol-units/settlement/mcr/contracts/test/token/stlMoveToken.t.sol @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../src/token/stlMoveToken.sol"; +import "../../src/token/MOVETokenDev.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract stlMoveTokenTest is Test { + address public multisig = address(this); + MOVETokenDev public underlyingToken; + stlMoveToken public token; + + function setUp() public { + MOVETokenDev underlyingTokenImpl = new MOVETokenDev(); + TransparentUpgradeableProxy underlyingTokenProxy = new TransparentUpgradeableProxy( + address(underlyingTokenImpl), + address(this), + abi.encodeWithSignature("initialize(address)", multisig) + ); + + + stlMoveToken tokenImpl = new stlMoveToken(); + TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy( + address(tokenImpl), + address(this), + abi.encodeWithSignature("initialize(address)", address(underlyingTokenProxy)) + ); + underlyingToken = MOVETokenDev(address(underlyingTokenProxy)); + token = stlMoveToken(address(tokenProxy)); + + // Check the token details + assertEq(token.name(), "Stakable Locked Move Token"); + assertEq(token.symbol(), "stlMOVE"); + } + + function testCannotInitializeTwice() public { + + + // Expect reversion + vm.expectRevert(0xf92ee8a9); + token.initialize(underlyingToken); + } + + function testSimulateStaking() public { + + vm.prank(multisig); + underlyingToken.grantMinterRole(address(token)); + assert(underlyingToken.hasRole(underlyingToken.MINTER_ROLE(), address(token))); + + // signers + address payable alice = payable(vm.addr(1)); + address payable bob = payable(vm.addr(2)); + address payable carol = payable(vm.addr(3)); + address payable dave = payable(vm.addr(4)); + + // mint locked tokens + address[] memory addresses = new address[](6); + addresses[0] = alice; + addresses[1] = bob; + addresses[2] = carol; + addresses[3] = dave; + addresses[4] = alice; + addresses[5] = bob; + uint256[] memory mintAmounts = new uint256[](6); + mintAmounts[0] = 100; + mintAmounts[1] = 100; + mintAmounts[2] = 100; + mintAmounts[3] = 100; + mintAmounts[4] = 0; + mintAmounts[5] = 0; + uint256[] memory lockAmounts = new uint256[](6); + lockAmounts[0] = 100; + lockAmounts[1] = 100; + lockAmounts[2] = 100; + lockAmounts[3] = 100; + lockAmounts[4] = UINT256_MAX; + lockAmounts[5] = UINT256_MAX; + uint256[] memory locks = new uint256[](6); + locks[0] = block.timestamp + 100; + locks[1] = block.timestamp + 100; + locks[2] = block.timestamp + 100; + locks[3] = block.timestamp + 100; + locks[4] = block.timestamp + 200; + locks[5] = block.timestamp + 200; + token.mintAndLock(addresses, mintAmounts, lockAmounts, locks); + assertEq(token.balanceOf(alice), 100); + assertEq(token.balanceOf(bob), 100); + assertEq(token.balanceOf(carol), 100); + assertEq(token.balanceOf(dave), 100); + assertEq(underlyingToken.balanceOf(address(token)), 400); + assertEq(underlyingToken.balanceOf(alice), 0); + assertEq(underlyingToken.balanceOf(bob), 0); + assertEq(underlyingToken.balanceOf(carol), 0); + assertEq(underlyingToken.balanceOf(dave), 0); + + vm.warp(block.timestamp + 1); + // cannot release locked tokens + vm.prank(alice); + token.release(); + assertEq(token.balanceOf(alice), 100); + assertEq(underlyingToken.balanceOf(address(token)), 400); + assertEq(underlyingToken.balanceOf(alice), 0); + vm.prank(bob); + token.release(); + assertEq(token.balanceOf(bob), 100); + assertEq(underlyingToken.balanceOf(address(token)), 400); + assertEq(underlyingToken.balanceOf(bob), 0); + vm.prank(carol); + token.release(); + assertEq(token.balanceOf(carol), 100); + assertEq(underlyingToken.balanceOf(address(token)), 400); + assertEq(underlyingToken.balanceOf(carol), 0); + vm.prank(dave); + token.release(); + assertEq(token.balanceOf(dave), 100); + assertEq(underlyingToken.balanceOf(address(token)), 400); + assertEq(underlyingToken.balanceOf(dave), 0); + + // add a transfer sink to represent a staking pool + address payable stakingPool = payable(vm.addr(5)); + token.grantTransferSinkRole(stakingPool); + token.grantBuyerRole(stakingPool); + + // mint some funds on the underlying token for the staking pool to reward stakers + underlyingToken.mint(stakingPool, 100); + + // use to custodian to stake the locked tokens + vm.prank(alice); + token.transfer(stakingPool, 100); + assertEq(token.balanceOf(alice), 0); + assertEq(underlyingToken.balanceOf(stakingPool), 200); + assertEq(underlyingToken.balanceOf(address(token)), 300); + vm.prank(bob); + token.transfer(stakingPool, 100); + assertEq(token.balanceOf(bob), 0); + assertEq(underlyingToken.balanceOf(stakingPool), 300); + assertEq(underlyingToken.balanceOf(address(token)), 200); + vm.prank(carol); + token.transfer(stakingPool, 100); + assertEq(token.balanceOf(carol), 0); + assertEq(underlyingToken.balanceOf(stakingPool), 400); + assertEq(underlyingToken.balanceOf(address(token)), 100); + // ! dave does not stake + + // alice gets reward and cashes out through the custodian, but cannot withdraw + vm.prank(stakingPool); + underlyingToken.approve(address(token), 110); + vm.prank(stakingPool); + token.buyCustodialToken(alice, 110); + assertEq(token.balanceOf(alice), 110); + assertEq(underlyingToken.balanceOf(stakingPool), 290); + assertEq(underlyingToken.balanceOf(address(token)), 210); + vm.prank(alice); + token.release(); + assertEq(token.balanceOf(alice), 110); + assertEq(underlyingToken.balanceOf(alice), 0); + assertEq(underlyingToken.balanceOf(address(token)), 210); + + // bob does not get a reward but cashes out through the custodian + vm.prank(stakingPool); + underlyingToken.approve(address(token), 100); + vm.prank(stakingPool); + token.buyCustodialToken(bob, 100); + assertEq(token.balanceOf(bob), 100); + assertEq(underlyingToken.balanceOf(stakingPool), 190); + assertEq(underlyingToken.balanceOf(address(token)), 310); + vm.prank(bob); + token.release(); + assertEq(token.balanceOf(bob), 100); + assertEq(underlyingToken.balanceOf(bob), 0); + assertEq(underlyingToken.balanceOf(address(token)), 310); + + // time passes + vm.warp(block.timestamp + 101); + + // alice withdraws as much as she can + vm.prank(alice); + token.release(); + assertEq(token.balanceOf(alice), 10); + assertEq(underlyingToken.balanceOf(alice), 100); + assertEq(underlyingToken.balanceOf(address(token)), 210); + + // bob withdraws as much as he can + vm.prank(bob); + token.release(); + assertEq(token.balanceOf(bob), 0); + assertEq(underlyingToken.balanceOf(bob), 100); + assertEq(underlyingToken.balanceOf(address(token)), 110); + + // carol withdraws as much as she can, but it she doesn't have any because here funds are still staked + vm.prank(carol); + token.release(); + assertEq(token.balanceOf(carol), 0); + assertEq(underlyingToken.balanceOf(carol), 0); + assertEq(underlyingToken.balanceOf(address(token)), 110); + + // carol gets reward and cashes out through the custodian + vm.prank(stakingPool); + underlyingToken.approve(address(token), 110); + vm.prank(stakingPool); + token.buyCustodialToken(carol, 110); + assertEq(token.balanceOf(carol), 110); + assertEq(underlyingToken.balanceOf(stakingPool), 80); // spent 20 in total on rewards + assertEq(underlyingToken.balanceOf(address(token)), 220); + + // carol withdraws as much as she can + vm.prank(carol); + token.release(); + assertEq(token.balanceOf(carol), 10); + assertEq(underlyingToken.balanceOf(carol), 100); + assertEq(underlyingToken.balanceOf(address(token)), 120); + + // dave withdraws as much as he can + vm.prank(dave); + token.release(); + assertEq(token.balanceOf(dave), 0); + assertEq(underlyingToken.balanceOf(dave), 100); + assertEq(underlyingToken.balanceOf(address(token)), 20); + + // time passes + vm.warp(block.timestamp + 101); + + // alice withdraws as much as she can; she can withdraw her rewards + vm.prank(alice); + token.release(); + assertEq(token.balanceOf(alice), 0); + assertEq(underlyingToken.balanceOf(alice), 110); + assertEq(underlyingToken.balanceOf(address(token)), 10); + + // bob withdraws as much as he can; he can withdraw his rewards, but doesn't have any + vm.prank(bob); + token.release(); + assertEq(token.balanceOf(bob), 0); + assertEq(underlyingToken.balanceOf(bob), 100); + assertEq(underlyingToken.balanceOf(address(token)), 10); + + // carol withdraws as much as she can; she can't withdraw her rewards + vm.prank(carol); + token.release(); + assertEq(token.balanceOf(carol), 10); + assertEq(underlyingToken.balanceOf(carol), 100); + assertEq(underlyingToken.balanceOf(address(token)), 10); + } +}