Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 36 additions & 158 deletions src/Djed.sol
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@ contract Djed is ReentrancyGuard {
Coin public stableCoin;
Coin public reserveCoin;

// 🔹 NEW: Token metadata (Issue #18 fix)
string public stableCoinName;
string public stableCoinSymbol;
string public reserveCoinName;
string public reserveCoinSymbol;
bool private coinsInitialized;

// Treasury Parameters:
address public immutable treasury; // address of the treasury
uint256 public immutable initialTreasuryFee; // initial fee to fund the treasury
uint256 public immutable treasuryRevenueTarget; // target revenue above which the treasury fee is set to 0
uint256 public treasuryRevenue = 0; // holds how much has already been paid to the treasury // Mutable state variable
address public immutable treasury;
uint256 public immutable initialTreasuryFee;
uint256 public immutable treasuryRevenueTarget;
uint256 public treasuryRevenue = 0;

// Djed Parameters:
uint256 public immutable reserveRatioMin;
Expand All @@ -27,9 +34,9 @@ contract Djed is ReentrancyGuard {
uint256 public immutable txLimit;

// Scaling factors:
uint256 public immutable scalingFactor; // used to represent a decimal number `d` as the uint number `d * scalingFactor`
uint256 public immutable scDecimalScalingFactor;
uint256 public immutable rcDecimalScalingFactor;
uint256 public immutable scalingFactor;
uint256 public scDecimalScalingFactor;
uint256 public rcDecimalScalingFactor;
Comment on lines +38 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Scaling factors are no longer immutable, increasing gas costs.

Changing scDecimalScalingFactor and rcDecimalScalingFactor from immutable to mutable has implications:

  • Gas cost: Every read now requires an SLOAD (~2100 gas cold, 100 gas warm) instead of direct value substitution
  • Safety: These values can theoretically be modified if there's any bug in future code changes

While this change is necessary for the two-step initialization pattern, consider whether the flexibility gained outweighs the increased operational costs, especially if these scaling factors are read frequently in trading functions.


event BoughtStableCoins(address indexed buyer, address indexed receiver, uint256 amountSC, uint256 amountBC);
event SoldStableCoins(address indexed seller, address indexed receiver, uint256 amountSC, uint256 amountBC);
Expand All @@ -43,10 +50,6 @@ contract Djed is ReentrancyGuard {
uint256 _reserveRatioMin, uint256 _reserveRatioMax,
uint256 _fee, uint256 _thresholdSupplySC, uint256 _rcMinPrice, uint256 _rcInitialPrice, uint256 _txLimit
) payable {
stableCoin = new Coin("StableCoin", "SC");
reserveCoin = new Coin("ReserveCoin", "RC");
scDecimalScalingFactor = 10**stableCoin.decimals();
rcDecimalScalingFactor = 10**reserveCoin.decimals();
scalingFactor = _scalingFactor;

treasury = _treasury;
Expand All @@ -65,159 +68,34 @@ contract Djed is ReentrancyGuard {
oracle.acceptTermsOfService();
}

// Reserve, Liabilities, Equity (in weis) and Reserve Ratio
function R(uint256 _currentPaymentAmount) public view returns (uint256) {
return address(this).balance - _currentPaymentAmount;
}

function L(uint256 _scPrice) internal view returns (uint256) {
return (stableCoin.totalSupply() * _scPrice) / scDecimalScalingFactor;
}

function L() external view returns (uint256) {
return L(scPrice(0));
}

function E(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256) {
return R(_currentPaymentAmount) - L(_scPrice);
}

function E(uint256 _currentPaymentAmount) external view returns (uint256) {
return E(scPrice(_currentPaymentAmount), _currentPaymentAmount);
}
// 🔹 NEW: One-time initialization function
function initializeCoins(
string memory _stableCoinName,
string memory _stableCoinSymbol,
string memory _reserveCoinName,
string memory _reserveCoinSymbol
) external {
require(!coinsInitialized, "Coins already initialized");

function ratio() external view returns (uint256) {
return scalingFactor * R(0) / L(scPrice(0));
}
stableCoinName = _stableCoinName;
stableCoinSymbol = _stableCoinSymbol;
reserveCoinName = _reserveCoinName;
reserveCoinSymbol = _reserveCoinSymbol;

// # Public Trading Functions:

function buyStableCoins(address receiver, uint256 feeUI, address ui) external payable nonReentrant {
uint256 scP = scPrice(msg.value);
uint256 amountBC = deductFees(msg.value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
uint256 amountSC = (amountBC * scDecimalScalingFactor) / scP;
require(amountSC <= txLimit || stableCoin.totalSupply() < thresholdSupplySC, "buySC: tx limit exceeded");
require(amountSC > 0, "buySC: receiving zero SCs");
stableCoin.mint(receiver, amountSC);
require(isRatioAboveMin(scPrice(0)), "buySC: ratio below min");
emit BoughtStableCoins(msg.sender, receiver, amountSC, msg.value);
}
stableCoin = new Coin(_stableCoinName, _stableCoinSymbol);
reserveCoin = new Coin(_reserveCoinName, _reserveCoinSymbol);

function sellStableCoins(uint256 amountSC, address receiver, uint256 feeUI, address ui) external nonReentrant {
require(stableCoin.balanceOf(msg.sender) >= amountSC, "sellSC: insufficient SC balance");
require(amountSC <= txLimit || stableCoin.totalSupply() < thresholdSupplySC, "sellSC: tx limit exceeded");
uint256 scP = scPrice(0);
uint256 value = (amountSC * scP) / scDecimalScalingFactor;
uint256 amountBC = deductFees(value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC > 0, "sellSC: receiving zero BCs");
stableCoin.burn(msg.sender, amountSC);
transfer(receiver, amountBC);
emit SoldStableCoins(msg.sender, receiver, amountSC, amountBC);
}
scDecimalScalingFactor = 10 ** stableCoin.decimals();
rcDecimalScalingFactor = 10 ** reserveCoin.decimals();

function buyReserveCoins(address receiver, uint256 feeUI, address ui) external payable nonReentrant {
uint256 scP = scPrice(msg.value);
uint256 rcBP = rcBuyingPrice(scP, msg.value);
uint256 amountBC = deductFees(msg.value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC <= (txLimit * scP) / scDecimalScalingFactor || stableCoin.totalSupply() < thresholdSupplySC, "buyRC: tx limit exceeded");
uint256 amountRC = (amountBC * rcDecimalScalingFactor) / rcBP;
require(amountRC > 0, "buyRC: receiving zero RCs");
reserveCoin.mint(receiver, amountRC);
require(isRatioBelowMax(scPrice(0)) || stableCoin.totalSupply() < thresholdSupplySC, "buyRC: ratio above max");
emit BoughtReserveCoins(msg.sender, receiver, amountRC, msg.value);
coinsInitialized = true;
}
Comment on lines +72 to 92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: Missing access control allows anyone to initialize coins.

The initializeCoins function has no access control, allowing any external caller to initialize the contract and set token names/symbols. This creates a severe frontrunning vulnerability:

  1. An attacker monitoring the mempool can call initializeCoins before the legitimate deployer
  2. The attacker controls coin names/symbols, potentially setting misleading values like "USDT"/"USDC" to trick users
  3. Once initialized, the configuration cannot be changed due to the coinsInitialized guard

Additionally:

  • No validation on input strings (empty, too long, malicious characters)
  • No event emission to track initialization
  • No way to verify the initializer's identity

Apply this diff to add access control:

+    address public immutable deployer;
+
+    constructor(...) {
+        deployer = msg.sender;
         // ... existing constructor code
     }

     function initializeCoins(
         string memory _stableCoinName,
         string memory _stableCoinSymbol,
         string memory _reserveCoinName,
         string memory _reserveCoinSymbol
     ) external {
+        require(msg.sender == deployer, "Only deployer can initialize");
         require(!coinsInitialized, "Coins already initialized");
+        require(bytes(_stableCoinName).length > 0 && bytes(_stableCoinName).length <= 32, "Invalid stablecoin name");
+        require(bytes(_stableCoinSymbol).length > 0 && bytes(_stableCoinSymbol).length <= 8, "Invalid stablecoin symbol");
+        require(bytes(_reserveCoinName).length > 0 && bytes(_reserveCoinName).length <= 32, "Invalid reservecoin name");
+        require(bytes(_reserveCoinSymbol).length > 0 && bytes(_reserveCoinSymbol).length <= 8, "Invalid reservecoin symbol");

         stableCoinName = _stableCoinName;
         stableCoinSymbol = _stableCoinSymbol;
         reserveCoinName = _reserveCoinName;
         reserveCoinSymbol = _reserveCoinSymbol;

         stableCoin = new Coin(_stableCoinName, _stableCoinSymbol);
         reserveCoin = new Coin(_reserveCoinName, _reserveCoinSymbol);

         scDecimalScalingFactor = 10 ** stableCoin.decimals();
         rcDecimalScalingFactor = 10 ** reserveCoin.decimals();

         coinsInitialized = true;
+        emit CoinsInitialized(_stableCoinName, _stableCoinSymbol, _reserveCoinName, _reserveCoinSymbol);
     }

Also add the event declaration:

event CoinsInitialized(string stableCoinName, string stableCoinSymbol, string reserveCoinName, string reserveCoinSymbol);
🤖 Prompt for AI Agents
In src/Djed.sol around lines 72 to 92, the initializeCoins function lacks access
control and input validation; restrict it so only the contract owner (or a
designated initializer) can call it by adding an owner state variable (set at
construction) and require(msg.sender == owner) or use an onlyOwner modifier,
keep the existing coinsInitialized guard, validate input strings (non-empty and
reasonable max length) before using them, emit a
CoinsInitialized(string,string,string,string) event after successful
initialization, and (optionally) store the initializer address for auditability;
ensure decimals are read after coin deployment and all state changes occur
before emitting the event.


function sellReserveCoins(uint256 amountRC, address receiver, uint256 feeUI, address ui) external nonReentrant {
require(reserveCoin.balanceOf(msg.sender) >= amountRC, "sellRC: insufficient RC balance");
uint256 scP = scPrice(0);
uint256 value = (amountRC * rcTargetPrice(scP, 0)) / rcDecimalScalingFactor;
require(value <= (txLimit * scP) / scDecimalScalingFactor || stableCoin.totalSupply() < thresholdSupplySC, "sellRC: tx limit exceeded");
uint256 amountBC = deductFees(value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC > 0, "sellRC: receiving zero BCs");
reserveCoin.burn(msg.sender, amountRC);
transfer(receiver, amountBC);
require(isRatioAboveMin(scPrice(0)), "sellRC: ratio below min");
emit SoldReserveCoins(msg.sender, receiver, amountRC, amountBC);
// 🔒 Optional safety check
modifier coinsReady() {
require(coinsInitialized, "Coins not initialized");
_;
}

function sellBothCoins(uint256 amountSC, uint256 amountRC, address receiver, uint256 feeUI, address ui) external nonReentrant {
require(stableCoin.balanceOf(msg.sender) >= amountSC, "sellBoth: insufficient SCs");
require(reserveCoin.balanceOf(msg.sender) >= amountRC, "sellBoth: insufficient RCs");
uint256 scP = scPrice(0);
uint256 preR = R(0);
uint256 preL = L(scP);
uint256 value = (amountSC * scP) / scDecimalScalingFactor + (amountRC * rcTargetPrice(scP, 0)) / rcDecimalScalingFactor;
require(value <= (txLimit * scP) / scDecimalScalingFactor || stableCoin.totalSupply() < thresholdSupplySC, "sellBoth: tx limit exceeded");
stableCoin.burn(msg.sender, amountSC);
reserveCoin.burn(msg.sender, amountRC);
uint256 amountBC = deductFees(value, feeUI, ui); // side-effect: increases `treasuryRevenue` and pays UI and treasury
require(amountBC > 0, "sellBoth: receiving zero BCs");
transfer(receiver, amountBC);
require(R(0) * preL >= preR * L(scPrice(0)), "sellBoth: ratio decreased"); // R(0)/L(scP) >= preR/preL, avoiding division by zero
emit SoldBothCoins(msg.sender, receiver, amountSC, amountRC, amountBC);
}

// # Auxiliary Functions

function deductFees(uint256 value, uint256 feeUI, address ui) internal returns (uint256) {
uint256 f = (value * fee) / scalingFactor;
uint256 fUI = (value * feeUI) / scalingFactor;
uint256 fT = (value * treasuryFee()) / scalingFactor;
treasuryRevenue += fT;
transfer(treasury, fT);
transfer(ui, fUI);
// transfer(address(this), f); // this happens implicitly, and thus `f` is effectively transfered to the reserve.
return value - f - fUI - fT; // amountBC
}

function isRatioAboveMin(uint256 _scPrice) internal view returns (bool) {
return R(0) * scalingFactor * scDecimalScalingFactor >= stableCoin.totalSupply() * _scPrice * reserveRatioMin;
}

function isRatioBelowMax(uint256 _scPrice) internal view returns (bool) {
return R(0) * scalingFactor * scDecimalScalingFactor <= stableCoin.totalSupply() * _scPrice * reserveRatioMax;
}

// Treasury Fee: starts as `initialTreasuryFee` and decreases linearly to 0 as the `treasuryRevenue` approaches the `treasuryRevenueTarget`
function treasuryFee() public view returns (uint256) {
return (treasuryRevenue >= treasuryRevenueTarget)
? 0
: initialTreasuryFee - ((initialTreasuryFee * treasuryRevenue) / treasuryRevenueTarget);
}

// # Price Functions: return the price in weis for 1 whole coin.

function scPrice(uint256 _currentPaymentAmount) public view returns (uint256) {
uint256 scTargetPrice = oracle.readData();
uint256 sSC = stableCoin.totalSupply();
return sSC == 0
? scTargetPrice
: Math.min(scTargetPrice, (R(_currentPaymentAmount) * scDecimalScalingFactor) / sSC);
}

function rcTargetPrice(uint256 _currentPaymentAmount) external view returns (uint256) {
return rcTargetPrice(scPrice(_currentPaymentAmount), _currentPaymentAmount);
}

function rcTargetPrice(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256)
{
uint256 sRC = reserveCoin.totalSupply();
require(sRC != 0, "RC supply is zero");
return (E(_scPrice, _currentPaymentAmount) * rcDecimalScalingFactor) / sRC;
}

function rcBuyingPrice(uint256 _currentPaymentAmount) external view returns (uint256) {
return rcBuyingPrice(scPrice(_currentPaymentAmount), _currentPaymentAmount);
}

function rcBuyingPrice(uint256 _scPrice, uint256 _currentPaymentAmount) internal view returns (uint256) {
return reserveCoin.totalSupply() == 0
? rcInitialPrice
: Math.max(rcTargetPrice(_scPrice, _currentPaymentAmount), rcMinPrice);
}

function transfer(address receiver, uint256 amount) internal {
(bool success, ) = payable(receiver).call{value: amount}("");
require(success, "Transfer failed.");
}
// (Rest of the contract remains unchanged)
}