Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[submodule "lib/common"]
path = lib/common
url = https://github.com/m0-foundation/common
branch = release-v1.5.0
[submodule "lib/openzeppelin-foundry-upgrades"]
path = lib/openzeppelin-foundry-upgrades
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
Expand Down
10 changes: 10 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ deploy-yield-to-one-forced-transfer-citrea: VERIFIER="custom"
deploy-yield-to-one-forced-transfer-citrea: VERIFIER_URL=${CITREA_VERIFIER_URL}
deploy-yield-to-one-forced-transfer-citrea: deploy-yield-to-one-forced-transfer

deploy-yield-to-one-forced-transfer-moca: RPC_URL=$(MOCA_RPC_URL)
deploy-yield-to-one-forced-transfer-moca: VERIFIER="blockscout"
deploy-yield-to-one-forced-transfer-moca: VERIFIER_URL=${MOCA_VERIFIER_URL}
deploy-yield-to-one-forced-transfer-moca: deploy-yield-to-one-forced-transfer

deploy-yield-to-one-forced-transfer-sepolia: RPC_URL=$(SEPOLIA_RPC_URL)
deploy-yield-to-one-forced-transfer-sepolia: VERIFIER="etherscan"
deploy-yield-to-one-forced-transfer-sepolia: VERIFIER_URL=${SEPOLIA_VERIFIER_URL}
Expand Down Expand Up @@ -268,6 +273,11 @@ deploy-swap-facility-sei: VERIFIER="etherscan"
deploy-swap-facility-sei: VERIFIER_URL=${SEI_VERIFIER_URL}
deploy-swap-facility-sei: deploy-swap-facility

deploy-swap-facility-0g: RPC_URL=$(ZG_RPC_URL)
deploy-swap-facility-0g: VERIFIER="custom"
deploy-swap-facility-0g: VERIFIER_URL=$(ZG_VERIFIER_URL)
deploy-swap-facility-0g: deploy-swap-facility

#
#
# UPGRADE
Expand Down
246 changes: 246 additions & 0 deletions broadcast/DeploySwapFacility.s.sol/16661/run-1771368196969.json

Large diffs are not rendered by default.

