Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 4 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@
url = https://github.com/api3dao/contracts
[submodule "lib/hebeswap-contract"]
path = lib/hebeswap-contract
url = https://github.com/HebePlatform/Oracle.git
url = https://github.com/HebePlatform/Oracle.git
[submodule "lib/chainlink-brownie-contracts"]
path = lib/chainlink-brownie-contracts
url = https://github.com/smartcontractkit/chainlink-brownie-contracts
43 changes: 43 additions & 0 deletions scripts/deployDjedTefnutContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import "./DeploymentParameters.sol";
import {DjedTefnut} from "../src/DjedTefnut.sol";

contract DeployDjedTefnut is Script, DeploymentParameters {
function run(SupportedNetworks network, SupportedVersion version) external {
uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(senderPrivateKey);

(
address oracleAddress,
address treasuryAddress,
uint256 scalingFactor,
uint256 treasuryFee,
, // treasuryRevenueTarget - unused
, // reserveRatioMin - unused
, // reserveRatioMax - unused
uint256 fee,
uint256 thresholdSupplySc,
uint256 rcMinPrice,
uint256 rcInitialPrice,
uint256 txLimit
) = getConfigFromNetwork(network, version);

DjedTefnut djedTefnut = new DjedTefnut(
oracleAddress,
scalingFactor,
treasuryAddress,
treasuryFee,
fee,
thresholdSupplySc,
rcMinPrice,
rcInitialPrice,
txLimit
);

console.log("DjedTefnut deployed at:", address(djedTefnut));
vm.stopBroadcast();
}
}
33 changes: 33 additions & 0 deletions scripts/deployDjedTefnutWithMockOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "forge-std/Script.sol";
import {DjedTefnut} from "../src/DjedTefnut.sol";
import {MockShuOracle} from "../src/mock/MockShuOracle.sol";

contract DeployDjedTefnutWithMock is Script {
function run() external {
uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(senderPrivateKey);

// Deploy MockShuOracle with initial price of $1 (1e18)
MockShuOracle oracle = new MockShuOracle(1e18);
console.log("MockShuOracle deployed at:", address(oracle));

// Deploy DjedTefnut with mock oracle
DjedTefnut djedTefnut = new DjedTefnut(
address(oracle), // oracle address
1e18, // scalingFactor (1.0)
msg.sender, // treasury address (deployer)
100, // treasuryFee (1%)
200, // fee (2%)
1000e18, // thresholdSupplySc (1000 stable coins)
1e15, // rcMinPrice (0.001)
1e17, // rcInitialPrice (0.1)
100e18 // txLimit (100 ETH)
);

console.log("DjedTefnut deployed at:", address(djedTefnut));
vm.stopBroadcast();
Comment on lines +9 to +31
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

Fix treasury address + align fee units with scalingFactor (deployment would misconfigure protocol).

  • msg.sender (Line 21) in a Forge script is commonly the script contract address, not the broadcaster EOA; you likely want the broadcast signer address.
  • With scalingFactor = 1e18 (Line 20), treasuryFee = 100 and fee = 200 (Line 22-23) don’t represent 1%/2% under a “scaled decimal” convention; they’ll be effectively ~0.

Proposed fix:

 function run() external {
     uint256 senderPrivateKey = vm.envUint("PRIVATE_KEY");
+    address deployer = vm.addr(senderPrivateKey);
     vm.startBroadcast(senderPrivateKey);

     // Deploy DjedTefnut with mock oracle
     DjedTefnut djedTefnut = new DjedTefnut(
         address(oracle),     // oracle address
         1e18,                // scalingFactor (1.0)
-        msg.sender,          // treasury address (deployer)
-        100,                 // treasuryFee (1%)
-        200,                 // fee (2%)
+        deployer,            // treasury address (deployer)
+        1e16,                // treasuryFee (1%) if scalingFactor=1e18
+        2e16,                // fee (2%) if scalingFactor=1e18
         1000e18,             // thresholdSupplySc (1000 stable coins)
         1e15,                // rcMinPrice (0.001)
         1e17,                // rcInitialPrice (0.1)
         100e18               // txLimit (100 ETH)
     );
 }

Also: confirm console.log compiles in your setup (some setups require console2.log or an explicit console import).

🤖 Prompt for AI Agents
scripts/deployDjedTefnutWithMockOracle.sol around lines 9 to 31: the script
passes msg.sender as the treasury address and uses unscaled integer fees (100,
200) while scalingFactor is 1e18, which will misconfigure the protocol; change
the treasury address to the broadcaster EOA by deriving it from the private key
(e.g., vm.addr(senderPrivateKey)) instead of msg.sender, and convert percent
fees to scaled decimals matching scalingFactor (1% => 1e16, 2% => 2e16) so
treasuryFee and fee are set to 1e16 and 2e16 respectively; also verify your
environment supports console.log (import or switch to console2.log if required).

}
}
176 changes: 176 additions & 0 deletions src/DjedTefnut.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: AEL
pragma solidity ^0.8.0;

