Skip to content

Commit 745c19d

Browse files
authored
Merge pull request #58 from AztecProtocol/pw/full-math
Pw/full math
2 parents 7561ae0 + 21560f5 commit 745c19d

File tree

8 files changed

+544
-258
lines changed

8 files changed

+544
-258
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
yarn setup:foundry
2323
yarn compile:typechain
2424
- run:
25-
name: Test Contracts No Element
25+
name: Test Contracts
2626
shell: /bin/bash
2727
command: |
2828
yarn config set script-shell /bin/bash

package.json

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@
1010
"clean": "rm -rf ./cache ./dest ./out ./artifacts ./typechain-types ./client-dest",
1111
"test": "yarn test:contracts && yarn test:client",
1212
"test:client": "yarn compile:typechain && jest test",
13-
"test:element": "forge test --fork-block-number 14000000 --match-contract Element -vvv --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c",
14-
"test:set": "forge test --fork-block-number 14000000 --match-contract Set -vvv --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c",
13+
"test:pinned": "forge test --fork-block-number 14000000 --match-contract 'Element|Set' -vvv --fork-url https://mainnet.infura.io/v3/9928b52099854248b3a096be07a6b23c",
1514
"test:contracts:block": "forge test --fork-block-number",
1615
"cast": "cast",
17-
"test:contracts": "forge test --no-match-contract 'Element|Set' -vvv && yarn test:element && yarn test:set",
18-
"test:contracts:noelement": "forge test --no-match-contract Element -vvv",
16+
"test:contracts": "forge test --no-match-contract 'Element|Set' -vvv && yarn test:pinned",
1917
"build": "yarn clean && yarn compile:typechain && yarn compile:client-dest"
2018
},
2119
"dependencies": {
@@ -29,7 +27,7 @@
2927
"rootDir": "./src"
3028
},
3129
"name": "@aztec/bridge-clients",
32-
"version": "0.1.32",
30+
"version": "0.1.33",
3331
"description": "This repo contains the solidity files and typescript helper class for all of the Aztec Connect Bridge Contracts",
3432
"repository": "[email protected]:AztecProtocol/aztec-connect-bridges.git",
3533
"license": "MIT",

src/bridges/element/ElementBridge.sol

Lines changed: 79 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {IERC20Permit, IERC20} from '../../interfaces/IERC20Permit.sol';
1212
import {IWrappedPosition} from './interfaces/IWrappedPosition.sol';
1313
import {IRollupProcessor} from '../../interfaces/IRollupProcessor.sol';
1414
import {MinHeap} from './MinHeap.sol';
15+
import {FullMath} from '../uniswapv3/libraries/FullMath.sol';
1516

1617
import {IDefiBridge} from '../../interfaces/IDefiBridge.sol';
1718

