<100 subscribers

While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called "DVNFT", now at 999 ETH each
This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
from web3.auto import w3
txDecode = lambda x: w3.eth.account.privateKeyToAccount(x)
d1 = '''4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35'''
>>> base64.b64decode(bytes.fromhex(d1).decode("ASCII"))
b'0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
# => 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
txDecode('0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9').address
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15'
d2 = '''4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'''
# => 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
If we analyze the data extracted from the defi web application server mentioned in the previous problem, we can see that it is composed of a specific hex format and by converting it to ASCII format and performing base64 decode, we can extract pure data.
This allows us to check the Ethereum address value and, by analyzing its structure through the web3 module, we can confirm that it belongs to the Oracle address of this contract.
0xe92401A4d3af5E446d93D11EEc806b1462b39D15,0x81A5D6E50C214044bE44cA0CB057fe119097850c
attacker Signer
deployer
attacker
contract deploy
Exchange
DamnValuableNFT
TrustFulOracle
TrustFulOracleInitializer
hardhat_setBalance => 2ETH settings (Balance)
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
attacker Balance Settings => 0.1 ETH Settings (Balance)
Deploy the oracle and setup the trusted sources with initial prices
TrustFulOracle Contract <= TrustOracleInitializer Contract Setup
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
"DVNFT"
"DVNFT"
"DVNFT"
INITIAL_NFT_PRICE = 999 ether
INITIAL_NFT_PRICE = 999 ether
INITIAL_NFT_PRICE = 999 ether
Oracle Setup
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./TrustfulOracle.sol";
/**
* @title TrustfulOracleInitializer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrustfulOracleInitializer {
event NewTrustfulOracle(address oracleAddress);
TrustfulOracle public oracle;
constructor(
address[] memory sources,
string[] memory symbols,
uint256[] memory initialPrices
)
{
oracle = new TrustfulOracle(sources, true);
oracle.setupInitialPrices(sources, symbols, initialPrices);
emit NewTrustfulOracle(address(oracle));
}
}
Initialize the Sources, symbols, initialPrices state variables in the TrustfulOracleInitializer Contract Constructor
Initialize the TrustfulOracle Contract instance using the specified address array
// TrustfulOracle Contract
constructor(address[] memory sources, bool enableInitialization) {
require(sources.length > 0);
for(uint256 i = 0; i < sources.length; i++) {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
}
if (enableInitialization) {
_setupRole(INITIALIZER_ROLE, msg.sender);
}
}
Grant the TRUSTED_SOURCE_ROLE to the three addresses initialized in the setup.
Apply the INITIALIZER_ROLE permission to the msg.sender address based on the enableInitialization value.
Perform role separation
// TrustfulOracle Contract
// A handy utility allowing the deployer to setup initial prices (only once)
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
Use the _setPrice function to specify the necessary variables and manage the data in the mapping structure.
If we check the function, we see that it assigns and processes the event based on the price value for the symbol based on the address in the pricesBySource double mapping structure.
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
After completing the overall setup, use renounceRole to delete the initial setup permission for msg.sender.
TrustFulOracle Contract address
msg.value{EXCHANGE_INITIAL_ETH_BALANCE=9990ether}
constructor(address oracleAddress) payable {
token = new DamnValuableNFT();
oracle = TrustfulOracle(oracleAddress);
}
We can see that the previously initialized TrustfulOracle contract and NFT token contract are being set up.
By analyzing the deployment script, we can see that the Exchange Contract has a balance of 9990 ether because the msg.value value was assigned when this contract was deployed."
exchange contract => token func()
this.nftToken = await DamnValuableNFTFactory.attach(await this.exchange.token());
Exchange Contract Balance is 0
Attacker Address Balance is 9990 ether
nftToken Balance Attacker is 0
oracle => getMedianPrice(“DVNFT”) == 999 ether
TrustOracleInitializer.sol

The Oracle initialization contract is also a TrustFulOracle that assigns code data and permissions, and then assigns instances.
Use the setupInitialPrices function to assign initial prices based on the Oracle instance.
Process transaction events.
TrustfulOracle.sol


Exchange.sol

function setupInitialPrices(address[] memory sources, string[] memory symbols, uint256[] memory prices) public onlyInitializer
✅ The lengths of the sources and symbols arrays must be the same, and the lengths of the symbols and prices arrays must also be the same in order for the condition to be satisfied.
The _setPrice function is called based on the length of the sources data, with the source, symbols, and prices arrays being assigned to meet the internal setup criteria and perform the operation.
If we examine the function, we can see that the pricesBySource double mapping structure assigns the price to each value based on the address and symbol value, and processes the event.
The renounceRole function is used to remove the INITIALIZER_ROLE permission from msg.sender.
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource
The postPrice function is used to set a single price and is only available to trusted sources as defined by the onlyTrustedSource modifier. It calls the _setPrice function and passes in the msg.sender, symbol, and newPrice as arguments.
function getMedianPrice(string calldata symbol) external view returns (uint256)
The getMedianPrice function is a view function that returns the median price for the given symbol by calling the _computeMedianPrice function and passing in the symbol.
function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory
The getAllPricesForSymbol function returns an array of all the prices for the given symbol. It does this by first getting the number of trusted sources with the TRUSTED_SOURCE_ROLE permission and then creating a prices array of that size. It then uses the getPriceBySource function to copy the set prices for the given symbol into the prices array and returns it.
function _computeMedianPrice(string memory symbol) private view returns (uint256)
The _computeMedianPrice function is a private view function that returns the median price for the given symbol. It does this by first getting all the prices for the symbol using the getAllPricesForSymbol function and then sorting the array. It then returns the middle value of the sorted array as the median price.
If the value is 0 when the length value of the prices array is mod 2, perform the following logic when it is a multiple of 2
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
Otherwise, the following ~
return prices[prices.length / 2];
function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(
amountPaidInWei >= currentPriceInWei,
"Amount paid is not enough"
);
uint256 tokenId = token.safeMint(msg.sender);
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
function sellOne(uint256 tokenId) external nonReentrant {
require(
msg.sender == token.ownerOf(tokenId),
"Seller must be the owner"
);
require(
token.getApproved(tokenId) == address(this),
"Seller must have approved transfer"
);
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(
address(this).balance >= currentPriceInWei,
"Not enough ETH in balance"
);
token.transferFrom(msg.sender, address(this), tokenId);
token.burn(tokenId);
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
Exchanges use the getMedianPrice() method to calculate the median price of an NFT from the oracle object to use in the actual purchase function. (buyOne, sellOne)
function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}
// [OMIT]
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));
// calculate median price
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
Exchange is using the getMedianPrice() method to calculate the median value of the NFT price from the oracle object for actual purchase functionality. The addresses used to determine the original value of this NFT price can be seen in the deployment code previously. 3 addresses can be used to check the dependent user accounts of this contract, each used to determine the ownership and median value of the NFT price.
from web3.auto import w3
txDecode = lambda x: w3.eth.account.privateKeyToAccount(x)
d1 = '''4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35'''
>>> base64.b64decode(bytes.fromhex(d1).decode("ASCII"))
b'0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
# => 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
txDecode('0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9').address
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15'
d2 = '''4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'''
# => 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
We previously received specific hex data from the target web application server and analyzed it to confirm that it was the Oracle address of the contract. Among them, you can check the private keys of '0xe92401A4d3af5E446d93D11EEEC806b1462b39D15' and '0x81A5D6E50C214044bE44cA0CB057fe119097850c’
If we can set some half of the data in the logic of obtaining this intermediate value, we can confirm that a certain intermediate price can be arbitrarily created and generated at the desired target. In other words, problems arise with the mechanisms used to update Oracle prices.
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
Calling the postPrice method after the Oracle price is updated exists in additional individual Oracle. Because an exposed Oracle private key exists, all transactions can be created, so you can create as many transactions as you want.


Thank you for the @tinchoabbate that made a good wargame.

While poking around a web service of one of the most popular DeFi projects in the space, you get a somewhat strange response from their server. This is a snippet:
HTTP/2 200 OK
content-type: text/html
content-language: en
vary: Accept-Encoding
server: cloudflare
4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35
4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34
A related on-chain exchange is selling (absurdly overpriced) collectibles called "DVNFT", now at 999 ETH each
This price is fetched from an on-chain oracle, and is based on three trusted reporters: 0xA73209FB1a42495120166736362A1DfA9F95A105,0xe92401A4d3af5E446d93D11EEc806b1462b39D15 and 0x81A5D6E50C214044bE44cA0CB057fe119097850c.
Starting with only 0.1 ETH in balance, you must steal all ETH available in the exchange.
from web3.auto import w3
txDecode = lambda x: w3.eth.account.privateKeyToAccount(x)
d1 = '''4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35'''
>>> base64.b64decode(bytes.fromhex(d1).decode("ASCII"))
b'0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
# => 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
txDecode('0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9').address
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15'
d2 = '''4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'''
# => 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
If we analyze the data extracted from the defi web application server mentioned in the previous problem, we can see that it is composed of a specific hex format and by converting it to ASCII format and performing base64 decode, we can extract pure data.
This allows us to check the Ethereum address value and, by analyzing its structure through the web3 module, we can confirm that it belongs to the Oracle address of this contract.
0xe92401A4d3af5E446d93D11EEc806b1462b39D15,0x81A5D6E50C214044bE44cA0CB057fe119097850c
attacker Signer
deployer
attacker
contract deploy
Exchange
DamnValuableNFT
TrustFulOracle
TrustFulOracleInitializer
hardhat_setBalance => 2ETH settings (Balance)
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
attacker Balance Settings => 0.1 ETH Settings (Balance)
Deploy the oracle and setup the trusted sources with initial prices
TrustFulOracle Contract <= TrustOracleInitializer Contract Setup
'0xA73209FB1a42495120166736362A1DfA9F95A105',
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15',
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
"DVNFT"
"DVNFT"
"DVNFT"
INITIAL_NFT_PRICE = 999 ether
INITIAL_NFT_PRICE = 999 ether
INITIAL_NFT_PRICE = 999 ether
Oracle Setup
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./TrustfulOracle.sol";
/**
* @title TrustfulOracleInitializer
* @author Damn Vulnerable DeFi (https://damnvulnerabledefi.xyz)
*/
contract TrustfulOracleInitializer {
event NewTrustfulOracle(address oracleAddress);
TrustfulOracle public oracle;
constructor(
address[] memory sources,
string[] memory symbols,
uint256[] memory initialPrices
)
{
oracle = new TrustfulOracle(sources, true);
oracle.setupInitialPrices(sources, symbols, initialPrices);
emit NewTrustfulOracle(address(oracle));
}
}
Initialize the Sources, symbols, initialPrices state variables in the TrustfulOracleInitializer Contract Constructor
Initialize the TrustfulOracle Contract instance using the specified address array
// TrustfulOracle Contract
constructor(address[] memory sources, bool enableInitialization) {
require(sources.length > 0);
for(uint256 i = 0; i < sources.length; i++) {
_setupRole(TRUSTED_SOURCE_ROLE, sources[i]);
}
if (enableInitialization) {
_setupRole(INITIALIZER_ROLE, msg.sender);
}
}
Grant the TRUSTED_SOURCE_ROLE to the three addresses initialized in the setup.
Apply the INITIALIZER_ROLE permission to the msg.sender address based on the enableInitialization value.
Perform role separation
// TrustfulOracle Contract
// A handy utility allowing the deployer to setup initial prices (only once)
function setupInitialPrices(
address[] memory sources,
string[] memory symbols,
uint256[] memory prices
)
public
onlyInitializer
{
// Only allow one (symbol, price) per source
require(sources.length == symbols.length && symbols.length == prices.length);
for(uint256 i = 0; i < sources.length; i++) {
_setPrice(sources[i], symbols[i], prices[i]);
}
renounceRole(INITIALIZER_ROLE, msg.sender);
}
Use the _setPrice function to specify the necessary variables and manage the data in the mapping structure.
If we check the function, we see that it assigns and processes the event based on the price value for the symbol based on the address in the pricesBySource double mapping structure.
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
After completing the overall setup, use renounceRole to delete the initial setup permission for msg.sender.
TrustFulOracle Contract address
msg.value{EXCHANGE_INITIAL_ETH_BALANCE=9990ether}
constructor(address oracleAddress) payable {
token = new DamnValuableNFT();
oracle = TrustfulOracle(oracleAddress);
}
We can see that the previously initialized TrustfulOracle contract and NFT token contract are being set up.
By analyzing the deployment script, we can see that the Exchange Contract has a balance of 9990 ether because the msg.value value was assigned when this contract was deployed."
exchange contract => token func()
this.nftToken = await DamnValuableNFTFactory.attach(await this.exchange.token());
Exchange Contract Balance is 0
Attacker Address Balance is 9990 ether
nftToken Balance Attacker is 0
oracle => getMedianPrice(“DVNFT”) == 999 ether
TrustOracleInitializer.sol