import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import {Coin} from "./Coin.sol";
import {IOracleShu} from "./IOracleShu.sol";

contract DjedTefnut is ReentrancyGuard {
IOracleShu public oracle;
Coin public stableCoin;
Coin public reserveCoin;

// Treasury Parameters:
address public immutable TREASURY; // address of the treasury
uint256 public immutable TREASURY_FEE; // fixed fee to fund the treasury

// Djed Parameters:
uint256 public immutable FEE;
uint256 public immutable THRESHOLD_SUPPLY_SC;
uint256 public immutable RC_MIN_PRICE;
uint256 public immutable RC_INITIAL_PRICE;
uint256 public immutable TX_LIMIT;

// Scaling factors:
uint256 public immutable SCALING_FACTOR; // used to represent a decimal number `d` as the uint number `d * scalingFactor`
uint256 public immutable SC_DECIMAL_SCALING_FACTOR;
uint256 public immutable RC_DECIMAL_SCALING_FACTOR;

event BoughtStableCoins(address indexed buyer, address indexed receiver, uint256 amountSc, uint256 amountBc);
event SoldStableCoins(address indexed seller, address indexed receiver, uint256 amountSc, uint256 amountBc);
event BoughtReserveCoins(address indexed buyer, address indexed receiver, uint256 amountRc, uint256 amountBc);
event SoldReserveCoins(address indexed seller, address indexed receiver, uint256 amountRc, uint256 amountBc);

constructor(
address oracleAddress, uint256 _scalingFactor,
address _treasury, uint256 _treasuryFee,
uint256 _fee, uint256 _thresholdSupplySc, uint256 _rcMinPrice, uint256 _rcInitialPrice, uint256 _txLimit
) payable {
stableCoin = new Coin("StableCoin", "SC");
reserveCoin = new Coin("ReserveCoin", "RC");
SC_DECIMAL_SCALING_FACTOR = 10**stableCoin.decimals();
RC_DECIMAL_SCALING_FACTOR = 10**reserveCoin.decimals();
SCALING_FACTOR = _scalingFactor;

TREASURY = _treasury;
TREASURY_FEE = _treasuryFee;

FEE = _fee;
THRESHOLD_SUPPLY_SC = _thresholdSupplySc;
RC_MIN_PRICE = _rcMinPrice;
RC_INITIAL_PRICE = _rcInitialPrice;
TX_LIMIT = _txLimit;

oracle = IOracleShu(oracleAddress);
oracle.acceptTermsOfService();
}

// Reserve, Liabilities, Equity (in weis)
function R(uint256 _currentPaymentAmount) public view returns (uint256) {
return address(this).balance - _currentPaymentAmount;
}
Comment on lines +60 to +62
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: buy* misprices / can underflow because fees are transferred out before calling sc*Price(msg.value).
After deductFees() (Line 83/104) sends ETH out, address(this).balance may become < msg.value, so R(msg.value) (Line 61) can revert. Even when it doesn’t revert, it’s no longer computing “reserve before current payment” consistently.

Suggested fix: compute prices first, then transfer fees, then mint using the post-fee amountBc.

 function buyStableCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
     oracle.updateOracleValues();
-    uint256 amountBc = deductFees(msg.value, feeUi, ui);
-    uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / scMaxPrice(msg.value);
+    uint256 price = scMaxPrice(msg.value);
+    uint256 amountBc = deductFees(msg.value, feeUi, ui);
+    uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / price;
     require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buySC: tx limit exceeded");
     require(amountSc > 0, "buySC: receiving zero SCs");
     stableCoin.mint(receiver, amountSc);
-    emit BoughtStableCoins(msg.sender, receiver, amountSc, msg.value);
+    emit BoughtStableCoins(msg.sender, receiver, amountSc, amountBc);
 }

 function buyReserveCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
     oracle.updateOracleValues();
