Skip to content

Commit

Permalink
feat: pausability (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
sakulstra authored Aug 8, 2024
1 parent a8e061a commit 4ad98c0
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 68 deletions.
41 changes: 22 additions & 19 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ test = 'tests'
script = 'scripts'
optimizer = true
optimizer_runs = 200
solc='0.8.19'
solc = '0.8.20'
evm_version = 'paris'
bytecode_hash = 'none'
out = 'out'
libs = ['lib']
remappings = [
remappings = []
fs_permissions = [
{ access = "write", path = "./reports" },
{ access = "read", path = "./out" },
{ access = "read", path = "./config" },
]
fs_permissions = [{access = "write", path = "./reports"}, {access = "read", path = "./out" }, {access = "read", path = "./config"}]
ffi = true

[fuzz]
Expand All @@ -25,7 +28,7 @@ avalanche = "${RPC_AVALANCHE}"
polygon = "${RPC_POLYGON}"
arbitrum = "${RPC_ARBITRUM}"
fantom = "${RPC_FANTOM}"
scroll= "${RPC_SCROLL}"
scroll = "${RPC_SCROLL}"
celo = "${RPC_CELO}"
fantom_testnet = "${RPC_FANTOM_TESTNET}"
harmony = "${RPC_HARMONY}"
Expand All @@ -38,19 +41,19 @@ gnosis = "${RPC_GNOSIS}"
base = "${RPC_BASE}"

[etherscan]
mainnet={key="${ETHERSCAN_API_KEY_MAINNET}",chainId=1}
optimism={key="${ETHERSCAN_API_KEY_OPTIMISM}",chainId=10}
avalanche={key="${ETHERSCAN_API_KEY_AVALANCHE}",chainId=43114}
polygon={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=137}
arbitrum={key="${ETHERSCAN_API_KEY_ARBITRUM}",chainId=42161}
fantom={key="${ETHERSCAN_API_KEY_FANTOM}",chainId=250}
scroll={key="${ETHERSCAN_API_KEY_SCROLL}",chainId=534352, url='https://api.scrollscan.com/api\?'}
celo={key="${ETHERSCAN_API_KEY_CELO}",chainId=42220}
sepolia={key="${ETHERSCAN_API_KEY_MAINNET}",chainId=11155111}
mumbai={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=80001}
amoy={key="${ETHERSCAN_API_KEY_POLYGON}",chainId=80002}
bnb_testnet={key="${ETHERSCAN_API_KEY_BNB}",chainId=97,url='https://api-testnet.bscscan.com/api'}
bnb={key="${ETHERSCAN_API_KEY_BNB}",chainId=56,url='https://api.bscscan.com/api'}
base={key="${ETHERSCAN_API_KEY_BASE}",chain=8453}
gnosis={key="${ETHERSCAN_API_KEY_GNOSIS}",chainId=100}
mainnet = { key = "${ETHERSCAN_API_KEY_MAINNET}", chainId = 1 }
optimism = { key = "${ETHERSCAN_API_KEY_OPTIMISM}", chainId = 10 }
avalanche = { key = "${ETHERSCAN_API_KEY_AVALANCHE}", chainId = 43114 }
polygon = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 137 }
arbitrum = { key = "${ETHERSCAN_API_KEY_ARBITRUM}", chainId = 42161 }
fantom = { key = "${ETHERSCAN_API_KEY_FANTOM}", chainId = 250 }
scroll = { key = "${ETHERSCAN_API_KEY_SCROLL}", chainId = 534352, url = 'https://api.scrollscan.com/api\?' }
celo = { key = "${ETHERSCAN_API_KEY_CELO}", chainId = 42220 }
sepolia = { key = "${ETHERSCAN_API_KEY_MAINNET}", chainId = 11155111 }
mumbai = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 80001 }
amoy = { key = "${ETHERSCAN_API_KEY_POLYGON}", chainId = 80002 }
bnb_testnet = { key = "${ETHERSCAN_API_KEY_BNB}", chainId = 97, url = 'https://api-testnet.bscscan.com/api' }
bnb = { key = "${ETHERSCAN_API_KEY_BNB}", chainId = 56, url = 'https://api.bscscan.com/api' }
base = { key = "${ETHERSCAN_API_KEY_BASE}", chain = 8453 }
gnosis = { key = "${ETHERSCAN_API_KEY_GNOSIS}", chainId = 100 }
# See more config options https://github.com/gakonst/foundry/tree/master/config
12 changes: 12 additions & 0 deletions src/periphery/contracts/static-a-token/DeprecationGap.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

