Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
[submodule "lib/sdai"]
path = lib/sdai
url = https://github.com/makerdao/sdai
[submodule "lib/spark-address-registry"]
path = lib/spark-address-registry
url = https://github.com/marsfoundation/spark-address-registry
[submodule "lib/erc20-helpers"]
path = lib/erc20-helpers
url = https://github.com/marsfoundation/erc20-helpers
Expand All @@ -34,3 +31,6 @@
[submodule "lib/aave-v3-origin"]
path = lib/aave-v3-origin
url = https://github.com/aave-dao/aave-v3-origin
[submodule "lib/spark-address-registry"]
path = lib/spark-address-registry
url = https://github.com/sparkdotfi/spark-address-registry
27 changes: 7 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,12 @@ The diagram below provides and example of calling to mint USDS using the Sky all
All contracts in this repo inherit and implement the AccessControl contract from OpenZeppelin to manage permissions. The following roles are defined:
- `DEFAULT_ADMIN_ROLE`: The admin role is the role that can grant and revoke roles. Also used for general admin functions in all contracts.
- `RELAYER`: Used for the ALM Planner offchain system. This address can call functions on `controller` contracts to perform actions on behalf of the `ALMProxy` contract.
- `FREEZER`: Allows an address with this role to freeze all actions on the `controller` contracts. This role is intended to be used in emergency situations.
- `FREEZER`: Allows an address with this role to remove a `RELAYER` that has been compromised. The intention of this is to have a backup `RELAYER` that the system can fall back to when the main one is removed.
- `CONTROLLER`: Used for the `ALMProxy` contract. Only contracts with this role can call the `call` functions on the `ALMProxy` contract. Also used in the RateLimits contract, only this role can update rate limits.

## Controller Functionality
All functions below change the balance of funds in the ALMProxy contract and are only callable by the `RELAYER` role.