-    uint256 amountBc = deductFees(msg.value, feeUi, ui);
-    uint256 minPrice = scMinPrice(msg.value);
+    uint256 minPrice = scMinPrice(msg.value);
+    uint256 rcPrice = rcBuyingPrice(minPrice, msg.value);
+    uint256 amountBc = deductFees(msg.value, feeUi, ui);
     require(amountBc <= (TX_LIMIT * minPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buyRC: tx limit exceeded");
-    uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcBuyingPrice(minPrice, msg.value);
+    uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcPrice;
     require(amountRc > 0, "buyRC: receiving zero RCs");
     reserveCoin.mint(receiver, amountRc);
-    emit BoughtReserveCoins(msg.sender, receiver, amountRc, msg.value);
+    emit BoughtReserveCoins(msg.sender, receiver, amountRc, amountBc);
 }

 function deductFees(uint256 value, uint256 feeUi, address ui) internal returns (uint256) {
+    if (feeUi > 0) require(ui != address(0), "ui required when feeUi > 0");
     uint256 totalFees = ((value * FEE) + (value * feeUi) + (value * TREASURY_FEE)) / SCALING_FACTOR;
     transfer(TREASURY, (value * TREASURY_FEE) / SCALING_FACTOR);
     transfer(ui, (value * feeUi) / SCALING_FACTOR);
     return value - totalFees;
 }

Also applies to: 81-89, 102-111, 127-132


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

function L() external view returns (uint256) {
return L(scMaxPrice(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(scMaxPrice(_currentPaymentAmount), _currentPaymentAmount);
}

// # Public Trading Functions:
function buyStableCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
oracle.updateOracleValues();
uint256 amountBc = deductFees(msg.value, feeUi, ui);
uint256 amountSc = (amountBc * SC_DECIMAL_SCALING_FACTOR) / scMaxPrice(msg.value);
require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buySC: tx limit exceeded");
require(amountSc > 0, "buySC: receiving zero SCs");
stableCoin.mint(receiver, amountSc);
emit BoughtStableCoins(msg.sender, receiver, amountSc, msg.value);
}

function sellStableCoins(uint256 amountSc, address receiver, uint256 feeUi, address ui) external nonReentrant {
oracle.updateOracleValues();
require(stableCoin.balanceOf(msg.sender) >= amountSc, "sellSC: insufficient SC balance");
require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellSC: tx limit exceeded");
stableCoin.burn(msg.sender, amountSc);
uint256 amountBc = deductFees((amountSc * scMinPrice(0)) / SC_DECIMAL_SCALING_FACTOR, feeUi, ui);
require(amountBc > 0, "sellSC: receiving zero BCs");
transfer(receiver, amountBc);
emit SoldStableCoins(msg.sender, receiver, amountSc, amountBc);
}
Comment on lines +91 to +100
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: sell* overpays by burning before pricing (and RC recomputes price after burn).

  • sellStableCoins: burn happens before scMinPrice(0) (Line 95-96), increasing scPrice and overpaying sellers.
  • sellReserveCoins: rcTargetPrice(...) is called again after burn (Line 118-120), which can significantly increase payout.

Suggested fix: compute price (and gross payout) first, then burn, then deduct fees and transfer.

 function sellStableCoins(uint256 amountSc, address receiver, uint256 feeUi, address ui) external nonReentrant {
     oracle.updateOracleValues();
     require(stableCoin.balanceOf(msg.sender) >= amountSc, "sellSC: insufficient SC balance");
     require(amountSc <= TX_LIMIT || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellSC: tx limit exceeded");
-    stableCoin.burn(msg.sender, amountSc);
-    uint256 amountBc = deductFees((amountSc * scMinPrice(0)) / SC_DECIMAL_SCALING_FACTOR, feeUi, ui);
+    uint256 price = scMinPrice(0);
+    uint256 grossBc = (amountSc * price) / SC_DECIMAL_SCALING_FACTOR;
+    stableCoin.burn(msg.sender, amountSc);
+    uint256 amountBc = deductFees(grossBc, feeUi, ui);
     require(amountBc > 0, "sellSC: receiving zero BCs");
     transfer(receiver, amountBc);
     emit SoldStableCoins(msg.sender, receiver, amountSc, amountBc);
 }

 function sellReserveCoins(uint256 amountRc, address receiver, uint256 feeUi, address ui) external nonReentrant {
     oracle.updateOracleValues();
     require(reserveCoin.balanceOf(msg.sender) >= amountRc, "sellRC: insufficient RC balance");
     uint256 maxPrice = scMaxPrice(0);
-    require((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR <= (TX_LIMIT * maxPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
-    reserveCoin.burn(msg.sender, amountRc);
-    uint256 amountBc = deductFees((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR, feeUi, ui);
+    uint256 rcPrice = rcTargetPrice(maxPrice, 0);
+    uint256 grossBc = (amountRc * rcPrice) / RC_DECIMAL_SCALING_FACTOR;
+    require(grossBc <= (TX_LIMIT * maxPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
+    reserveCoin.burn(msg.sender, amountRc);
+    uint256 amountBc = deductFees(grossBc, feeUi, ui);
     require(amountBc > 0, "sellRC: receiving zero BCs");
     transfer(receiver, amountBc);
     emit SoldReserveCoins(msg.sender, receiver, amountRc, amountBc);
 }

Also applies to: 113-123, 156-170


function buyReserveCoins(address receiver, uint256 feeUi, address ui) external payable nonReentrant {
oracle.updateOracleValues();
uint256 amountBc = deductFees(msg.value, feeUi, ui);
uint256 minPrice = scMinPrice(msg.value);
require(amountBc <= (TX_LIMIT * minPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "buyRC: tx limit exceeded");
uint256 amountRc = (amountBc * RC_DECIMAL_SCALING_FACTOR) / rcBuyingPrice(minPrice, msg.value);
require(amountRc > 0, "buyRC: receiving zero RCs");
reserveCoin.mint(receiver, amountRc);
emit BoughtReserveCoins(msg.sender, receiver, amountRc, msg.value);
}

function sellReserveCoins(uint256 amountRc, address receiver, uint256 feeUi, address ui) external nonReentrant {
oracle.updateOracleValues();
require(reserveCoin.balanceOf(msg.sender) >= amountRc, "sellRC: insufficient RC balance");
uint256 maxPrice = scMaxPrice(0);
require((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR <= (TX_LIMIT * maxPrice) / SC_DECIMAL_SCALING_FACTOR || stableCoin.totalSupply() < THRESHOLD_SUPPLY_SC, "sellRC: tx limit exceeded");
reserveCoin.burn(msg.sender, amountRc);
uint256 amountBc = deductFees((amountRc * rcTargetPrice(maxPrice, 0)) / RC_DECIMAL_SCALING_FACTOR, feeUi, ui);
require(amountBc > 0, "sellRC: receiving zero BCs");
transfer(receiver, amountBc);
emit SoldReserveCoins(msg.sender, receiver, amountRc, amountBc);
}

// # Auxiliary Functions

function deductFees(uint256 value, uint256 feeUi, address ui) internal returns (uint256) {
uint256 totalFees = ((value * FEE) + (value * feeUi) + (value * TREASURY_FEE)) / SCALING_FACTOR;
transfer(TREASURY, (value * TREASURY_FEE) / SCALING_FACTOR);
transfer(ui, (value * feeUi) / SCALING_FACTOR);
return value - totalFees;
}

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

function scPrice(uint256 _currentPaymentAmount, uint256 scTargetPrice) private view returns (uint256) {
return stableCoin.totalSupply() == 0
? scTargetPrice
: Math.min(scTargetPrice, (R(_currentPaymentAmount) * SC_DECIMAL_SCALING_FACTOR) / stableCoin.totalSupply());
}

function scMaxPrice(uint256 _currentPaymentAmount) public view returns (uint256) {
(uint256 scTargetPrice, ) = oracle.readMaxPrice();
return scPrice(_currentPaymentAmount, scTargetPrice);
}

function scMinPrice(uint256 _currentPaymentAmount) public view returns (uint256) {
(uint256 scTargetPrice, ) = oracle.readMinPrice();
return scPrice(_currentPaymentAmount, scTargetPrice);
}

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

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

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

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

function transfer(address receiver, uint256 amount) internal {
(bool success, ) = payable(receiver).call{value: amount}("");
require(success, "Transfer failed.");
}
}