Skip to content
Merged
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: 5 additions & 0 deletions .changeset/safetrc20-try-get-decimals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-tron-solidity': minor
---

`SafeTRC20`: Add `tryGetDecimals` helper that safely queries a token's `decimals()` without reverting.
18 changes: 11 additions & 7 deletions contracts/token/TRC20/extensions/TRC20Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

pragma solidity ^0.8.20;

import {ITRC20, ITRC20Metadata, TRC20} from "../TRC20.sol";
import {ITRC20, TRC20} from "../TRC20.sol";
import {SafeTRC20} from "../utils/SafeTRC20.sol";
import {Math} from "../../../utils/math/Math.sol";

/**
* @dev Extension of the TRC-20 token contract to support token wrapping.
Expand Down Expand Up @@ -33,13 +34,16 @@ abstract contract TRC20Wrapper is TRC20 {
_underlying = underlyingToken;
}

/// @inheritdoc ITRC20Metadata
/**
* @dev See {ITRC20Metadata}. Uses {Math-ternary} for branchless selection, which evaluates both branches. This is safe
* because the default {TRC20-decimals} is commonly a constant.
*
* NOTE: If a derived contract overrides `super.decimals()` to read from
* storage, it should also override this function and use a conditional ternary instead.
*/
function decimals() public view virtual override returns (uint8) {
try ITRC20Metadata(address(_underlying)).decimals() returns (uint8 value) {
return value;
} catch {
return super.decimals();
}
(bool success, uint8 decimals_) = SafeTRC20.tryGetDecimals(_underlying);
return uint8(Math.ternary(success, decimals_, super.decimals())); // Safe cast. Both are uint8.
}

/**
Expand Down
21 changes: 1 addition & 20 deletions contracts/token/TRC20/extensions/TRC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ pragma solidity ^0.8.24;
import {ITRC20, ITRC20Metadata, TRC20} from "../TRC20.sol";
import {SafeTRC20} from "../utils/SafeTRC20.sol";
import {ITRC4626} from "../../../interfaces/ITRC4626.sol";
import {LowLevelCall} from "../../../utils/LowLevelCall.sol";
import {Memory} from "../../../utils/Memory.sol";
import {Math} from "../../../utils/math/Math.sol";

/**
Expand Down Expand Up @@ -98,28 +96,11 @@ abstract contract TRC4626 is TRC20, ITRC4626 {
* @dev Set the underlying asset contract. This must be a TRC20-compatible contract (TRC-20 or TRC-777).
*/
constructor(ITRC20 asset_) {
(bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_);
(bool success, uint8 assetDecimals) = SafeTRC20.tryGetDecimals(asset_);
_underlyingDecimals = success ? assetDecimals : 18;
_asset = asset_;
}

/**
* @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way.
*/
function _tryGetAssetDecimals(ITRC20 asset_) private view returns (bool ok, uint8 assetDecimals) {
Memory.Pointer ptr = Memory.getFreeMemoryPointer();
(bool success, bytes32 returnedDecimals, ) = LowLevelCall.staticcallReturn64Bytes(
address(asset_),
abi.encodeCall(ITRC20Metadata.decimals, ())
);
Memory.unsafeSetFreeMemoryPointer(ptr);

return
(success && LowLevelCall.returnDataSize() >= 32 && uint256(returnedDecimals) <= type(uint8).max)
? (true, uint8(uint256(returnedDecimals)))
: (false, 0);
}

/**
* @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This
* "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the
Expand Down
12 changes: 12 additions & 0 deletions contracts/token/TRC20/utils/SafeTRC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity ^0.8.20;

import {ITRC20} from "../ITRC20.sol";
import {ITRC1363} from "../../../interfaces/ITRC1363.sol";
import {ITRC20Metadata} from "../../../interfaces/ITRC20Metadata.sol";

/**
* @title SafeTRC20
Expand Down Expand Up @@ -336,4 +337,15 @@ library SafeTRC20 {
mstore(0x40, fmp)
}
}

/// @dev Attempts to fetch the token decimals. A return value of false indicates that the attempt failed in some way.
function tryGetDecimals(ITRC20 token) internal view returns (bool success, uint8 decimals) {
bytes4 selector = ITRC20Metadata.decimals.selector;
assembly ("memory-safe") {
mstore(0x00, selector)
success := staticcall(gas(), token, 0x00, 4, 0x00, 0x20)
success := and(and(success, gt(returndatasize(), 0x1f)), lt(mload(0x00), 0x100))
decimals := mul(success, mload(0x00))
}
}
}
30 changes: 30 additions & 0 deletions test/token/TRC20/utils/SafeTRC20.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ async function fixture() {
const trc20ForceApproveMock = await ethers.deployContract('$TRC20ForceApproveMock', [name, symbol]);
const trc20UsdtMock = await ethers.deployContract('$TRC20USDTMock', [name, symbol]);
const trc20UsdtFeeMock = await ethers.deployContract('$TRC20USDTFeeMock', [name, symbol]);
const trc20DecimalsMock = await ethers.deployContract('$TRC20DecimalsMock', [name, symbol, 6]);
const trc20ExcessDecimalsMock = await ethers.deployContract('$TRC20ExcessDecimalsMock');
const erc1363Mock = await ethers.deployContract('$TRC1363', [name, symbol]);
const erc1363ReturnFalseOnErc20Mock = await ethers.deployContract('$TRC1363ReturnFalseOnTRC20Mock', [name, symbol]);
const erc1363ReturnFalseMock = await ethers.deployContract('$TRC1363ReturnFalseMock', [name, symbol]);
Expand All @@ -38,6 +40,8 @@ async function fixture() {
trc20ForceApproveMock,
trc20UsdtMock,
trc20UsdtFeeMock,
trc20DecimalsMock,
trc20ExcessDecimalsMock,
erc1363Mock,
erc1363ReturnFalseOnErc20Mock,
erc1363ReturnFalseMock,
Expand Down Expand Up @@ -400,6 +404,32 @@ describe('SafeTRC20', function () {
});
});

describe('tryGetDecimals', function () {
it('returns decimals when token has standard 18 decimals', async function () {
const result = await this.mock.$tryGetDecimals(this.trc20ReturnTrueMock);
expect(result.success).to.be.true;
expect(result.decimals).to.equal(18n);
});

it('returns decimals when token has non-standard decimals', async function () {
const result = await this.mock.$tryGetDecimals(this.trc20DecimalsMock);
expect(result.success).to.be.true;
expect(result.decimals).to.equal(6n);
});

it('returns false when address has no code', async function () {
const result = await this.mock.$tryGetDecimals(this.hasNoCode);
expect(result.success).to.be.false;
expect(result.decimals).to.equal(0n);
});

it('returns false when token returns a value that does not fit in uint8', async function () {
const result = await this.mock.$tryGetDecimals(this.trc20ExcessDecimalsMock);
expect(result.success).to.be.false;
expect(result.decimals).to.equal(0n);
});
});

describe('with TRC1363 with usdt approval behaviour', function () {
beforeEach(async function () {
this.token = this.erc1363ForceApproveMock;
Expand Down
Loading