Skip to content

Commit

Permalink
feat: minting in L1 gateway with allowance
Browse files Browse the repository at this point in the history
  • Loading branch information
pcarranzav committed Sep 1, 2022
1 parent 3246896 commit ba07101
Show file tree
Hide file tree
Showing 2 changed files with 282 additions and 4 deletions.
110 changes: 108 additions & 2 deletions contracts/gateway/L1GraphTokenGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
address public escrow;
// Addresses for which this mapping is true are allowed to send callhooks in outbound transfers
mapping(address => bool) public callhookWhitelist;
// Total amount minted from L2
uint256 public totalMintedFromL2;
// Accumulated allowance for tokens minted from L2 at lastL2MintAllowanceUpdateBlock
uint256 public accumulatedL2MintAllowanceSnapshot;
// Block at which new L2 allowance starts accumulating
uint256 public lastL2MintAllowanceUpdateBlock;
// New L2 mint allowance per block
uint256 public l2MintAllowancePerBlock;

// Emitted when an outbound transfer is initiated, i.e. tokens are deposited from L1 to L2
event DepositInitiated(
Expand Down Expand Up @@ -64,6 +72,14 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
event AddedToCallhookWhitelist(address newWhitelisted);
// Emitted when an address is removed from the callhook whitelist
event RemovedFromCallhookWhitelist(address notWhitelisted);
// Emitted when the L2 mint allowance per block is updated
event L2MintAllowanceUpdated(
uint256 accumulatedL2MintAllowanceSnapshot,
uint256 l2MintAllowancePerBlock,
uint256 lastL2MintAllowanceUpdateBlock
);
// Emitted when tokens are minted due to an incoming transfer from L2
event TokensMintedFromL2(uint256 amount);

/**
* @dev Allows a function to be called only by the gateway's L2 counterpart.
Expand Down Expand Up @@ -166,6 +182,56 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
emit RemovedFromCallhookWhitelist(_notWhitelisted);
}

/**
* @dev Updates the L2 mint allowance per block
* It is meant to be called _after_ the issuancePerBlock is updated in L2.
* The caller should provide the new issuance per block and the block at which it was updated,
* the function will automatically compute the values so that the bridge's mint allowance
* correctly tracks the maximum rewards minted in L2.
* @param _l2IssuancePerBlock New issuancePerBlock that has been set in L2
* @param _updateBlockNum L1 Block number at which issuancePerBlock was updated in L2
*/
function updateL2MintAllowance(uint256 _l2IssuancePerBlock, uint256 _updateBlockNum)
external
onlyGovernor
{
require(_updateBlockNum <= block.number, "BLOCK_CANT_BE_FUTURE");
require(_updateBlockNum > lastL2MintAllowanceUpdateBlock, "BLOCK_MUST_BE_INCREMENTING");
accumulatedL2MintAllowanceSnapshot = accumulatedL2MintAllowanceAtBlock(_updateBlockNum);
lastL2MintAllowanceUpdateBlock = _updateBlockNum;
l2MintAllowancePerBlock = _l2IssuancePerBlock;
emit L2MintAllowanceUpdated(
accumulatedL2MintAllowanceSnapshot,
l2MintAllowancePerBlock,
lastL2MintAllowanceUpdateBlock
);
}

/**
* @dev Manually sets the parameters used to compute the L2 mint allowance
* The use of this function is not recommended, use updateL2MintAllowance instead;
* this one is only meant to be used as a backup recovery if a previous call to
* updateL2MintAllowance was done with incorrect values.
* @param _accumulatedL2MintAllowanceSnapshot Accumulated L2 mint allowance at L1 block _lastL2MintAllowanceUpdateBlock
* @param _l2MintAllowancePerBlock L2 issuance per block since block number _lastL2MintAllowanceUpdateBlock
* @param _lastL2MintAllowanceUpdateBlock L1 Block number at which issuancePerBlock was last updated in L2
*/
function setL2MintAllowanceParametersManual(
uint256 _accumulatedL2MintAllowanceSnapshot,
uint256 _l2MintAllowancePerBlock,
uint256 _lastL2MintAllowanceUpdateBlock
) external onlyGovernor {
require(_lastL2MintAllowanceUpdateBlock <= block.number, "BLOCK_CANT_BE_FUTURE");
accumulatedL2MintAllowanceSnapshot = _accumulatedL2MintAllowanceSnapshot;
l2MintAllowancePerBlock = _l2MintAllowancePerBlock;
lastL2MintAllowanceUpdateBlock = _lastL2MintAllowanceUpdateBlock;
emit L2MintAllowanceUpdated(
accumulatedL2MintAllowanceSnapshot,
l2MintAllowancePerBlock,
lastL2MintAllowanceUpdateBlock
);
}

/**
* @notice Creates and sends a retryable ticket to transfer GRT to L2 using the Arbitrum Inbox.
* The tokens are escrowed by the gateway until they are withdrawn back to L1.
Expand Down Expand Up @@ -269,8 +335,10 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
require(_l1Token == address(token), "TOKEN_NOT_GRT");

uint256 escrowBalance = token.balanceOf(escrow);
// If the bridge doesn't have enough tokens, something's very wrong!
require(_amount <= escrowBalance, "BRIDGE_OUT_OF_FUNDS");
if (_amount > escrowBalance) {
// This will revert if trying to mint more than allowed
_mintFromL2(_amount.sub(escrowBalance));
}
token.transferFrom(escrow, _to, _amount);

emit WithdrawalFinalized(_l1Token, _from, _to, 0, _amount);
Expand Down Expand Up @@ -354,4 +422,42 @@ contract L1GraphTokenGateway is GraphTokenGateway, L1ArbitrumMessenger {
}
return l2GRT;
}

/**
* @dev Get the accumulated L2 mint allowance at a particular block number
* @param _block Block at which allowance will be computed
* @return The accumulated GRT amount that can be minted from L2 at the specified block
*/
function accumulatedL2MintAllowanceAtBlock(uint256 _block) public view returns (uint256) {
require(_block >= lastL2MintAllowanceUpdateBlock, "INVALID_BLOCK_FOR_MINT_ALLOWANCE");
return
accumulatedL2MintAllowanceSnapshot.add(
l2MintAllowancePerBlock.mul(_block.sub(lastL2MintAllowanceUpdateBlock))
);
}

/**
* @dev Mint new L1 tokens coming from L2
* This will check if the amount to mint is within the L2's mint allowance, and revert otherwise.
* The tokens will be sent to the bridge escrow (from where they will then be sent to the destinatary
* of the current inbound transfer).
* @param _amount Number of tokens to mint
*/
function _mintFromL2(uint256 _amount) internal {
// If we're trying to mint more than allowed, something's gone terribly wrong
// (either the L2 issuance is wrong, or the Arbitrum bridge has been compromised)
require(_l2MintAmountAllowed(_amount), "INVALID_L2_MINT_AMOUNT");
totalMintedFromL2 = totalMintedFromL2.add(_amount);
graphToken().mint(escrow, _amount);
emit TokensMintedFromL2(_amount);
}

/**
* @dev Check if minting a certain amount of tokens from L2 is within allowance
* @param _amount Number of tokens that would be minted
* @return true if minting those tokens is allowed, or false if it would be over allowance
*/
function _l2MintAmountAllowed(uint256 _amount) internal view returns (bool) {
return (totalMintedFromL2.add(_amount) <= accumulatedL2MintAllowanceAtBlock(block.number));
}
}
176 changes: 174 additions & 2 deletions test/gateway/l1GraphTokenGateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
toGRT,
Account,
applyL1ToL2Alias,
advanceBlocks,
} from '../lib/testHelpers'
import { BridgeEscrow } from '../../build/types/BridgeEscrow'