The Oracle initialization contract is also a TrustFulOracle that assigns code data and permissions, and then assigns instances.
Use the setupInitialPrices function to assign initial prices based on the Oracle instance.
Process transaction events.
TrustfulOracle.sol


Exchange.sol

function setupInitialPrices(address[] memory sources, string[] memory symbols, uint256[] memory prices) public onlyInitializer
✅ The lengths of the sources and symbols arrays must be the same, and the lengths of the symbols and prices arrays must also be the same in order for the condition to be satisfied.
The _setPrice function is called based on the length of the sources data, with the source, symbols, and prices arrays being assigned to meet the internal setup criteria and perform the operation.
If we examine the function, we can see that the pricesBySource double mapping structure assigns the price to each value based on the address and symbol value, and processes the event.
The renounceRole function is used to remove the INITIALIZER_ROLE permission from msg.sender.
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource
The postPrice function is used to set a single price and is only available to trusted sources as defined by the onlyTrustedSource modifier. It calls the _setPrice function and passes in the msg.sender, symbol, and newPrice as arguments.
function getMedianPrice(string calldata symbol) external view returns (uint256)
The getMedianPrice function is a view function that returns the median price for the given symbol by calling the _computeMedianPrice function and passing in the symbol.
function getAllPricesForSymbol(string memory symbol) public view returns (uint256[] memory
The getAllPricesForSymbol function returns an array of all the prices for the given symbol. It does this by first getting the number of trusted sources with the TRUSTED_SOURCE_ROLE permission and then creating a prices array of that size. It then uses the getPriceBySource function to copy the set prices for the given symbol into the prices array and returns it.
function _computeMedianPrice(string memory symbol) private view returns (uint256)
The _computeMedianPrice function is a private view function that returns the median price for the given symbol. It does this by first getting all the prices for the symbol using the getAllPricesForSymbol function and then sorting the array. It then returns the middle value of the sorted array as the median price.
If the value is 0 when the length value of the prices array is mod 2, perform the following logic when it is a multiple of 2
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
Otherwise, the following ~
return prices[prices.length / 2];
function buyOne() external payable nonReentrant returns (uint256) {
uint256 amountPaidInWei = msg.value;
require(amountPaidInWei > 0, "Amount paid must be greater than zero");
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(
amountPaidInWei >= currentPriceInWei,
"Amount paid is not enough"
);
uint256 tokenId = token.safeMint(msg.sender);
payable(msg.sender).sendValue(amountPaidInWei - currentPriceInWei);
emit TokenBought(msg.sender, tokenId, currentPriceInWei);
return tokenId;
}
function sellOne(uint256 tokenId) external nonReentrant {
require(
msg.sender == token.ownerOf(tokenId),
"Seller must be the owner"
);
require(
token.getApproved(tokenId) == address(this),
"Seller must have approved transfer"
);
// Price should be in [wei / NFT]
uint256 currentPriceInWei = oracle.getMedianPrice(token.symbol());
require(
address(this).balance >= currentPriceInWei,
"Not enough ETH in balance"
);
token.transferFrom(msg.sender, address(this), tokenId);
token.burn(tokenId);
payable(msg.sender).sendValue(currentPriceInWei);
emit TokenSold(msg.sender, tokenId, currentPriceInWei);
}
Exchanges use the getMedianPrice() method to calculate the median price of an NFT from the oracle object to use in the actual purchase function. (buyOne, sellOne)
function getMedianPrice(string calldata symbol) external view returns (uint256) {
return _computeMedianPrice(symbol);
}
// [OMIT]
function _computeMedianPrice(string memory symbol) private view returns (uint256) {
uint256[] memory prices = _sort(getAllPricesForSymbol(symbol));
// calculate median price
if (prices.length % 2 == 0) {
uint256 leftPrice = prices[(prices.length / 2) - 1];
uint256 rightPrice = prices[prices.length / 2];
return (leftPrice + rightPrice) / 2;
} else {
return prices[prices.length / 2];
}
}
Exchange is using the getMedianPrice() method to calculate the median value of the NFT price from the oracle object for actual purchase functionality. The addresses used to determine the original value of this NFT price can be seen in the deployment code previously. 3 addresses can be used to check the dependent user accounts of this contract, each used to determine the ownership and median value of the NFT price.
from web3.auto import w3
txDecode = lambda x: w3.eth.account.privateKeyToAccount(x)
d1 = '''4d 48 68 6a 4e 6a 63 34 5a 57 59 78 59 57 45 30 4e 54 5a 6b 59 54 59 31 59 7a 5a 6d 59 7a 55 34 4e 6a 46 6b 4e 44 51 34 4f 54 4a 6a 5a 47 5a 68 59 7a 42 6a 4e 6d 4d 34 59 7a 49 31 4e 6a 42 69 5a 6a 42 6a 4f 57 5a 69 59 32 52 68 5a 54 4a 6d 4e 44 63 7a 4e 57 45 35'''
>>> base64.b64decode(bytes.fromhex(d1).decode("ASCII"))
b'0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9'
# => 0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9
txDecode('0xc678ef1aa456da65c6fc5861d44892cdfac0c6c8c2560bf0c9fbcdae2f4735a9').address
'0xe92401A4d3af5E446d93D11EEc806b1462b39D15'
d2 = '''4d 48 67 79 4d 44 67 79 4e 44 4a 6a 4e 44 42 68 59 32 52 6d 59 54 6c 6c 5a 44 67 34 4f 57 55 32 4f 44 56 6a 4d 6a 4d 31 4e 44 64 68 59 32 4a 6c 5a 44 6c 69 5a 57 5a 6a 4e 6a 41 7a 4e 7a 46 6c 4f 54 67 33 4e 57 5a 69 59 32 51 33 4d 7a 59 7a 4e 44 42 69 59 6a 51 34'''
# => 0x208242c40acdfa9ed889e685c23547acbed9befc60371e9875fbcd736340bb48
'0x81A5D6E50C214044bE44cA0CB057fe119097850c'
We previously received specific hex data from the target web application server and analyzed it to confirm that it was the Oracle address of the contract. Among them, you can check the private keys of '0xe92401A4d3af5E446d93D11EEEC806b1462b39D15' and '0x81A5D6E50C214044bE44cA0CB057fe119097850c’
If we can set some half of the data in the logic of obtaining this intermediate value, we can confirm that a certain intermediate price can be arbitrarily created and generated at the desired target. In other words, problems arise with the mechanisms used to update Oracle prices.
function postPrice(string calldata symbol, uint256 newPrice) external onlyTrustedSource {
_setPrice(msg.sender, symbol, newPrice);
}
function _setPrice(address source, string memory symbol, uint256 newPrice) private {
uint256 oldPrice = pricesBySource[source][symbol];
pricesBySource[source][symbol] = newPrice;
emit UpdatedPrice(source, symbol, oldPrice, newPrice);
}
Calling the postPrice method after the Oracle price is updated exists in additional individual Oracle. Because an exposed Oracle private key exists, all transactions can be created, so you can create as many transactions as you want.


Thank you for the @tinchoabbate that made a good wargame.
Share Dialog
Share Dialog
No comments yet