Borrowers can choose whatever token they want to be the underlying token for a market. The problem comes when those tokens are Rebasing tokens such as Ampleforth. The balances of those tokens are changed (rebased) by a certain algorithm depending on the purpose of the Rebasing token and the market conditions. When lenders initially deposit Rebase tokens in the Wildcat protocol, the amount of that deposit will be stored within the protocol. Based on the internal rebasing of Wildcat the scaledAmound will change based on scaledFactor which increases in order to add APR to the lender deposits, but that doesn’t account for rebasing in the underlying token. At a later stage this may result in wrong user balance accounting. In the case of Ampleforth if the price_exchange_rate of AMPL > 1 2019 USD the market is indicating there is more demand than supply. The Ampleforth protocol automatically and proportionally increases the quantity of tokens in user wallets. This will result in users being able to withdraw more tokens that they are supposed to and eventually the last user to withdraw won’t be able to, as there won’t be enough tokens left in the Wildcat protocol. Similar scenario is presented in the POC
Add the following test to WildcatMarketController.t.sol
function test_rebaseToken() public {
setUpContracts();
/// INFO: Mint Rebase token to Alice and Bob
vm.startPrank(rebaseTokenOwner);
rebaseToken.transfer(alice, 10e9);
rebaseToken.transfer(bob, 10e9);
console.log("");
console.log("Mint Rebase token to Alice and Bob");
console.log("Alice rebase token balance: ", rebaseToken.balanceOf(alice));
console.log("Alice rebase token balance: ", rebaseToken.balanceOf(bob));
console.log("");
vm.stopPrank();
/// INFO: Approve Lenders
vm.startPrank(borrower);
address[] memory lenders = new address[](2);
lenders[0] = alice;
lenders[1] = bob;
wildcatMarketController.authorizeLenders(lenders);
address[] memory markets = new address[](1);
markets[0] = marketAddress;
wildcatMarketController.updateLenderAuthorization(alice, markets);
wildcatMarketController.updateLenderAuthorization(bob, markets);
vm.stopPrank();
vm.startPrank(alice);
rebaseToken.approve(marketAddress, 10e9);
wildcatMarket.depositUpTo(10e9);
vm.stopPrank();
vm.startPrank(bob);
rebaseToken.approve(marketAddress, 10e9);
wildcatMarket.depositUpTo(10e9);
vm.stopPrank();
/// INFO: Rebase the token
vm.startPrank(rebaseTokenOwner);
rebaseToken.rebase(-49_000_000e8);
vm.stopPrank();
vm.startPrank(alice);
wildcatMarket.queueWithdrawal(10e9);
uint32 expiry = wildcatMarket.currentState().pendingWithdrawalExpiry;
skip(3601);
wildcatMarket.executeWithdrawal(alice, expiry);
vm.stopPrank();
vm.startPrank(bob);
wildcatMarket.queueWithdrawal(10e9);
uint32 expiryNew = wildcatMarket.currentState().pendingWithdrawalExpiry;
skip(3601);
wildcatMarket.executeWithdrawal(bob, expiryNew);
console.log("");
console.log("Balance of Bob, Alice and Market after Bob and Alice withdrew their rebase token");
console.log("Alice rebase token balance after withdraw: ", rebaseToken.balanceOf(alice));
console.log("Bob rebase token balance after withdraw: ", rebaseToken.balanceOf(bob));
console.log("Market rebase token balance after rebase: ", rebaseToken.balanceOf(marketAddress));
vm.stopPrank();
}
Logs:
Rebase token got deployed: 0xc7183455a4C133Ae270771860664b6B7ec320bB1
Mint Rebase token to Alice and Bob
Alice rebase token balance: 10000000000
Alice rebase token balance: 10000000000
Balance of Bob, Alice and Market after Bob and Alice withdrew their rebase token
Alice rebase token balance after withdraw: 10000000000
Bob rebase token balance after withdraw: 8040000000
Market rebase token balance after rebase: 0
When bob quarries for withdraw the 10e9 rebase tokens he deposited and later withdraws them, he receives less than he is supposed to, and Alice effectively stole portion of Bob’s deposit due to wrong accounting. In the above test both Alice and Bob should have been allowed to withdraw a maximum of 9020000000 tokens - usually minus the protocol fees, which in the POC are set to 0.
In order to run the test you have to import the following to WildcatMarketController.t.sol
import {WildcatArchController} from 'src/WildcatArchController.sol';
import {WildcatMarketControllerFactory} from 'src/WildcatMarketControllerFactory.sol';
import {
MinimumDelinquencyGracePeriod, MaximumDelinquencyGracePeriod,
MinimumReserveRatioBips, MaximumReserveRatioBips, MinimumDelinquencyFeeBips,
MaximumDelinquencyFeeBips, MinimumWithdrawalBatchDuration, MaximumWithdrawalBatchDuration,
MinimumAnnualInterestBips, MaximumAnnualInterestBips } from './shared/TestConstants.sol';
import {WildcatMarketController} from 'src/WildcatMarketController.sol';
import {WildcatSanctionsSentinel, IChainalysisSanctionsList, IWildcatArchController } from 'src/WildcatSanctionsSentinel.sol';
import {WildcatSanctionsEscrow, IWildcatSanctionsEscrow } from 'src/WildcatSanctionsEscrow.sol';
import {SanctionsList} from 'src/libraries/Chainalysis.sol';
import {MockChainalysis, deployMockChainalysis } from './helpers/MockChainalysis.sol';
import {RebaseToken} from './helpers/RebaseToken.sol';
Add the following variables to WildcatMarketController.t.sol
address public alice = address(123);
address public bob = address(124);
address public hacker = address(125);
address public borrower = address(126);
address public archOwner = address(127);
address public jon = address(128);
address public rebaseTokenOwner = address(129);
WildcatArchController public wildcatArchController;
WildcatMarketControllerFactory public wildcatMarketControllerFactory;
MarketParameterConstraints public constraintsWMC;
WildcatMarket public wildcatMarket;
address public marketAddress;
WildcatMarketController public wildcatMarketController;
address public marketControllerAddress;
WildcatSanctionsSentinel internal sentinel;
RebaseToken public rebaseToken;
Add the following functions to WildcatMarketController.t.sol
function _resetConstraints() internal {
constraintsWMC = MarketParameterConstraints({
minimumDelinquencyGracePeriod: MinimumDelinquencyGracePeriod,
maximumDelinquencyGracePeriod: MaximumDelinquencyGracePeriod,
minimumReserveRatioBips: MinimumReserveRatioBips,
maximumReserveRatioBips: MaximumReserveRatioBips,
minimumDelinquencyFeeBips: MinimumDelinquencyFeeBips,
maximumDelinquencyFeeBips: MaximumDelinquencyFeeBips,
minimumWithdrawalBatchDuration: MinimumWithdrawalBatchDuration,
maximumWithdrawalBatchDuration: MaximumWithdrawalBatchDuration,
minimumAnnualInterestBips: MinimumAnnualInterestBips,
maximumAnnualInterestBips: MaximumAnnualInterestBips
});
}
function setUpContracts() public {
/// INFO: Deploy MockErc20 token and mint tokens
rebaseToken = new RebaseToken();
console.log("Rebase token got deployed: ", address(rebaseToken));
vm.startPrank(rebaseTokenOwner);
rebaseToken.initialize(rebaseTokenOwner);
vm.stopPrank();
vm.startPrank(archOwner);
/// INFO: Deploy & set up ArchController
wildcatArchController = new WildcatArchController();
wildcatArchController.registerBorrower(borrower);
/// INFO: Set up sentinel
sentinel = new WildcatSanctionsSentinel(address(wildcatArchController), address(SanctionsList));
/// INFO: Deploy Factory
_resetConstraints();
wildcatMarketControllerFactory = new WildcatMarketControllerFactory(
address(wildcatArchController),
address(sentinel),
constraintsWMC
);
wildcatArchController.registerControllerFactory(address(wildcatMarketControllerFactory));
vm.stopPrank();
/// INFO: Deploy MarketController and Market
vm.startPrank(borrower);
uint128 maxTotalSupply = 100_000e18;
uint16 annualInterestBips = 1000; // 10%
uint16 delinquencyFeeBips = 1000; // 10%
uint32 withdrawalBatchDuration = 3600; // 1 hour
uint16 reserveRatioBips = 1000; // 10%
uint32 delinquencyGracePeriod = 3600; // 1 hour
(marketControllerAddress, marketAddress) = wildcatMarketControllerFactory.deployControllerAndMarket(
"WildcatTokenR",
"WCTKNR",
address(rebaseToken),
maxTotalSupply,
annualInterestBips,
delinquencyFeeBips,
withdrawalBatchDuration,
reserveRatioBips,
delinquencyGracePeriod
);
wildcatMarket = WildcatMarket(marketAddress);
wildcatMarketController = WildcatMarketController(marketControllerAddress);
vm.stopPrank();
}
Add RebaseToken.sol to test/helpers
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "./SafeMathInt.sol";
import "./SafeMath.sol";
contract RebaseToken {
using SafeMath for uint256;
using SafeMathInt for int256;
uint256 public constant DECIMALS = 9;
uint256 public constant MAX_UINT256 = type(uint256).max;
uint256 public constant INITIAL_FRAGMENTS_SUPPLY = 50 * 10**6 * 10**DECIMALS;
string public _name;
string public _symbol;
uint8 public _decimals;
// TOTAL_GONS is a multiple of INITIAL_FRAGMENTS_SUPPLY so that _gonsPerFragment is an integer.
// Use the highest value that fits in a uint256 for max granularity.
uint256 public constant TOTAL_GONS = MAX_UINT256 - (MAX_UINT256 % INITIAL_FRAGMENTS_SUPPLY);
// MAX_SUPPLY = maximum integer < (sqrt(4*TOTAL_GONS + 1) - 1) / 2
uint256 public constant MAX_SUPPLY = type(uint128).max; // (2^128) - 1
uint256 public _totalSupply;
uint256 public _gonsPerFragment;
mapping(address => uint256) public _gonBalances;
// This is denominated in Fragments, because the gons-fragments conversion might change before
// it's fully paid.
mapping(address => mapping(address => uint256)) public _allowedFragments;
function initialize(address owner_) public {
_name = "Ampleforth";
_symbol = "AMPL";
_decimals = uint8(DECIMALS);
_totalSupply = INITIAL_FRAGMENTS_SUPPLY;
_gonBalances[owner_] = TOTAL_GONS;
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
}
function rebase(int256 supplyDelta) external returns (uint256)
{
if (supplyDelta == 0) {
return _totalSupply;
}
if (supplyDelta < 0) {
_totalSupply = _totalSupply.sub(uint256(supplyDelta.abs()));
} else {
_totalSupply = _totalSupply.add(uint256(supplyDelta));
}
if (_totalSupply > MAX_SUPPLY) {
_totalSupply = MAX_SUPPLY;
}
_gonsPerFragment = TOTAL_GONS.div(_totalSupply);
return _totalSupply;
}
/**
* @return The total number of fragments.
*/
function totalSupply() external view returns (uint256) {
return _totalSupply;
}
function decimals() external view returns (uint256) {
return _decimals;
}
function name() external view returns (string memory) {
return _name;
}
function symbol() external view returns (string memory) {
return _symbol;
}
/**
* @param who The address to query.
* @return The balance of the specified address.
*/
function balanceOf(address who) external view returns (uint256) {
return _gonBalances[who].div(_gonsPerFragment);
}
/**
* @param who The address to query.
* @return The gon balance of the specified address.
*/
function scaledBalanceOf(address who) external view returns (uint256) {
return _gonBalances[who];
}
/**
* @return the total number of gons.
*/
function scaledTotalSupply() external pure returns (uint256) {
return TOTAL_GONS;
}
function transfer(address to, uint256 value) external returns (bool) {
uint256 gonValue = value.mul(_gonsPerFragment);
_gonBalances[msg.sender] = _gonBalances[msg.sender].sub(gonValue);
_gonBalances[to] = _gonBalances[to].add(gonValue);
return true;
}
function allowance(address owner_, address spender) external view returns (uint256) {
return _allowedFragments[owner_][spender];
}
function transferFrom(address from, address to, uint256 value) external returns (bool) {
_allowedFragments[from][msg.sender] = _allowedFragments[from][msg.sender].sub(value);
uint256 gonValue = value.mul(_gonsPerFragment);
_gonBalances[from] = _gonBalances[from].sub(gonValue);
_gonBalances[to] = _gonBalances[to].add(gonValue);
return true;
}
function approve(address spender, uint256 value) external returns (bool) {
_allowedFragments[msg.sender][spender] = value;
return true;
}
}
Add SafeMath.sol to test/helpers
pragma solidity 0.8.20;
/**
* @title SafeMath
* @dev Math operations with safety checks that revert on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, reverts on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
// Gas optimization: this is cheaper than requiring 'a' not being zero, but the
// benefit is lost if 'b' is also tested.
// See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers truncating the quotient, reverts on division by zero.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
require(b > 0); // Solidity only automatically asserts when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, reverts on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a);
uint256 c = a - b;
return c;
}
/**
* @dev Adds two numbers, reverts on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a);
return c;
}
/**
* @dev Divides two numbers and returns the remainder (unsigned integer modulo),
* reverts when dividing by zero.
*/
function mod(uint256 a, uint256 b) internal pure returns (uint256) {
require(b != 0);
return a % b;
}
}
Add SafeMathInt.sol to test/helpers
/*
MIT License
Copyright (c) 2018 requestnetwork
Copyright (c) 2018 Fragments, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity 0.8.20;
/**
* @title SafeMathInt
* @dev Math operations for int256 with overflow safety checks.
*/
library SafeMathInt {
int256 private constant MIN_INT256 = int256(1) << 255;
int256 private constant MAX_INT256 = ~(int256(1) << 255);
/**
* @dev Multiplies two int256 variables and fails on overflow.
*/
function mul(int256 a, int256 b) internal pure returns (int256) {
int256 c = a * b;
// Detect overflow when multiplying MIN_INT256 with -1
require(c != MIN_INT256 || (a & MIN_INT256) != (b & MIN_INT256));
require((b == 0) || (c / b == a));
return c;
}
/**
* @dev Division of two int256 variables and fails on overflow.
*/
function div(int256 a, int256 b) internal pure returns (int256) {
// Prevent overflow when dividing MIN_INT256 by -1
require(b != -1 || a != MIN_INT256);
// Solidity already throws when dividing by 0.
return a / b;
}
/**
* @dev Subtracts two int256 variables and fails on overflow.
*/
function sub(int256 a, int256 b) internal pure returns (int256) {
int256 c = a - b;
require((b >= 0 && c <= a) || (b < 0 && c > a));
return c;
}
/**
* @dev Adds two int256 variables and fails on overflow.
*/
function add(int256 a, int256 b) internal pure returns (int256) {
int256 c = a + b;
require((b >= 0 && c >= a) || (b < 0 && c < a));
return c;
}
/**
* @dev Converts to absolute value, and fails on overflow.
*/
function abs(int256 a) internal pure returns (int256) {
require(a != MIN_INT256);
return a < 0 ? -a : a;
}
/**
* @dev Computes 2^exp with limited precision where -100 <= exp <= 100 * one
* @param one 1.0 represented in the same fixed point number format as exp
* @param exp The power to raise 2 to -100 <= exp <= 100 * one
* @return 2^exp represented with same number of decimals after the point as one
*/
function twoPower(int256 exp, int256 one) internal pure returns (int256) {
bool reciprocal = false;
if (exp < 0) {
reciprocal = true;
exp = abs(exp);
}
// Precomputed values for 2^(1/2^i) in 18 decimals fixed point numbers
int256[5] memory ks = [
int256(1414213562373095049),
1189207115002721067,
1090507732665257659,
1044273782427413840,
1021897148654116678
];
int256 whole = div(exp, one);
require(whole <= 100);
int256 result = mul(int256(uint256(1) << uint256(whole)), one);
int256 remaining = sub(exp, mul(whole, one));
int256 current = div(one, 2);
for (uint256 i = 0; i < 5; i++) {
if (remaining >= current) {
remaining = sub(remaining, current);
result = div(mul(result, ks[i]), 10**18); // 10**18 to match hardcoded ks values
}
current = div(current, 2);
}
if (reciprocal) {
result = div(mul(one, one), result);
}
return result;
}
}
To run the test use forge test -vvv --mt test_rebaseToken
Manual review & Foundry
Implement a whitelist for allowed tokens.
Other
The text was updated successfully, but these errors were encountered:
All reactions