- `ForeignController`: This contract currently implements logic to:
- Deposit and withdraw on EVM compliant L2 PSM3 contracts (see [spark-psm](https://github.com/marsfoundation/spark-psm) for implementation).
- Initiate a transfer of USDC to other domains using CCTP.
- Deposit, withdraw, and redeem from ERC4626 contracts.
- Deposit and withdraw from AAVE.
- `MainnetController`: This contract currently implements logic to:
- Mint and burn USDS.
- Deposit, withdraw, redeem from ERC4626 contracts.
- Deposit and withdraw from AAVE.
- Mint and burn USDe.
- Cooldown and unstake from sUSDe.
- Swap USDS to USDC and vice versa using the mainnet PSM.
- Transfer USDC to other domains using CCTP.
The `MainnetController` contains all logic necessary to interact with the Sky allocation system to mint and burn USDS, swap USDS to USDC in the PSM, as well as interact with mainnet external protocols and CCTP for bridging USDC.
The `ForeignController` contains all logic necessary to deposit, withdraw, and swap assets in L2 PSMs as well as interact with external protocols on L2s and CCTP for bridging USDC.

## Rate Limits

Expand All @@ -79,8 +66,8 @@ Below are all stated trust assumptions for using this contract in production:
- The `RELAYER` role is assumed to be able to be fully compromised by a malicious actor. **This should be a major consideration during auditing engagements.**
- The logic in the smart contracts must prevent the movement of value anywhere outside of the ALM system of contracts.
- Any action must be limited to "reasonable" slippage/losses/opportunity cost by rate limits.
- The `FREEZER` must be able to stop the compromised `RELAYER` from performing more harmful actions within the max rate limits by using the `freeze()` function.
- A compromised `RELAYER` can DOS Ethena unstaking, but this can be mitigated by freezing the Controller and reassigning the `RELAYER`. This is outlined in a test `test_compromisedRelayer_lockingFundsInEthenaSilo`.
- The `FREEZER` must be able to stop the compromised `RELAYER` from performing more harmful actions within the max rate limits by using the `removeRelayer` function.
- A compromised `RELAYER` can perform DOS attacks. These attacks along with their respective recovery procedures are outlined in the `Attacks.t.sol` test files.
- Ethena USDe Mint/Burn is trusted to not honor requests with over 50bps slippage from a delegated signer.

## Operational Requirements
Expand All @@ -100,9 +87,9 @@ forge test
```

## Deployments
All commands to deploy:
All commands to deploy:
- Either the full system or just the controller
- To mainnet or base
- To mainnet or base
- For staging or production

Can be found in the Makefile, with the nomenclature `make deploy-<domain>-<env>-<type>`.
Expand Down
10 changes: 4 additions & 6 deletions deploy/ForeignControllerInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ library ForeignControllerInit {
)
internal
{
_initController(controllerInst, configAddresses, checkAddresses, mintRecipients);
_initController(controllerInst, configAddresses, checkAddresses, mintRecipients);

IALMProxy almProxy = IALMProxy(controllerInst.almProxy);
IRateLimits rateLimits = IRateLimits(controllerInst.rateLimits);

require(configAddresses.oldController != address(0), "ForeignControllerInit/old-controller-zero-address");
require(configAddresses.oldController != address(0), "ForeignControllerInit/old-controller-zero-address");

require(almProxy.hasRole(almProxy.CONTROLLER(), configAddresses.oldController), "ForeignControllerInit/old-controller-not-almProxy-controller");
require(rateLimits.hasRole(rateLimits.CONTROLLER(), configAddresses.oldController), "ForeignControllerInit/old-controller-not-rateLimits-controller");
Expand All @@ -98,7 +98,7 @@ library ForeignControllerInit {
CheckAddressParams memory checkAddresses,
MintRecipient[] memory mintRecipients
)
private
private
{
// Step 1: Perform controller sanity checks

Expand All @@ -113,8 +113,6 @@ library ForeignControllerInit {
require(address(newController.usdc()) == checkAddresses.usdc, "ForeignControllerInit/incorrect-usdc");
require(address(newController.cctp()) == checkAddresses.cctp, "ForeignControllerInit/incorrect-cctp");

require(newController.active(), "ForeignControllerInit/controller-not-active");

require(configAddresses.oldController != address(newController), "ForeignControllerInit/old-controller-is-new-controller");

// Step 2: Perform PSM sanity checks
Expand Down
11 changes: 5 additions & 6 deletions deploy/MainnetControllerInit.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ library MainnetControllerInit {
/**********************************************************************************************/

function initAlmSystem(
address vault,
address vault,
address usds,
ControllerInstance memory controllerInst,
ConfigAddressParams memory configAddresses,
Expand Down Expand Up @@ -89,12 +89,12 @@ library MainnetControllerInit {
)
internal
{
_initController(controllerInst, configAddresses, checkAddresses, mintRecipients);
_initController(controllerInst, configAddresses, checkAddresses, mintRecipients);

IALMProxy almProxy = IALMProxy(controllerInst.almProxy);
IRateLimits rateLimits = IRateLimits(controllerInst.rateLimits);

require(configAddresses.oldController != address(0), "MainnetControllerInit/old-controller-zero-address");
require(configAddresses.oldController != address(0), "MainnetControllerInit/old-controller-zero-address");

require(almProxy.hasRole(almProxy.CONTROLLER(), configAddresses.oldController), "MainnetControllerInit/old-controller-not-almProxy-controller");
require(rateLimits.hasRole(rateLimits.CONTROLLER(), configAddresses.oldController), "MainnetControllerInit/old-controller-not-rateLimits-controller");
Expand All @@ -117,7 +117,7 @@ library MainnetControllerInit {
CheckAddressParams memory checkAddresses,
MintRecipient[] memory mintRecipients
)
private
private
{
// Step 1: Perform controller sanity checks

Expand All @@ -134,7 +134,6 @@ library MainnetControllerInit {
require(address(newController.cctp()) == checkAddresses.cctp, "MainnetControllerInit/incorrect-cctp");

require(newController.psmTo18ConversionFactor() == 1e12, "MainnetControllerInit/incorrect-psmTo18ConversionFactor");
require(newController.active(), "MainnetControllerInit/controller-not-active");

require(configAddresses.oldController != address(newController), "MainnetControllerInit/old-controller-is-new-controller");

Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fs_permissions = [
{ access = "read", path = "./script/input/"},
{ access = "read-write", path = "./script/output/"}
]
evm_version = 'shanghai'
evm_version = 'cancun'

[fuzz]
runs = 1000
Expand Down
2 changes: 1 addition & 1 deletion lib/spark-address-registry
39 changes: 22 additions & 17 deletions script/staging/test/StagingDeployment.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ contract MainnetStagingDeploymentTests is StagingDeploymentTestBase {
mainnetController.withdrawERC4626(Ethereum.SUSDS, 10e18);
vm.stopPrank();

assertEq(usds.balanceOf(address(almProxy)), startingBalance + 10e18);
assertEq(usds.balanceOf(address(almProxy)), startingBalance + 10e18);

assertGe(IERC4626(Ethereum.SUSDS).balanceOf(address(almProxy)), 0); // Interest earned
}
Expand All @@ -214,7 +214,7 @@ contract MainnetStagingDeploymentTests is StagingDeploymentTestBase {

assertGe(usds.balanceOf(address(almProxy)), startingBalance + 10e18); // Interest earned

assertEq(IERC4626(Ethereum.SUSDS).balanceOf(address(almProxy)), 0);
assertEq(IERC4626(Ethereum.SUSDS).balanceOf(address(almProxy)), 0);
}

function test_depositAndWithdrawUsdsFromAave() public {
Expand Down Expand Up @@ -266,20 +266,20 @@ contract MainnetStagingDeploymentTests is StagingDeploymentTestBase {

_simulateUsdeBurn(10e18 - 1);

assertEq(usdc.balanceOf(address(almProxy)), startingBalance + 10e6 - 1); // Rounding not captured
assertEq(usdc.balanceOf(address(almProxy)), startingBalance + 10e6 - 1); // Rounding not captured

assertGe(IERC4626(Ethereum.SUSDE).balanceOf(address(almProxy)), 0); // Interest earned
}

function test_mintDepositCooldownSharesBurnUsde() public {
uint256 startingBalance = usdc.balanceOf(address(almProxy));

vm.startPrank(relayerSafe);
mainnetController.mintUSDS(10e18);
mainnetController.swapUSDSToUSDC(10e6);
mainnetController.prepareUSDeMint(10e6);
vm.stopPrank();

uint256 startingBalance = usdc.balanceOf(address(almProxy));

_simulateUsdeMint(10e6);

vm.startPrank(relayerSafe);
Expand All @@ -288,23 +288,28 @@ contract MainnetStagingDeploymentTests is StagingDeploymentTestBase {
uint256 usdeAmount = mainnetController.cooldownSharesSUSDe(IERC4626(Ethereum.SUSDE).balanceOf(address(almProxy)));
skip(7 days);
mainnetController.unstakeSUSDe();
mainnetController.prepareUSDeBurn(usdeAmount);

// Handle situation where usde balance of ALM Proxy is higher than max rate limit
uint256 maxBurnAmount = rateLimits.getCurrentRateLimit(mainnetController.LIMIT_USDE_BURN());
uint256 burnAmount = usdeAmount > maxBurnAmount ? maxBurnAmount : usdeAmount;
mainnetController.prepareUSDeBurn(burnAmount);

vm.stopPrank();

_simulateUsdeBurn(usdeAmount);
_simulateUsdeBurn(burnAmount);

assertGe(usdc.balanceOf(address(almProxy)), startingBalance - 1); // Interest earned (rounding)

assertGe(usdc.balanceOf(address(almProxy)), startingBalance + 10e6 - 1); // Interest earned (rounding)

assertEq(IERC4626(Ethereum.SUSDE).balanceOf(address(almProxy)), 0);
assertEq(IERC4626(Ethereum.SUSDE).balanceOf(address(almProxy)), 0);
}

/**********************************************************************************************/
/**** Helper functions ***/
/**********************************************************************************************/

// NOTE: In reality these actions are performed by the signer submitting an order with an
// EIP712 signature which is verified by the ethenaMinter contract,
// minting/burning USDe into the ALMProxy. Also, for the purposes of this test,
// NOTE: In reality these actions are performed by the signer submitting an order with an
// EIP712 signature which is verified by the ethenaMinter contract,
// minting/burning USDe into the ALMProxy. Also, for the purposes of this test,
// minting/burning is done 1:1 with USDC.

// TODO: Try doing ethena minting with EIP-712 signatures (vm.sign)
Expand All @@ -313,8 +318,8 @@ contract MainnetStagingDeploymentTests is StagingDeploymentTestBase {
vm.prank(Ethereum.ETHENA_MINTER);
usdc.transferFrom(address(almProxy), Ethereum.ETHENA_MINTER, amount);
deal(
Ethereum.USDE,
address(almProxy),
Ethereum.USDE,
address(almProxy),
IERC20(Ethereum.USDE).balanceOf(address(almProxy)) + amount * 1e12
);
}
Expand Down Expand Up @@ -496,7 +501,7 @@ contract BaseStagingDeploymentTests is StagingDeploymentTestBase {

assertGe(usdcBase.balanceOf(address(baseAlmProxy)), 10e6); // Interest earned

assertEq(IERC20(MORPHO_VAULT_USDC).balanceOf(address(baseAlmProxy)), 0);
assertEq(IERC20(MORPHO_VAULT_USDC).balanceOf(address(baseAlmProxy)), 0);

baseController.transferUSDCToCCTP(1e6 - 1, CCTPForwarder.DOMAIN_ID_CIRCLE_ETHEREUM); // Account for potential rounding
vm.stopPrank();
Expand Down
Loading
Loading