diff --git a/audits/11-09-2024_Certora_StataTokenV2.pdf b/audits/11-09-2024_Certora_StataTokenV2.pdf index 1d6e8654..b908d33e 100644 Binary files a/audits/11-09-2024_Certora_StataTokenV2.pdf and b/audits/11-09-2024_Certora_StataTokenV2.pdf differ diff --git a/certora/stata/specs/StataToken/StataToken.spec b/certora/stata/specs/StataToken/StataToken.spec index 479816b3..ee1c3ef5 100644 --- a/certora/stata/specs/StataToken/StataToken.spec +++ b/certora/stata/specs/StataToken/StataToken.spec @@ -102,8 +102,8 @@ import "../methods/methods_base.spec"; f.contract == currentContract && !harnessOnlyMethods(f) && f.selector != sig:initialize(address, string, string).selector) && - f.selector != sig:emergencyEtherTransfer(uint256).selector && - f.selector != sig:emergencyTokenTransfer(address,uint256).selector + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && + f.selector != sig:emergencyTokenTransfer(address,address,uint256).selector } { // Assuming single reward single_RewardToken_setup(); @@ -137,7 +137,7 @@ import "../methods/methods_base.spec"; && !harnessMethodsMinusHarnessClaimMethods(f) && !claimFunctions(f) && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector - && f.selector != sig:emergencyEtherTransfer(uint256).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector } { preserved redeem(uint256 shares, address receiver, address owner) with (env e1) { @@ -152,7 +152,7 @@ import "../methods/methods_base.spec"; requireInvariant solvency_total_asset_geq_total_supply(); require balanceOf(owner) <= totalSupply(); } - preserved emergencyTokenTransfer(address asset, uint256 amount) with (env e3) { + preserved emergencyTokenTransfer(address asset, address to, uint256 amount) with (env e3) { require rate() >= RAY(); } } @@ -170,7 +170,7 @@ import "../methods/methods_base.spec"; f.contract == currentContract && !harnessMethodsMinusHarnessClaimMethods(f) && !claimFunctions(f) - && f.selector != sig:emergencyEtherTransfer(uint256).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector && f.selector != sig:claimDoubleRewardOnBehalfSame(address, address, address).selector } { preserved withdraw(uint256 assets, address receiver, address owner) with (env e3) { @@ -198,7 +198,7 @@ import "../methods/methods_base.spec"; preserved redeemATokens(uint256 shares, address receiver, address owner) with (env e2) { require balanceOf(owner) <= totalSupply(); } - preserved emergencyTokenTransfer(address asset, uint256 amount) with (env e1) { + preserved emergencyTokenTransfer(address asset, address to, uint256 amount) with (env e1) { require rate() >= RAY(); } } @@ -216,7 +216,7 @@ import "../methods/methods_base.spec"; => (_RewardsController.getUserAccruedReward(_asset, reward, user) == _RewardsController.getUserAccruedRewards(reward, user))) filtered {f -> f.contract == currentContract && - f.selector != sig:emergencyEtherTransfer(uint256).selector && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && !harnessOnlyMethods(f) } { @@ -256,8 +256,8 @@ import "../methods/methods_base.spec"; && !collectAndUpdateFunction(f) && !harnessOnlyMethods(f) && f.selector != sig:initialize(address,string,string).selector - && f.selector != sig:emergencyEtherTransfer(uint256).selector - && f.selector != sig:emergencyTokenTransfer(address,uint256).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector + && f.selector != sig:emergencyTokenTransfer(address,address,uint256).selector } { env e; @@ -301,7 +301,7 @@ rule getClaimableRewards_stable(method f) && !claimFunctions(f) && !collectAndUpdateFunction(f) && f.selector != sig:initialize(address,string,string).selector - && f.selector != sig:emergencyEtherTransfer(uint256).selector + && f.selector != sig:emergencyEtherTransfer(address,uint256).selector && !harnessOnlyMethods(f) } { diff --git a/certora/stata/specs/StataToken/aTokenProperties.spec b/certora/stata/specs/StataToken/aTokenProperties.spec index be0f9fad..55d4e705 100644 --- a/certora/stata/specs/StataToken/aTokenProperties.spec +++ b/certora/stata/specs/StataToken/aTokenProperties.spec @@ -193,7 +193,7 @@ import "../methods/methods_base.spec"; !f.isView && f.selector != sig:redeem(uint256,address,address).selector && f.selector != sig:redeemATokens(uint256,address,address).selector && - f.selector != sig:emergencyEtherTransfer(uint256).selector && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && !harnessOnlyMethods(f)} { preserved with (env e){ diff --git a/certora/stata/specs/erc4626/erc4626Extended.spec b/certora/stata/specs/erc4626/erc4626Extended.spec index cbf95cb6..c84370b9 100644 --- a/certora/stata/specs/erc4626/erc4626Extended.spec +++ b/certora/stata/specs/erc4626/erc4626Extended.spec @@ -235,7 +235,7 @@ import "../methods/methods_base.spec"; f.contract == currentContract && !f.isView && !harnessOnlyMethods(f) && - f.selector != sig:emergencyEtherTransfer(uint256).selector && + f.selector != sig:emergencyEtherTransfer(address,uint256).selector && f.selector != sig:deposit(uint256,address).selector && f.selector != sig:depositWithPermit(uint256,address,uint256,IERC4626StataToken.SignatureParams,bool).selector && f.selector != sig:withdraw(uint256,address,address).selector && diff --git a/src/periphery/contracts/static-a-token/StataTokenV2.sol b/src/periphery/contracts/static-a-token/StataTokenV2.sol index 0653a683..f3784053 100644 --- a/src/periphery/contracts/static-a-token/StataTokenV2.sol +++ b/src/periphery/contracts/static-a-token/StataTokenV2.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.0; import {ERC20Upgradeable, ERC20PermitUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol'; import {IERC20Metadata} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {PausableUpgradeable} from 'openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol'; -import {IPermissionlessRescuable, PermissionlessRescuable} from 'solidity-utils/contracts/utils/PermissionlessRescuable.sol'; +import {IRescuable, Rescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; import {IRescuableBase, RescuableBase} from 'solidity-utils/contracts/utils/RescuableBase.sol'; import {IERC20Permit} from '@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol'; @@ -24,7 +24,7 @@ contract StataTokenV2 is ERC20AaveLMUpgradeable, ERC4626StataTokenUpgradeable, PausableUpgradeable, - PermissionlessRescuable, + Rescuable, IStataTokenV2 { using Math for uint256; @@ -59,9 +59,9 @@ contract StataTokenV2 is else _unpause(); } - /// @inheritdoc IPermissionlessRescuable - function whoShouldReceiveFunds() public view override returns (address) { - return IAToken(address(aToken())).RESERVE_TREASURY_ADDRESS(); + /// @inheritdoc IRescuable + function whoCanRescue() public view override returns (address) { + return POOL_ADDRESSES_PROVIDER.getACLAdmin(); } /// @inheritdoc IRescuableBase diff --git a/tests/periphery/static-a-token/StataTokenV2Rescuable.sol b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol index 9dd59f90..c1c89f0d 100644 --- a/tests/periphery/static-a-token/StataTokenV2Rescuable.sol +++ b/tests/periphery/static-a-token/StataTokenV2Rescuable.sol @@ -1,7 +1,9 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.10; +import {IRescuable} from 'solidity-utils/contracts/utils/Rescuable.sol'; import {IAToken} from '../../../src/periphery/contracts/static-a-token/StataTokenV2.sol'; +import {IERC20} from 'openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol'; import {BaseTest} from './TestBase.sol'; contract StataTokenV2RescuableTest is BaseTest { @@ -12,14 +14,28 @@ contract StataTokenV2RescuableTest is BaseTest { uint256 amount ); + function test_rescuable_shouldRevertForInvalidCaller() external { + deal(tokenList.usdx, address(stataTokenV2), 1 ether); + vm.expectRevert('ONLY_RESCUE_GUARDIAN'); + IRescuable(address(stataTokenV2)).emergencyTokenTransfer( + tokenList.usdx, + address(this), + 1 ether + ); + } + function test_rescuable_shouldTransferAssetsToCollector() external { deal(tokenList.usdx, address(stataTokenV2), 1 ether); - stataTokenV2.emergencyTokenTransfer(tokenList.usdx, 1 ether); + vm.startPrank(poolAdmin); + stataTokenV2.emergencyTokenTransfer(tokenList.usdx, address(this), 1 ether); + assertEq(IERC20(tokenList.usdx).balanceOf(address(this)), 1 ether); } function test_rescuable_shouldWorkForAToken() external { _fundAToken(1 ether, address(stataTokenV2)); - stataTokenV2.emergencyTokenTransfer(aToken, 1 ether); + vm.startPrank(poolAdmin); + stataTokenV2.emergencyTokenTransfer(aToken, address(this), 1 ether); + assertApproxEqAbs(IERC20(aToken).balanceOf(address(this)), 1 ether, 1); } function test_rescuable_shouldNotCauseInsolvency(uint256 donation, uint256 stake) external { @@ -31,7 +47,8 @@ contract StataTokenV2RescuableTest is BaseTest { address treasury = IAToken(aToken).RESERVE_TREASURY_ADDRESS(); vm.expectEmit(true, true, true, true); - emit ERC20Rescued(address(this), aToken, treasury, donation); - stataTokenV2.emergencyTokenTransfer(aToken, donation + stake); + emit ERC20Rescued(poolAdmin, aToken, address(this), donation); + vm.startPrank(poolAdmin); + stataTokenV2.emergencyTokenTransfer(aToken, address(this), donation + stake); } }