-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Permissioned Across-v2 bond token #226
Conversation
This PR creates a new bond token, to be used as the collateral required by the HubPool on the following functions: - proposeRootBundle() - disputeRootBundle() The new bond token (symbol "ABT") hosts an internal set of approved proposer addresses in order to impose constraints on attempted inbound ABT transfers to the HubPool. Permissioned addresses are unconstrained in their transfer of ABT, whereas all other addresses must _not_ be the proposer of a root bundle in order to transfer ABT to the HubPool. This permits any ABT holder to submit a dispute of an existing root bundle proposal, whilst preventing non-approved addresses from proposing. ABT inherits from the WETH9 contract with minimal modifications necessary to override the transferFrom() function.
contracts/BondToken.sol
Outdated
import "./HubPoolInterface.sol"; | ||
import "./WETH9.sol"; | ||
|
||
interface _HubPool is HubPoolInterface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer sticking to existing naming convention and not prefixing this with a _. Maybe name HubPoolExtendedInterface or something, or even IHubPool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - I'll find something better 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented: 91c1780
contracts/BondToken.sol
Outdated
* @param _hubPool Address of the target HubPool contract. | ||
*/ | ||
constructor(_HubPool _hubPool) { | ||
name = "Across v2 Bond Token"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name = "Across v2 Bond Token"; | |
name = "Across Bond Token"; |
Not sure V2 is necessary. Do you think we should name it Proposer Bond instead of just Bond?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preface: Naming things is hard :)
My thinking with the "v2" naming is that this token has a finite lifespan - it will only be in use as long as the current (Across v2) HubPool is deployed (nb. the HubPool address is immutable, per current implementation). That's why I preferred to include v2 in the name.
I'd prefer not to include "Proposer" in the name, since this token is also needed to dispute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally don't like Versions in token names, look at UMA Voting Token V1 for example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"v2" removed: ee0aedb
I do think it's a different scenario to UMA v1, but I can live with either :)
*/ | ||
constructor(_HubPool _hubPool) { | ||
name = "Across v2 Bond Token"; | ||
symbol = "ABT"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we come up with a catchier symbol? :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"AMT" => "Across Moon Token" ?
(Slightly) more seriously:
APRT => Across Proposer Responsibility Token
APPT => Across Permissioned Proposer Token
APDT => Across Permisionless Dispute Token
AHST => Across HubPool Security Token
IDK 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will think on this more haha
uint256 amt | ||
) public override returns (bool) { | ||
if (dst == address(hubPool)) { | ||
require(proposers[src] || hubPool.rootBundleProposal().proposer != src, "Transfer not permitted"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean proposer's can't dispute themselves? I don't think we should block such behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check only requires that one of the conditions is true, so approved proposers can always self-dispute without impediment (and, all other users can dispute as well). There's a test that verifies that - see test/BondToken.e2e.ts line 72.
The statement hubPool.rootBundleProposal().proposer != src
does seem counter-intuitive at first. It's there based on the observation that when a non-approved proposer is attempting to propose, their address is recorded as rootBundleProposal.proposer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is extremely counterintuitive, but it's really clever. This is the sort of logic that I think Certora would be really good for verifying. We would probably want to verify that any address with tokens can always send a dispute.
I'm not super worried about this edge case, but this logic does seem to mean that if I proposed and then I was removed from the whitelist, I would then be no longer able to dispute myself.
Just out of curiousity, why did you not use the the check we use in the HubPool, that unclaimedPoolRebalanceLeafCount != 0 to detect disputes vs proposals. See the noActiveRequests modifier.
EDIT: That last idea doesn't work. I didn't quite get the nuance you picked up on, that the variables are written before the transfer. I think the way you've done it or checking the timestamp is probably the only way I could see this working. Very clever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super worried about this edge case, but this logic does seem to mean that if I proposed and then I was removed from the whitelist, I would then be no longer able to dispute myself.
That was also my intuition, but then I tried to implement a test for it and found the opposite - it works unexpectedly. This is similarly because the pending root bundle is deleted before the bond token transferFrom() function is called, so the disputer is no longer the proposer, even though both the proposal and dispute were submitted by the same address. It's a happy accident, I guess.
For reference, see the test at line 78 in test/BondToken.e2e.ts.
Just out of curiousity, why did you not use the the check we use in the HubPool, that unclaimedPoolRebalanceLeafCount != 0 to detect disputes vs proposals. See the noActiveRequests modifier.
That was my first attempt as well, but it doesn't work (edit: As mentioned in your update 👍). At the time of evaluation, the proposed root bundle is the version submitted by the proposer, so it seems reliably non-zero in case of both proposals and disputes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is extremely counterintuitive
I've added a dev comment to the BondToken.safeTransferFrom() implementation to call this out more explicitly. See 5215b60.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the same question as Matt but see your point now!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, it keeps getting weirder @pxrl. Really would love some validation on this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a once over and left some comments
Comment from Nick.
For compliance with GPLv3 sections 4. and 5: https://www.gnu.org/licenses/gpl-3.0.html
And relocate the pragma.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a really clever implementation. Nice work, @pxrl!
uint256 amt | ||
) public override returns (bool) { | ||
if (dst == address(hubPool)) { | ||
require(proposers[src] || hubPool.rootBundleProposal().proposer != src, "Transfer not permitted"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is extremely counterintuitive, but it's really clever. This is the sort of logic that I think Certora would be really good for verifying. We would probably want to verify that any address with tokens can always send a dispute.
I'm not super worried about this edge case, but this logic does seem to mean that if I proposed and then I was removed from the whitelist, I would then be no longer able to dispute myself.
Just out of curiousity, why did you not use the the check we use in the HubPool, that unclaimedPoolRebalanceLeafCount != 0 to detect disputes vs proposals. See the noActiveRequests modifier.
EDIT: That last idea doesn't work. I didn't quite get the nuance you picked up on, that the variables are written before the transfer. I think the way you've done it or checking the timestamp is probably the only way I could see this working. Very clever.
contracts/WETH9.sol
Outdated
return transferFrom(msg.sender, dst, wad); | ||
} | ||
|
||
function transferFrom( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we add a comment that this is the only difference between this contract and the real WETH9?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added here: edbee53.
contracts/WETH9.sol
Outdated
|
||
pragma solidity ^0.8.0; | ||
|
||
contract WETH9 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we probably don't want two contracts in the repo called WETH9 -- this causes issues in hardhat I think. Any reason we can't just delete the interface or rename it to WETH9Interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point - I'd totally missed the existing interface. I've split this out - see #227.
Make way a copy of the WETH9 implementation to be imported.
Import the full contract so that it can be tweaked and used as the base contract for the Across Bond Token (name subject to change). The import is made as-is, with no changes. Follow-up commits will apply modifications as required.
- Fix SPDX license identifier: AGPL-3.0-only => GPL-3.0-or-later - Reinstate WETH9 license header for compliance with GPLv3 sections 4. and 5: https://www.gnu.org/licenses/gpl-3.0.html - Relocate the pragma.
Signed-off-by: Paul <108695806+pxrl@users.noreply.github.com>
Note: We should remember to seed the HubPool with a tiny balance to save gas on each proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
uint256 amt | ||
) public override returns (bool) { | ||
if (dst == address(hubPool)) { | ||
require(proposers[src] || hubPool.rootBundleProposal().proposer != src, "Transfer not permitted"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, it keeps getting weirder @pxrl. Really would love some validation on this!
This PR creates a new bond token, to be used as the collateral required by the HubPool on the following functions:
The new bond token (symbol "ABT") hosts an internal set of approved proposer addresses in order to impose constraints on attempted inbound ABT transfers to the HubPool. Permissioned addresses are unconstrained in their transfer of ABT, whereas all other addresses must not be the proposer of a root bundle in order to transfer ABT to the HubPool. This permits any ABT holder to submit a dispute of an existing root bundle proposal, whilst preventing non-approved addresses from proposing.
ABT inherits from the WETH9 contract with minimal modifications necessary to override the transferFrom() function.
Fixes ACX-699 ACX-700