Expand Down Expand Up @@ -374,6 +375,175 @@ describe('L1GraphTokenGateway', () => {
)
})

describe('updateL2MintAllowance', function () {
it('rejects calls that are not from the governor', async function () {
const tx = l1GraphTokenGateway
.connect(pauseGuardian.address)
.updateL2MintAllowance(toGRT('1'), await latestBlock())
await expect(tx).revertedWith('Caller must be Controller governor')
})
it('does not allow using a future block number', async function () {
const issuancePerBlock = toGRT('120')
let issuanceUpdatedAtBlock = (await latestBlock()).add(2)
const tx1 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx1).revertedWith('BLOCK_CANT_BE_FUTURE')
issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx
const tx2 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx2)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
})
it('does not allow using a block number lower than or equal to the previous one', async function () {
const issuancePerBlock = toGRT('120')
const issuanceUpdatedAtBlock = await latestBlock()
const tx1 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx1)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
const tx2 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx2).revertedWith('BLOCK_MUST_BE_INCREMENTING')
const tx3 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.sub(1))
await expect(tx3).revertedWith('BLOCK_MUST_BE_INCREMENTING')
const tx4 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock.add(1))
await expect(tx4)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(issuancePerBlock, issuancePerBlock, issuanceUpdatedAtBlock.add(1))
})
it('updates the snapshot and issuance to follow a new linear function, accumulating up to the specified block', async function () {
const issuancePerBlock = toGRT('120')
const issuanceUpdatedAtBlock = (await latestBlock()).sub(2)
const tx1 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx1)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
// Now the mint allowance should be issuancePerBlock * 3
await expect(
await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()),
).to.eq(issuancePerBlock.mul(3))
await expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(0)
await expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock)
await expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(
issuanceUpdatedAtBlock,
)

await advanceBlocks(10)

const newIssuancePerBlock = toGRT('200')
const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1)

const expectedAccumulatedSnapshot = issuancePerBlock.mul(
newIssuanceUpdatedAtBlock.sub(issuanceUpdatedAtBlock),
)
const tx2 = l1GraphTokenGateway
.connect(governor.signer)
.updateL2MintAllowance(newIssuancePerBlock, newIssuanceUpdatedAtBlock)
await expect(tx2)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(expectedAccumulatedSnapshot, newIssuancePerBlock, newIssuanceUpdatedAtBlock)