/**
* This contract adds a single slot gap
* The slot is required to account for the now deprecated Initializable.
* The new version of Initializable uses erc7201, so it no longer occupies the first slot.
* https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/proxy/utils/Initializable.sol#L60
*/
contract DeprecationGap {
uint256 internal __deprecated_initializable_gap;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {IStaticATokenFactory} from './interfaces/IStaticATokenFactory.sol';
*/
contract StaticATokenFactory is Initializable, IStaticATokenFactory {
IPool public immutable POOL;
address public immutable ADMIN;
address public immutable PROXY_ADMIN;
ITransparentProxyFactory public immutable TRANSPARENT_PROXY_FACTORY;
address public immutable STATIC_A_TOKEN_IMPL;

Expand All @@ -33,7 +33,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory {
address staticATokenImpl
) {
POOL = pool;
ADMIN = proxyAdmin;
PROXY_ADMIN = proxyAdmin;
TRANSPARENT_PROXY_FACTORY = transparentProxyFactory;
STATIC_A_TOKEN_IMPL = staticATokenImpl;
}
Expand All @@ -54,7 +54,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory {
);
address staticAToken = TRANSPARENT_PROXY_FACTORY.createDeterministic(
STATIC_A_TOKEN_IMPL,
ADMIN,
PROXY_ADMIN,
abi.encodeWithSelector(
StaticATokenLM.initialize.selector,
reserveData.aTokenAddress,
Expand All @@ -63,6 +63,7 @@ contract StaticATokenFactory is Initializable, IStaticATokenFactory {
),
bytes32(uint256(uint160(underlyings[i])))
);

_underlyingToStaticAToken[underlyings[i]] = staticAToken;
staticATokens[i] = staticAToken;
_staticATokens.push(staticAToken);
Expand Down
31 changes: 25 additions & 6 deletions src/periphery/contracts/static-a-token/StaticATokenLM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ pragma solidity ^0.8.10;

import {IPool} from '../../../core/contracts/interfaces/IPool.sol';
import {DataTypes, ReserveConfiguration} from '../../../core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol';
import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol';
import {WadRayMath} from '../../../core/contracts/protocol/libraries/math/WadRayMath.sol';
import {MathUtils} from '../../../core/contracts/protocol/libraries/math/MathUtils.sol';
import {IACLManager} from '../../../core/contracts/interfaces/IACLManager.sol';
import {IRewardsController} from '../rewards/interfaces/IRewardsController.sol';
import {SafeCast} from 'solidity-utils/contracts/oz-common/SafeCast.sol';
import {Initializable} from 'solidity-utils/contracts/transparent-proxy/Initializable.sol';
import {SafeERC20} from 'solidity-utils/contracts/oz-common/SafeERC20.sol';
import {IERC20Metadata} from 'solidity-utils/contracts/oz-common/interfaces/IERC20Metadata.sol';
import {IERC20} from 'solidity-utils/contracts/oz-common/interfaces/IERC20.sol';
Expand All @@ -21,6 +21,8 @@ import {IInitializableStaticATokenLM} from './interfaces/IInitializableStaticATo
import {StaticATokenErrors} from './StaticATokenErrors.sol';
import {RayMathExplicitRounding, Rounding} from '../libraries/RayMathExplicitRounding.sol';
import {IERC4626} from './interfaces/IERC4626.sol';
import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol';
import {DeprecationGap} from './DeprecationGap.sol';

/**
* @title StaticATokenLM
Expand All @@ -30,10 +32,11 @@ import {IERC4626} from './interfaces/IERC4626.sol';
* @author BGD labs
*/
contract StaticATokenLM is
Initializable,
DeprecationGap,
ERC20('STATIC__aToken_IMPL', 'STATIC__aToken_IMPL', 18),
IStaticATokenLM,
Rescuable
Rescuable,
PausableUpgradeable
{
using SafeERC20 for IERC20;
using SafeCast for uint256;
Expand Down Expand Up @@ -61,10 +64,20 @@ contract StaticATokenLM is
mapping(address => mapping(address => UserRewardsData)) internal _userRewardsData;

constructor(IPool pool, IRewardsController rewardsController) {
_disableInitializers();
POOL = pool;
INCENTIVES_CONTROLLER = rewardsController;
}

modifier onlyPauseGuardian() {
if (!canPause(msg.sender)) revert OnlyPauseGuardian(msg.sender);
_;
}

function canPause(address actor) public view returns (bool) {
return IACLManager(POOL.ADDRESSES_PROVIDER().getACLManager()).isEmergencyAdmin(actor);
}

///@inheritdoc IInitializableStaticATokenLM
function initialize(
address newAToken,
Expand Down Expand Up @@ -93,6 +106,12 @@ contract StaticATokenLM is
return POOL.ADDRESSES_PROVIDER().getACLAdmin();
}

///@inheritdoc IStaticATokenLM
function setPaused(bool paused) external onlyPauseGuardian {
if (paused) _pause();
else _unpause();
}

///@inheritdoc IStaticATokenLM
function refreshRewardTokens() public override {
address[] memory rewards = INCENTIVES_CONTROLLER.getRewardsByAsset(address(_aToken));
Expand Down Expand Up @@ -540,7 +559,7 @@ contract StaticATokenLM is
* @param from The address of the sender of tokens
* @param to The address of the receiver of tokens
*/
function _beforeTokenTransfer(address from, address to, uint256) internal override {
function _beforeTokenTransfer(address from, address to, uint256) internal override whenNotPaused {
for (uint256 i = 0; i < _rewardTokens.length; i++) {
address rewardToken = address(_rewardTokens[i]);
uint256 rewardsIndex = getCurrentRewardsIndex(rewardToken);
Expand Down Expand Up @@ -633,7 +652,7 @@ contract StaticATokenLM is
address onBehalfOf,
address receiver,
address[] memory rewards
) internal {
) internal whenNotPaused {
for (uint256 i = 0; i < rewards.length; i++) {
if (address(rewards[i]) == address(0)) {
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 {
uint248 lastUpdatedIndex;
}

error OnlyPauseGuardian(address caller);

event RewardTokenRegistered(address indexed reward, uint256 startIndex);

/**
Expand Down Expand Up @@ -207,7 +209,14 @@ interface IStaticATokenLM is IInitializableStaticATokenLM, IERC4626 {

/**
* @notice Checks if the passed token is a registered reward.
* @param reward The reward to claim
* @return bool signaling if token is a registered reward.
*/
function isRegisteredRewardToken(address reward) external view returns (bool);

/**
* @notice Pauses/unpauses all system's operations
* @param paused boolean determining if the token should be paused or unpaused
*/
function setPaused(bool paused) external;
}
125 changes: 125 additions & 0 deletions tests/periphery/static-a-token/Pausable.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

import {UpgradableOwnableWithGuardian} from 'solidity-utils/contracts/access-control/UpgradableOwnableWithGuardian.sol';
import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol';
import {AToken} from '../../../src/core/contracts/protocol/tokenization/AToken.sol';
import {DataTypes} from '../../../src/core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol';
import {IERC20, IERC20Metadata} from '../../../src/periphery/contracts/static-a-token/StaticATokenLM.sol';
import {RayMathExplicitRounding} from '../../../src/periphery/contracts/libraries/RayMathExplicitRounding.sol';
import {PullRewardsTransferStrategy} from '../../../src/periphery/contracts/rewards/transfer-strategies/PullRewardsTransferStrategy.sol';
import {RewardsDataTypes} from '../../../src/periphery/contracts/rewards/libraries/RewardsDataTypes.sol';
import {ITransferStrategyBase} from '../../../src/periphery/contracts/rewards/interfaces/ITransferStrategyBase.sol';
import {IEACAggregatorProxy} from '../../../src/periphery/contracts/misc/interfaces/IEACAggregatorProxy.sol';
import {IStaticATokenLM} from '../../../src/periphery/contracts/static-a-token/interfaces/IStaticATokenLM.sol';
import {SigUtils} from '../../utils/SigUtils.sol';
import {BaseTest, TestnetERC20} from './TestBase.sol';

contract Pausable is BaseTest {
using RayMathExplicitRounding for uint256;

function test_setPaused_shouldRevertForInvalidCaller(address actor) external {
vm.assume(actor != poolAdmin && actor != proxyAdmin);
vm.expectRevert(abi.encodeWithSelector(IStaticATokenLM.OnlyPauseGuardian.selector, actor));
_setPaused(actor, true);
}

function test_setPaused_shouldSuceedForOwner() external {
assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), false);
_setPaused(poolAdmin, true);
assertEq(PausableUpgradeable(address(staticATokenLM)).paused(), true);
}

function test_deposit_shouldRevert() external {
vm.startPrank(user);
uint128 amountToDeposit = 5 ether;
_fundUser(amountToDeposit, user);
IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit);
vm.stopPrank();

_setPausedAsAclAdmin(true);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.deposit(amountToDeposit, user, 0, true);
}

function test_mint_shouldRevert() external {
vm.startPrank(user);
uint128 amountToDeposit = 5 ether;
_fundUser(amountToDeposit, user);
IERC20(UNDERLYING).approve(address(staticATokenLM), amountToDeposit);
vm.stopPrank();

uint256 sharesToMint = staticATokenLM.previewDeposit(amountToDeposit);
_setPausedAsAclAdmin(true);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.mint(sharesToMint, user);
}

function test_redeem_shouldRevert() external {
uint128 amountToDeposit = 5 ether;
vm.startPrank(user);
_fundUser(amountToDeposit, user);
_depositAToken(amountToDeposit, user);
vm.stopPrank();

assertEq(staticATokenLM.maxRedeem(user), staticATokenLM.balanceOf(user));

_setPausedAsAclAdmin(true);
uint256 maxRedeem = staticATokenLM.maxRedeem(user);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.redeem(maxRedeem, user, user);
}

function test_withdraw_shouldRevert() external {
uint128 amountToDeposit = 5 ether;
vm.startPrank(user);
_fundUser(amountToDeposit, user);
_depositAToken(amountToDeposit, user);
vm.stopPrank();

uint256 maxWithdraw = staticATokenLM.maxWithdraw(user);
_setPausedAsAclAdmin(true);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.withdraw(maxWithdraw, user, user);
}

function test_transfer_shouldRevert() external {
uint128 amountToDeposit = 10 ether;
vm.startPrank(user);
_fundUser(amountToDeposit, user);
_depositAToken(amountToDeposit, user);
vm.stopPrank();

_setPausedAsAclAdmin(true);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.transfer(user1, amountToDeposit);
}

function test_claimingRewards_shouldRevert() external {
_configureLM();
uint128 amountToDeposit = 10 ether;
vm.startPrank(user);
_fundUser(amountToDeposit, user);
_depositAToken(amountToDeposit, user);
vm.stopPrank();

_setPausedAsAclAdmin(true);
vm.expectRevert(PausableUpgradeable.EnforcedPause.selector);
vm.prank(user);
staticATokenLM.claimRewardsToSelf(rewardTokens);
}

function _setPausedAsAclAdmin(bool paused) internal {
_setPaused(poolAdmin, paused);
}

function _setPaused(address actor, bool paused) internal {
vm.prank(actor);
staticATokenLM.setPaused(paused);
}
}
Loading

0 comments on commit 4ad98c0

Please sign in to comment.