@@ -50,6 +51,9 @@ contract ElementBridge is IDefiBridge {
5051
error UNREGISTERED_POOL();
5152
error UNREGISTERED_POSITION();
5253
error UNREGISTERED_PAIR();
54+
error INVALID_TOKEN_BALANCE_RECEIVED();
55+
error INVALID_CHANGE_IN_BALANCE();
56+
error RECEIVED_LESS_THAN_LIMIT();
5357

5458
/*----------------------------------------
5559
STRUCTS
@@ -144,21 +148,21 @@ contract ElementBridge is IDefiBridge {
144148
// 48 hours in seconds, usd for calculating speeedbump expiries
145149
uint256 internal constant _FORTY_EIGHT_HOURS = 172800;
146150

147-
uint256 internal constant MAX_INT = 2**256 - 1;
151+
uint256 internal constant MAX_UINT = type(uint256).max;
148152

149153
uint256 internal constant MIN_GAS_FOR_CHECK_AND_FINALISE = 50000;
150154
uint256 internal constant MIN_GAS_FOR_FUNCTION_COMPLETION = 5000;
151155
uint256 internal constant MIN_GAS_FOR_FAILED_INTERACTION = 20000;
152156
uint256 internal constant MIN_GAS_FOR_EXPIRY_REMOVAL = 25000;
153157

154158
// event emitted on every successful convert call
155-
event Convert(uint256 indexed nonce, uint256 totalInputValue);
159+
event LogConvert(uint256 indexed nonce, uint256 totalInputValue);
156160

157161
// event emitted on every attempt to finalise, successful or otherwise
158-
event Finalise(uint256 indexed nonce, bool success, string message);
162+
event LogFinalise(uint256 indexed nonce, bool success, string message);
159163

160164
// event emitted on wvery newly configured pool
161-
event PoolAdded(address poolAddress, address wrappedPositionAddress, uint64 expiry);
165+
event LogPoolAdded(address poolAddress, address wrappedPositionAddress, uint64 expiry);
162166

163167
/**
164168
* @dev Constructor
@@ -320,7 +324,7 @@ contract ElementBridge is IDefiBridge {
320324
}
321325

322326
// verify with Element that the provided contracts are registered
323-
validatePositionAndPoolAddresses(wrappedPositionAddress, poolAddress);
327+
validatePositionAndPoolAddressesWithElementRegistry(wrappedPositionAddress, poolAddress);
324328

325329
// we store the pool information against a hash of the asset and expiry
326330
uint256 assetExpiryHash = hashAssetAndExpiry(poolSpec.underlyingAsset, trancheExpiry);
@@ -338,9 +342,9 @@ contract ElementBridge is IDefiBridge {
338342
// initialising the expiry -> nonce mapping here like this reduces a chunk of gas later when we start to add interactions for this expiry
339343
uint256[] storage nonces = expiryToNonce[trancheExpiry];
340344
if (nonces.length == 0) {
341-
expiryToNonce[trancheExpiry].push(MAX_INT);
345+
expiryToNonce[trancheExpiry].push(MAX_UINT);
342346
}
343-
emit PoolAdded(poolAddress, wrappedPositionAddress, trancheExpiry);
347+
emit LogPoolAdded(poolAddress, wrappedPositionAddress, trancheExpiry);
344348
}
345349

346350
/**
@@ -375,7 +379,7 @@ contract ElementBridge is IDefiBridge {
375379
* @param wrappedPosition address of a wrapped position contract
376380
* @param pool address of a balancer pool contract
377381
*/
378-
function validatePositionAndPoolAddresses(address wrappedPosition, address pool) internal {
382+
function validatePositionAndPoolAddressesWithElementRegistry(address wrappedPosition, address pool) internal {
379383
IDeploymentValidator validator = IDeploymentValidator(elementDeploymentValidatorAddress);
380384
if (!validator.checkPoolValidation(pool)) {
381385
revert UNREGISTERED_POOL();
@@ -458,26 +462,7 @@ contract ElementBridge is IDefiBridge {
458462
// approve the transfer of tokens to the balancer address
459463
ERC20(inputAssetA.erc20Address).approve(balancerAddress, totalInputValue);
460464
// execute the swap on balancer
461-
address inputAsset = inputAssetA.erc20Address;
462-
463-
uint256 principalTokensAmount = IVault(balancerAddress).swap(
464-
IVault.SingleSwap({
465-
poolId: pool.poolId,
466-
kind: IVault.SwapKind.GIVEN_IN,
467-
assetIn: IAsset(inputAsset),
468-
assetOut: IAsset(pool.trancheAddress),
469-
amount: totalInputValue,
470-
userData: '0x00'
471-
}),
472-
IVault.FundManagement({
473-
sender: address(this), // the bridge has already received the tokens from the rollup so it owns totalInputValue of inputAssetA
474-
fromInternalBalance: false,
475-
recipient: payable(address(this)),
476-
toInternalBalance: false
477-
}),
478-
totalInputValue,
479-
block.timestamp
480-
);
465+
uint256 principalTokensAmount = exchangeAssetForTrancheTokens(inputAssetA.erc20Address, pool, totalInputValue);
481466
// store the tranche that underpins our interaction, the expiry and the number of received tokens against the nonce
482467
Interaction storage newInteraction = interactions[interactionNonce];
483468
newInteraction.expiry = trancheExpiry;
@@ -486,17 +471,65 @@ contract ElementBridge is IDefiBridge {
486471
newInteraction.quantityPT = principalTokensAmount;
487472
newInteraction.trancheAddress = pool.trancheAddress;
488473
// add the nonce and expiry to our expiry heap
489-
addNonceAndExpiry(interactionNonce, trancheExpiry);
474+
addNonceAndExpiryToNonceMapping(interactionNonce, trancheExpiry);
490475
// increase our tranche account deposits and holdings
491476
// other members are left as their initial values (all zeros)
492477
TrancheAccount storage trancheAccount = trancheAccounts[newInteraction.trancheAddress];
493478
trancheAccount.numDeposits++;
494479
trancheAccount.quantityTokensHeld += newInteraction.quantityPT;
495-
emit Convert(interactionNonce, totalInputValue);
480+
emit LogConvert(interactionNonce, totalInputValue);
496481
finaliseExpiredInteractions(MIN_GAS_FOR_FUNCTION_COMPLETION);
497482
// we need to get here with MIN_GAS_FOR_FUNCTION_COMPLETION gas to exit.
498483
}
499484

485+
/**
486+
* @dev Function to exchange the input asset for tranche tokens on Balancer
487+
* @param inputAsset the address of the asset we want to swap
488+
* @param pool storage struct containing details of the pool we wish to use for the swap
489+
* @param inputQuantity the quantity of the input asset we wish to swap
490+
* @return quantityReceived amount of tokens recieved
491+
*/
492+
function exchangeAssetForTrancheTokens(address inputAsset, Pool storage pool, uint256 inputQuantity) internal returns (uint256 quantityReceived) {
493+
IVault.SingleSwap memory singleSwap = IVault.SingleSwap({
494+
poolId: pool.poolId, // the id of the pool we want to use
495+
kind: IVault.SwapKind.GIVEN_IN, // We are exchanging a given number of input tokens
496+
assetIn: IAsset(inputAsset), // the input asset for the swap
497+
assetOut: IAsset(pool.trancheAddress), // the tranche token address as the output asset
498+
amount: inputQuantity, // the total amount of input asset we wish to swap
499+
userData: '0x00' // set to 0 as per the docs, this is unused in current balancer pools
500+
});
501+
IVault.FundManagement memory fundManagement = IVault.FundManagement({
502+
sender: address(this), // the bridge has already received the tokens from the rollup so it owns totalInputValue of inputAssetA
503+
fromInternalBalance: false,
504+
recipient: payable(address(this)), // we want the output tokens transferred back to us
505+
toInternalBalance: false
506+
});
507+
508+
uint256 trancheTokenQuantityBefore = ERC20(pool.trancheAddress).balanceOf(address(this));
509+
quantityReceived = IVault(balancerAddress).swap(
510+
singleSwap,
511+
fundManagement,
512+
inputQuantity, // we won't accept less than 1 output token per input token
513+
block.timestamp
514+
);
515+
516+
uint256 trancheTokenQuantityAfter = ERC20(pool.trancheAddress).balanceOf(address(this));
517+
// ensure we haven't lost tokens!
518+
if (trancheTokenQuantityAfter < trancheTokenQuantityBefore) {
519+
revert INVALID_CHANGE_IN_BALANCE();
520+
}
521+
// change in balance must be >= 0 here
522+
uint256 changeInBalance = trancheTokenQuantityAfter - trancheTokenQuantityBefore;
523+
// ensure the change in balance matches that reported to us
524+
if (changeInBalance != quantityReceived) {
525+
revert INVALID_TOKEN_BALANCE_RECEIVED();
526+
}
527+
// ensure we received at least the limit we placed
528+
if (quantityReceived < inputQuantity) {
529+
revert RECEIVED_LESS_THAN_LIMIT();
530+
}
531+
}
532+
500533
/**
501534
* @dev Function to attempt finalising of as many interactions as possible within the specified gas limit
502535
* Continue checking for and finalising interactions until we expend the available gas
@@ -510,7 +543,7 @@ contract ElementBridge is IDefiBridge {
510543
while (gasleft() > gasLoopCondition) {
511544
// check the heap to see if we can finalise an expired transaction
512545
// we provide a gas floor to the function which will enable us to leave this function without breaching our gasFloor
513-
(bool expiryAvailable, uint256 nonce) = checkNextExpiry(ourGasFloor);
546+
(bool expiryAvailable, uint256 nonce) = checkForNextInteractionToFinalise(ourGasFloor);
514547
if (!expiryAvailable) {
515548
break;
516549
}
@@ -570,7 +603,7 @@ contract ElementBridge is IDefiBridge {
570603
if (trancheAccount.numDeposits == 0) {
571604
// shouldn't be possible, this means we have had no deposits against this tranche
572605
setInteractionAsFailure(interaction, interactionNonce, 'NO_DEPOSITS_FOR_TRANCHE');
573-
popInteraction(interaction, interactionNonce);
606+
popInteractionFromNonceMapping(interaction, interactionNonce);
574607
return (0, 0, false);
575608
}
576609

@@ -586,12 +619,12 @@ contract ElementBridge is IDefiBridge {
586619
} catch Error(string memory errorMessage) {
587620
setInteractionAsFailure(interaction, interactionNonce, errorMessage);
588621
trancheAccount.redemptionStatus = TrancheRedemptionStatus.REDEMPTION_FAILED;
589-
popInteraction(interaction, interactionNonce);
622+
popInteractionFromNonceMapping(interaction, interactionNonce);
590623
return (0, 0, false);
591624
} catch {
592625
setInteractionAsFailure(interaction, interactionNonce, 'UNKNOWN_ERROR_FROM_TRANCHE_WITHDRAW');
593626
trancheAccount.redemptionStatus = TrancheRedemptionStatus.REDEMPTION_FAILED;
594-
popInteraction(interaction, interactionNonce);
627+
popInteractionFromNonceMapping(interaction, interactionNonce);
595628
return (0, 0, false);
596629
}
597630
}
@@ -606,7 +639,9 @@ contract ElementBridge is IDefiBridge {
606639
amountToAllocate = trancheAccount.quantityAssetRedeemed / trancheAccount.numDeposits;
607640
} else {
608641
// apportion the output asset based on the interaction's holding of the principle token
609-
amountToAllocate = (trancheAccount.quantityAssetRedeemed * interaction.quantityPT) / trancheAccount.quantityTokensHeld;
642+
// protects against phantom overflow in the operation of
643+
// amountToAllocate = (trancheAccount.quantityAssetRedeemed * interaction.quantityPT) / trancheAccount.quantityTokensHeld;
644+
amountToAllocate = FullMath.mulDiv(trancheAccount.quantityAssetRedeemed, interaction.quantityPT, trancheAccount.quantityTokensHeld);
610645
}
611646
// numDeposits and numFinalised are uint32 types, so easily within range for an int256
612647
int256 numRemainingInteractionsForTranche = int256(uint256(trancheAccount.numDeposits)) - int256(uint256(trancheAccount.numFinalised));
@@ -622,11 +657,11 @@ contract ElementBridge is IDefiBridge {
622657
// approve the transfer of funds back to the rollup contract
623658
ERC20(outputAssetA.erc20Address).approve(rollupProcessor, amountToAllocate);
624659
interaction.finalised = true;
625-
popInteraction(interaction, interactionNonce);
660+
popInteractionFromNonceMapping(interaction, interactionNonce);
626661
outputValueA = amountToAllocate;
627662
outputValueB = 0;
628663
interactionCompleted = true;
629-
emit Finalise(interactionNonce, interactionCompleted, '');
664+
emit LogFinalise(interactionNonce, interactionCompleted, '');
630665
}
631666

632667
/**
@@ -641,7 +676,7 @@ contract ElementBridge is IDefiBridge {
641676
string memory message
642677
) internal {
643678
interaction.failed = true;
644-
emit Finalise(interactionNonce, false, message);
679+
emit LogFinalise(interactionNonce, false, message);
645680
}
646681

647682
/**
@@ -650,12 +685,12 @@ contract ElementBridge is IDefiBridge {
650685
* @param expiry The expiry of the interaction to be added
651686
* @return expiryAdded Flag specifying whether the interactions expiry was added to the heap
652687
*/
653-
function addNonceAndExpiry(uint256 nonce, uint64 expiry) internal returns (bool expiryAdded) {
688+
function addNonceAndExpiryToNonceMapping(uint256 nonce, uint64 expiry) internal returns (bool expiryAdded) {
654689
// get the set of nonces already against this expiry
655-
// check for the MAX_INT placeholder nonce that exists to reduce gas costs at this point in the code
690+
// check for the MAX_UINT placeholder nonce that exists to reduce gas costs at this point in the code
656691
expiryAdded = false;
657692
uint256[] storage nonces = expiryToNonce[expiry];
658-
if (nonces.length == 1 && nonces[0] == MAX_INT) {
693+
if (nonces.length == 1 && nonces[0] == MAX_UINT) {
659694
nonces[0] = nonce;
660695
} else {
661696
nonces.push(nonce);
@@ -674,7 +709,7 @@ contract ElementBridge is IDefiBridge {
674709
* @param interactionNonce The nonce of the interaction to be removed
675710
* @return expiryRemoved Flag specifying whether the interactions expiry was removed from the heap
676711
*/
677-
function popInteraction(Interaction storage interaction, uint256 interactionNonce) internal returns (bool expiryRemoved) {
712+
function popInteractionFromNonceMapping(Interaction storage interaction, uint256 interactionNonce) internal returns (bool expiryRemoved) {
678713
uint256[] storage nonces = expiryToNonce[interaction.expiry];
679714
if (nonces.length == 0) {
680715
return (false);
@@ -706,7 +741,7 @@ contract ElementBridge is IDefiBridge {
706741
* @return expiryAvailable Flag specifying whether an expiry is available to be finalised
707742
* @return nonce The next interaction nonce to be finalised
708743
*/
709-
function checkNextExpiry(uint256 gasFloor)
744+
function checkForNextInteractionToFinalise(uint256 gasFloor)
710745
internal
711746
returns (
712747
bool expiryAvailable,
@@ -728,7 +763,7 @@ contract ElementBridge is IDefiBridge {
728763
uint256 minGasForLoop = (gasFloor + MIN_GAS_FOR_FAILED_INTERACTION);
729764
while (nonces.length > 0 && gasleft() >= minGasForLoop) {
730765
uint256 nextNonce = nonces[nonces.length - 1];
731-
if (nextNonce == MAX_INT) {
766+
if (nextNonce == MAX_UINT) {
732767
// this shouldn't happen, this value is the placeholder for reducing gas costs on convert
733768
// we just need to pop and continue
734769
nonces.pop();

src/bridges/element/MinHeap.sol

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ library MinHeap {
1010
error HEAP_EMPTY();
1111

1212
// maximum value of uint64. used as initial value in pre-allocated array
13-
uint64 internal constant MAX_INT = 2**64 - 1;
13+
uint64 internal constant MAX_INT = type(uint64).max;
1414

1515
/**
16-
* @dev Encapsulates the underlyding data structure used to manage the heap
16+
* @dev Encapsulates the underlying data structure used to manage the heap
1717
*
1818
* @param heap the array of values contained within the heap
1919
* @param heapSize the current size of the heap, usually different to the size of underlying array so tracked seperately
@@ -64,7 +64,7 @@ library MinHeap {
6464
* @param value the value to be removed
6565
*/
6666
function remove(MinHeapData storage self, uint64 value) internal {
67-
removeExpiryFromHeap(self, value);
67+
removeValueFromHeap(self, value);
6868
}
6969

7070
/**
@@ -110,8 +110,6 @@ library MinHeap {
110110
function addToHeap(MinHeapData storage self, uint64 value) private {
111111
// standard min-heap insertion
112112
// push to the end of the heap and sift up.
113-
// there is a high probability that the expiry being added will remain where it is
114-
// so this operation will end up being O(1)
115113
if (self.heapSize == self.heap.length) {
116114
self.heap.push(value);
117115
} else {
@@ -170,7 +168,7 @@ library MinHeap {
170168
* @param self reference to the underlying data structure of the heap
171169
* @param value the value to be removed
172170
*/
173-
function removeExpiryFromHeap(MinHeapData storage self, uint64 value) private {
171+
function removeValueFromHeap(MinHeapData storage self, uint64 value) private {
174172
uint256 index = 0;
175173
while (index < self.heapSize && self.heap[index] != value) {
176174
++index;

0 commit comments

Comments
 (0)