await expect(
await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()),
).to.eq(expectedAccumulatedSnapshot.add(newIssuancePerBlock.mul(2)))
await expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(
expectedAccumulatedSnapshot,
)
await expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock)
await expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(
newIssuanceUpdatedAtBlock,
)
})
})
describe('setL2MintAllowanceParametersManual', function () {
it('rejects calls that are not from the governor', async function () {
const tx = l1GraphTokenGateway
.connect(pauseGuardian.address)
.setL2MintAllowanceParametersManual(toGRT('0'), toGRT('1'), await latestBlock())
await expect(tx).revertedWith('Caller must be Controller governor')
})
it('does not allow using a future block number', async function () {
const issuancePerBlock = toGRT('120')
let issuanceUpdatedAtBlock = (await latestBlock()).add(2)
const tx1 = l1GraphTokenGateway
.connect(governor.signer)
.setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx1).revertedWith('BLOCK_CANT_BE_FUTURE')
issuanceUpdatedAtBlock = (await latestBlock()).add(1) // This will be block.number in our next tx
const tx2 = l1GraphTokenGateway
.connect(governor.signer)
.setL2MintAllowanceParametersManual(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
await expect(tx2)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(toGRT('0'), issuancePerBlock, issuanceUpdatedAtBlock)
})
it('updates the snapshot and issuance to follow a new linear function, manually setting the snapshot value', async function () {
const issuancePerBlock = toGRT('120')
const issuanceUpdatedAtBlock = (await latestBlock()).sub(2)
const snapshotValue = toGRT('10')
const tx1 = l1GraphTokenGateway
.connect(governor.signer)
.setL2MintAllowanceParametersManual(
snapshotValue,
issuancePerBlock,
issuanceUpdatedAtBlock,
)
await expect(tx1)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(snapshotValue, issuancePerBlock, issuanceUpdatedAtBlock)
// Now the mint allowance should be 10 + issuancePerBlock * 3
await expect(
await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()),
).to.eq(snapshotValue.add(issuancePerBlock.mul(3)))
await expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(
snapshotValue,
)
await expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(issuancePerBlock)
await expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(
issuanceUpdatedAtBlock,
)

await advanceBlocks(10)

const newIssuancePerBlock = toGRT('200')
const newIssuanceUpdatedAtBlock = (await latestBlock()).sub(1)
const newSnapshotValue = toGRT('10')

const tx2 = l1GraphTokenGateway
.connect(governor.signer)
.setL2MintAllowanceParametersManual(
newSnapshotValue,
newIssuancePerBlock,
newIssuanceUpdatedAtBlock,
)
await expect(tx2)
.emit(l1GraphTokenGateway, 'L2MintAllowanceUpdated')
.withArgs(newSnapshotValue, newIssuancePerBlock, newIssuanceUpdatedAtBlock)

await expect(
await l1GraphTokenGateway.accumulatedL2MintAllowanceAtBlock(await latestBlock()),
).to.eq(newSnapshotValue.add(newIssuancePerBlock.mul(2)))
await expect(await l1GraphTokenGateway.accumulatedL2MintAllowanceSnapshot()).to.eq(
newSnapshotValue,
)
await expect(await l1GraphTokenGateway.l2MintAllowancePerBlock()).to.eq(newIssuancePerBlock)
await expect(await l1GraphTokenGateway.lastL2MintAllowanceUpdateBlock()).to.eq(
newIssuanceUpdatedAtBlock,
)
})
})
describe('calculateL2TokenAddress', function () {
it('returns the L2 token address', async function () {
expect(await l1GraphTokenGateway.calculateL2TokenAddress(grt.address)).eq(mockL2GRT.address)
Expand Down Expand Up @@ -519,7 +689,7 @@ describe('L1GraphTokenGateway', () => {
)
await expect(tx).revertedWith('ONLY_COUNTERPART_GATEWAY')
})
it('reverts if the gateway does not have tokens', async function () {
it('reverts if the gateway does not have tokens or allowance', async function () {
// This scenario should never really happen, but we still
// test that the gateway reverts in this case
const encodedCalldata = l1GraphTokenGateway.interface.encodeFunctionData(
Expand Down Expand Up @@ -549,7 +719,7 @@ describe('L1GraphTokenGateway', () => {
toBN('0'),
encodedCalldata,
)
await expect(tx).revertedWith('BRIDGE_OUT_OF_FUNDS')
await expect(tx).revertedWith('INVALID_L2_MINT_AMOUNT')
})
it('reverts if the gateway is revoked from escrow', async function () {
await grt.connect(tokenSender.signer).approve(l1GraphTokenGateway.address, toGRT('10'))
Expand Down Expand Up @@ -625,6 +795,8 @@ describe('L1GraphTokenGateway', () => {
await expect(escrowBalance).eq(toGRT('2'))
await expect(senderBalance).eq(toGRT('998'))
})
it('mints tokens up to the L2 mint allowance')
it('reverts if the amount to mint is over the allowance')
})
})
})

0 comments on commit ba07101

Please sign in to comment.