Skip to content

Commit c620d43

Browse files
arr00Amxx
andauthored
Revert when wrapper is full (#268) (#271)
* Revert when wrapper full * up * simplify and add tests * update docs * cei * add doc * up * add custom error and fix tests * comment clarification * Rename `_checkTotalSupply` to `_checkConfidentialTotalSupply` * add changeset * Update contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol * docs --------- Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
1 parent f0914b6 commit c620d43

File tree

3 files changed

+103
-19
lines changed

3 files changed

+103
-19
lines changed

.changeset/nasty-walls-taste.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-confidential-contracts': patch
3+
---
4+
5+
`ERC7984ERC20Wrapper`: revert on wrap if there is a chance of total supply overflow.

contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC1363Receiver {
3030
event UnwrapFinalized(address indexed receiver, euint64 encryptedAmount, uint64 cleartextAmount);
3131

3232
error InvalidUnwrapRequest(euint64 amount);
33+
error ERC7984TotalSupplyOverflow();
3334

3435
constructor(IERC20 underlying_) {
3536
_underlying = underlying_;
@@ -45,28 +46,10 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC1363Receiver {
4546
}
4647
}
4748

48-
/// @inheritdoc ERC7984
49-
function decimals() public view virtual override returns (uint8) {
50-
return _decimals;
51-
}
52-
53-
/**
54-
* @dev Returns the rate at which the underlying token is converted to the wrapped token.
55-
* For example, if the `rate` is 1000, then 1000 units of the underlying token equal 1 unit of the wrapped token.
56-
*/
57-
function rate() public view virtual returns (uint256) {
58-
return _rate;
59-
}
60-
61-
/// @dev Returns the address of the underlying ERC-20 token that is being wrapped.
62-
function underlying() public view returns (IERC20) {
63-
return _underlying;
64-
}
65-
6649
/**
6750
* @dev `ERC1363` callback function which wraps tokens to the address specified in `data` or
6851
* the address `from` (if no address is specified in `data`). This function refunds any excess tokens
69-
* sent beyond the nearest multiple of {rate}. See {wrap} from more details on wrapping tokens.
52+
* sent beyond the nearest multiple of {rate} to `from`. See {wrap} from more details on wrapping tokens.
7053
*/
7154
function onTransferReceived(
7255
address /*operator*/,
@@ -149,6 +132,61 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC1363Receiver {
149132
emit UnwrapFinalized(to, burntAmount, burntAmountCleartext);
150133
}
151134

135+
/// @inheritdoc ERC7984
136+
function decimals() public view virtual override returns (uint8) {
137+
return _decimals;
138+
}
139+
140+
/**
141+
* @dev Returns the rate at which the underlying token is converted to the wrapped token.
142+
* For example, if the `rate` is 1000, then 1000 units of the underlying token equal 1 unit of the wrapped token.
143+
*/
144+
function rate() public view virtual returns (uint256) {
145+
return _rate;
146+
}
147+
148+
/// @dev Returns the address of the underlying ERC-20 token that is being wrapped.
149+
function underlying() public view returns (IERC20) {
150+
return _underlying;
151+
}
152+
153+
/**
154+
* @dev Returns the underlying balance divided by the {rate}, a value greater or equal to the actual
155+
* {confidentialTotalSupply}.
156+
*
157+
* NOTE: The return value of this function can be inflated by directly sending underlying tokens to the wrapper contract.
158+
* Reductions will lag compared to {confidentialTotalSupply} since it is updated on {unwrap} while this function updates
159+
* on {finalizeUnwrap}.
160+
*/
161+
function totalSupply() public view virtual returns (uint256) {
162+
return underlying().balanceOf(address(this)) / rate();
163+
}
164+
165+
/// @dev Returns the maximum total supply of wrapped tokens supported by the encrypted datatype.
166+
function maxTotalSupply() public view virtual returns (uint256) {
167+
return type(uint64).max;
168+
}
169+
170+
/**
171+
* @dev This function must revert if the new {confidentialTotalSupply} is invalid (overflow occurred).
172+
*
173+
* NOTE: Overflow can be detected here since the wrapper holdings are non-confidential. In other cases, it may be impossible
174+
* to infer total supply overflow synchronously. This function may revert even if the {confidentialTotalSupply} did
175+
* not overflow.
176+
*/
177+
function _checkConfidentialTotalSupply() internal virtual {
178+
if (totalSupply() > maxTotalSupply()) {
179+
revert ERC7984TotalSupplyOverflow();
180+
}
181+
}
182+
183+
function _update(address from, address to, euint64 amount) internal virtual override returns (euint64) {
184+
if (from == address(0)) {
185+
_checkConfidentialTotalSupply();
186+
}
187+
return super._update(from, to, amount);
188+
}
189+
152190
function _unwrap(address from, address to, euint64 amount) internal virtual {
153191
require(to != address(0), ERC7984InvalidReceiver(to));
154192
require(from == msg.sender || isOperator(from, msg.sender), ERC7984UnauthorizedSpender(from, msg.sender));

test/token/ERC7984/extensions/ERC7984Wrapper.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,47 @@ describe('ERC7984Wrapper', function () {
8282
).to.eventually.equal(10);
8383
});
8484

85+
it('max amount works', async function () {
86+
await this.token.$_mint(this.holder.address, ethers.MaxUint256 / 2n); // mint a lot of tokens
87+
88+
const rate = await this.wrapper.rate();
89+
const maxConfidentialSupply = await this.wrapper.maxTotalSupply();
90+
const maxUnderlyingBalance = maxConfidentialSupply * rate;
91+
92+
if (viaCallback) {
93+
await this.token.connect(this.holder).transferAndCall(this.wrapper, maxUnderlyingBalance);
94+
} else {
95+
await this.wrapper.connect(this.holder).wrap(this.holder.address, maxUnderlyingBalance);
96+
}
97+
98+
await expect(
99+
fhevm.userDecryptEuint(
100+
FhevmType.euint64,
101+
await this.wrapper.confidentialBalanceOf(this.holder.address),
102+
this.wrapper.target,
103+
this.holder,
104+
),
105+
).to.eventually.equal(maxConfidentialSupply);
106+
});
107+
108+
it('amount exceeding max fails', async function () {
109+
await this.token.$_mint(this.holder.address, ethers.MaxUint256 / 2n); // mint a lot of tokens
110+
111+
const rate = await this.wrapper.rate();
112+
const maxConfidentialSupply = await this.wrapper.maxTotalSupply();
113+
const maxUnderlyingBalance = maxConfidentialSupply * rate;
114+
115+
// first deposit close to the max
116+
await this.wrapper.connect(this.holder).wrap(this.holder.address, maxUnderlyingBalance);
117+
118+
// try to deposit more, causing the total supply to exceed the max supported amount
119+
await expect(
120+
viaCallback
121+
? this.token.connect(this.holder).transferAndCall(this.wrapper, rate)
122+
: this.wrapper.connect(this.holder).wrap(this.holder.address, rate),
123+
).to.be.revertedWithCustomError(this.wrapper, 'ERC7984TotalSupplyOverflow');
124+
});
125+
85126
if (viaCallback) {
86127
it('to another address', async function () {
87128
const amountToWrap = ethers.parseUnits('100', 18);

0 commit comments

Comments
 (0)