Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pausability #45

Merged
merged 11 commits into from
Aug 8, 2024
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
29 changes: 24 additions & 5 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 returns(bool) {
sakulstra marked this conversation as resolved.
Show resolved Hide resolved
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
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 the token which will forbid all transfers
sakulstra marked this conversation as resolved.
Show resolved Hide resolved
* @param paused boolean determining if the token should be paused or unpaused
*/
function setPaused(bool paused) external;
}
116 changes: 116 additions & 0 deletions tests/periphery/static-a-token/Pausable.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 _setPausedAsAclAdmin(bool paused) internal {
_setPaused(poolAdmin, paused);
}

function _setPaused(address actor, bool paused) internal {
vm.prank(actor);
staticATokenLM.setPaused(paused);
}
}
26 changes: 24 additions & 2 deletions tests/periphery/static-a-token/StaticATokenLM.t.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol';
import {Initializable} from 'openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.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';
Expand Down Expand Up @@ -29,8 +31,8 @@ contract StaticATokenLMTest is BaseTest {

function test_initializeShouldRevert() public {
address impl = factory.STATIC_A_TOKEN_IMPL();
vm.expectRevert();
IStaticATokenLM(impl).initialize(0xe50fA9b3c56FfB159cB0FCA61F5c9D750e8128c8, 'hey', 'ho');
vm.expectRevert(Initializable.InvalidInitialization.selector);
IStaticATokenLM(impl).initialize(A_TOKEN, 'hey', 'ho');
}

function test_getters() public view {
Expand Down Expand Up @@ -556,6 +558,26 @@ contract StaticATokenLMTest is BaseTest {
staticATokenLM.permit(permit.owner, permit.spender, permit.value, permit.deadline, v, r, s);
}

function test_rescuable_shouldRevertForInvalidCaller() external {
deal(tokenList.usdx, address(staticATokenLM), 1 ether);
vm.expectRevert('ONLY_RESCUE_GUARDIAN');
IRescuable(address(staticATokenLM)).emergencyTokenTransfer(
tokenList.usdx,
address(this),
1 ether
);
}

function test_rescuable_shouldSuceedForOwner() external {
deal(tokenList.usdx, address(staticATokenLM), 1 ether);
vm.startPrank(poolAdmin);
IRescuable(address(staticATokenLM)).emergencyTokenTransfer(
tokenList.usdx,
address(this),
1 ether
);
}

function _configureLM() internal {
PullRewardsTransferStrategy strat = new PullRewardsTransferStrategy(
report.rewardsControllerProxy,
Expand Down
Loading