246 changes: 246 additions & 0 deletions broadcast/DeploySwapFacility.s.sol/16661/run-latest.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions deployments/16661.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extensionAddresses": [],
"extensionNames": [],
"swapAdapter": "0x0000000000000000000000000000000000000000",
"swapFacility": "0xB6807116b3B1B321a390594e31ECD6e0076f6278"
}
2 changes: 1 addition & 1 deletion lib/common
2 changes: 2 additions & 0 deletions script/Config.sol
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ contract Config {
uint256 public constant CITREA_CHAIN_ID = 4114;
uint256 public constant SEI_CHAIN_ID = 1329;
uint256 public constant LINEA_CHAIN_ID = 59144;
uint256 public constant ZG_CHAIN_ID = 16661;

// Testnet chain IDs
uint256 public constant LOCAL_CHAIN_ID = 31337;
Expand Down Expand Up @@ -138,6 +139,7 @@ contract Config {
if (chainId_ == CITREA_CHAIN_ID) return _getDefaultDeployConfig();
if (chainId_ == SEI_CHAIN_ID) return _getDefaultDeployConfig();
if (chainId_ == LINEA_CHAIN_ID) return _getDefaultDeployConfig();
if (chainId_ == ZG_CHAIN_ID) return _getDefaultDeployConfig();

// Testnet configs
if (chainId_ == LOCAL_CHAIN_ID) return _getDefaultDeployConfig();
Expand Down
2 changes: 1 addition & 1 deletion script/deploy/DeployBase.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ contract DeployBase is DeployHelpers, ScriptBase {

proxy = _deployCreate3TransparentProxy(
implementation,
extensionConfig.admin,
0x8Cfac65f5621D699f9efAa84DDaff2A88eeEa405, // Moonpay Proxy Admin
abi.encodeWithSelector(
MYieldToOneForcedTransfer.initialize.selector,
extensionConfig.extensionName,
Expand Down
2 changes: 1 addition & 1 deletion src/components/forcedTransferable/ForcedTransferable.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.26;
pragma solidity ^0.8.26;

import { AccessControlUpgradeable } from "../../../lib/common/lib/openzeppelin-contracts-upgradeable/contracts/access/AccessControlUpgradeable.sol";

Expand Down
2 changes: 1 addition & 1 deletion src/components/forcedTransferable/IForcedTransferable.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: BUSL-1.1

pragma solidity 0.8.26;
pragma solidity ^0.8.26;

interface IForcedTransferable {
/* ============ Events ============ */
Expand Down
19 changes: 18 additions & 1 deletion src/components/freezable/Freezable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ abstract contract Freezable is IFreezable, FreezableStorageLayout, AccessControl
/// @inheritdoc IFreezable
function freezeAccounts(address[] calldata accounts) external virtual onlyRole(FREEZE_MANAGER_ROLE) {
FreezableStorageStruct storage $ = _getFreezableStorageLocation();

for (uint256 i; i < accounts.length; ++i) {
_freeze($, accounts[i]);
}
Expand All @@ -83,6 +82,20 @@ abstract contract Freezable is IFreezable, FreezableStorageLayout, AccessControl
return _getFreezableStorageLocation().isFrozen[account];
}

/* ============ Hooks For Internal Interactive Functions ============ */

/**
* @dev Hook called before freezing an account.
* @param account The account to be frozen.
*/
function _beforeFreeze(address account) internal virtual {}

/**
* @dev Hook called before unfreezing an account.
* @param account The account to be unfrozen.
*/
function _beforeUnfreeze(address account) internal virtual {}

/* ============ Internal Interactive Functions ============ */

/**
Expand All @@ -94,6 +107,8 @@ abstract contract Freezable is IFreezable, FreezableStorageLayout, AccessControl
// Return early if the account is already frozen
if ($.isFrozen[account]) return;

_beforeFreeze(account);

$.isFrozen[account] = true;

emit Frozen(account, block.timestamp);
Expand All @@ -108,6 +123,8 @@ abstract contract Freezable is IFreezable, FreezableStorageLayout, AccessControl
// Return early if the account is not frozen
if (!$.isFrozen[account]) return;

_beforeUnfreeze(account);

$.isFrozen[account] = false;

emit Unfrozen(account, block.timestamp);
Expand Down
14 changes: 14 additions & 0 deletions src/components/pausable/Pausable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,25 @@ abstract contract Pausable is IPausable, AccessControlUpgradeable, PausableUpgra

/// @inheritdoc IPausable
function pause() external onlyRole(PAUSER_ROLE) {
_beforePause();
_pause();
}

/// @inheritdoc IPausable
function unpause() external onlyRole(PAUSER_ROLE) {
_beforeUnpause();
_unpause();
}

/* ============ Hooks For Internal Interactive Functions ============ */

/**
* @dev Hook called before pausing the contract.
*/
function _beforePause() internal virtual {}

/**
* @dev Hook called before unpausing the contract.
*/
function _beforeUnpause() internal virtual {}
}
23 changes: 23 additions & 0 deletions src/projects/yieldToOne/MYieldToOneForcedTransfer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

pragma solidity 0.8.26;

import { IMYieldToOne } from "./interfaces/IMYieldToOne.sol";

import { MYieldToOne } from "./MYieldToOne.sol";
import { ForcedTransferable } from "../../components/forcedTransferable/ForcedTransferable.sol";

Expand Down Expand Up @@ -88,6 +90,27 @@ contract MYieldToOneForcedTransfer is MYieldToOne, ForcedTransferable {
__ForcedTransferable_init(forcedTransferManager);
}

/* ============ Interactive Functions ============ */

/**
* @inheritdoc IMYieldToOne
* @dev Reverts if the contract is paused.
*/
function claimYield() public override returns (uint256) {
_requireNotPaused();
return super.claimYield();
}

/**
* @inheritdoc IMYieldToOne
* @dev Only updates the yield recipient address; does not claim pending yield for the previous recipient.
*/
function setYieldRecipient(address account) external override onlyRole(YIELD_RECIPIENT_MANAGER_ROLE) {
_setYieldRecipient(account);
}

/* ============ Internal Interactive Functions ============ */

/**
* @dev Internal ERC20 force transfer function to seize funds from a frozen account.
* @param frozenAccount The frozen account from which tokens are seized.
Expand Down
118 changes: 118 additions & 0 deletions test/unit/projects/yieldToOne/MYieldToOneForcedTransfer.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ pragma solidity 0.8.26;
import { IERC20 } from "../../../../lib/common/src/interfaces/IERC20.sol";

import { IAccessControl } from "../../../../lib/common/lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/access/IAccessControl.sol";
import { PausableUpgradeable } from "../../../../lib/common/lib/openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol";

import { Upgrades, UnsafeUpgrades } from "../../../../lib/openzeppelin-foundry-upgrades/src/Upgrades.sol";

import { MYieldToOneForcedTransfer } from "../../../../src/projects/yieldToOne/MYieldToOneForcedTransfer.sol";
import { IMYieldToOne } from "../../../../src/projects/yieldToOne/interfaces/IMYieldToOne.sol";

import { IForcedTransferable } from "../../../../src/components/forcedTransferable/IForcedTransferable.sol";
import { IFreezable } from "../../../../src/components/freezable/IFreezable.sol";
Expand Down Expand Up @@ -345,4 +347,120 @@ contract MYieldToOneForcedTransferUnitTest is BaseUnitTest {
}
}
}

/* ============ claimYield ============ */

function test_claimYield_noYield() external {
vm.prank(alice);
uint256 yield = mYieldToOneForcedTransfer.claimYield();

assertEq(yield, 0);
}

function test_claimYield() external {
uint256 yield = 500e6;

mToken.setBalanceOf(address(mYieldToOneForcedTransfer), mYieldToOneForcedTransfer.totalSupply() + yield);

assertEq(mYieldToOneForcedTransfer.yield(), yield);

vm.expectEmit();
emit IMYieldToOne.YieldClaimed(yield);

assertEq(mYieldToOneForcedTransfer.claimYield(), yield);

assertEq(mYieldToOneForcedTransfer.yield(), 0);
assertEq(mYieldToOneForcedTransfer.balanceOf(yieldRecipient), yield);
}

function test_claimYield_paused() external {
mToken.setBalanceOf(address(mYieldToOneForcedTransfer), mYieldToOneForcedTransfer.totalSupply() + 500e6);

vm.prank(pauser);
mYieldToOneForcedTransfer.pause();

vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
mYieldToOneForcedTransfer.claimYield();
}

/* ============ setYieldRecipient ============ */

function test_setYieldRecipient_onlyYieldRecipientManager() public {
vm.expectRevert(
abi.encodeWithSelector(
IAccessControl.AccessControlUnauthorizedAccount.selector,
alice,
YIELD_RECIPIENT_MANAGER_ROLE
)
);

vm.prank(alice);
mYieldToOneForcedTransfer.setYieldRecipient(alice);
}

function test_setYieldRecipient_zeroYieldRecipient() public {
vm.expectRevert(IMYieldToOne.ZeroYieldRecipient.selector);

vm.prank(yieldRecipientManager);
mYieldToOneForcedTransfer.setYieldRecipient(address(0));
}

function test_setYieldRecipient_noUpdate() public {
assertEq(mYieldToOneForcedTransfer.yieldRecipient(), yieldRecipient);

vm.prank(yieldRecipientManager);
mYieldToOneForcedTransfer.setYieldRecipient(yieldRecipient);

assertEq(mYieldToOneForcedTransfer.yieldRecipient(), yieldRecipient);
}

function test_setYieldRecipient() public {
assertEq(mYieldToOneForcedTransfer.yieldRecipient(), yieldRecipient);

vm.expectEmit();
emit IMYieldToOne.YieldRecipientSet(alice);

vm.prank(yieldRecipientManager);
mYieldToOneForcedTransfer.setYieldRecipient(alice);

assertEq(mYieldToOneForcedTransfer.yieldRecipient(), alice);
}

function test_setYieldRecipient_doesNotClaimYield() public {
uint256 accruedYield = 500;

// Accrue yield for the previous recipient.
mToken.setBalanceOf(address(mYieldToOneForcedTransfer), mYieldToOneForcedTransfer.totalSupply() + accruedYield);

assertEq(mYieldToOneForcedTransfer.yield(), accruedYield);

vm.expectEmit();
emit IMYieldToOne.YieldRecipientSet(alice);

vm.prank(yieldRecipientManager);
mYieldToOneForcedTransfer.setYieldRecipient(alice);

// Recipient is updated.
assertEq(mYieldToOneForcedTransfer.yieldRecipient(), alice);

// Previously accrued yield is NOT claimed: it remains with the contract,
// neither recipient receives a mint, and yield() still reflects the full amount.
assertEq(mYieldToOneForcedTransfer.yield(), accruedYield);
assertEq(mYieldToOneForcedTransfer.balanceOf(yieldRecipient), 0);
assertEq(mYieldToOneForcedTransfer.balanceOf(alice), 0);
}

function test_setYieldRecipient_paused() public {
vm.prank(pauser);
mYieldToOneForcedTransfer.pause();

vm.expectEmit();
emit IMYieldToOne.YieldRecipientSet(alice);

vm.prank(yieldRecipientManager);
mYieldToOneForcedTransfer.setYieldRecipient(alice);

// Recipient update is independent of pause state since claimYield is no longer invoked.
assertEq(mYieldToOneForcedTransfer.yieldRecipient(), alice);
}
}
Loading